{"record":{"id":"88e8c48825ec9e23","repo":"ruvnet/ruflo","slug":"invalid-approval","errorCode":null,"errorMessage":"invalid-approval","messagePattern":"invalid-approval","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/security/src/policy/engine.ts","lineNumber":119,"sourceCode":"    if (index >= 0) this.state.budgets[index] = structuredClone(limit);\n    else this.state.budgets.push(structuredClone(limit));\n  }\n\n  issueApproval(approval: Omit<PolicyApproval, 'uses' | 'issuedAt'> & { uses?: number; issuedAt?: number }): PolicyApproval {\n    if (approval.issuedBy === approval.principal) throw new Error('self-approval-forbidden');\n    if (this.approvalIssuerVerifier?.(approval.issuedBy) !== true) {\n      throw new Error('untrusted-approval-issuer');\n    }\n    const issuedAt = approval.issuedAt ?? this.now();\n    const record: PolicyApproval = { ...approval, issuedAt, uses: approval.uses ?? 0 };\n    if (this.state.approvals.some((item) => item.id === record.id)) throw new Error('duplicate-approval-id');\n    if (!record.id\n      || record.expiresAt <= issuedAt\n      || !Number.isInteger(record.maxUses)\n      || record.maxUses <= 0\n      || !Number.isInteger(record.uses)\n      || record.uses < 0\n      || record.uses > record.maxUses) throw new Error('invalid-approval');\n    this.state.approvals.push(record);\n    return structuredClone(record);\n  }\n\n  revokeApproval(id: string): boolean {\n    const approval = this.state.approvals.find((item) => item.id === id);\n    if (!approval || approval.revokedAt) return false;\n    approval.revokedAt = this.now();\n    return true;\n  }\n\n  evaluate(request: PolicyRequest): PolicyDecision {\n    const normalized: PolicyRequest = {\n      ...request,\n      requestId: request.requestId ?? crypto.randomUUID(),\n      // Caller time is evidence only; expiry, budgets, and receipts always use\n      // the authority's clock.\n      context: { ...request.context, now: this.now() },","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/security/src/policy/engine.ts#L101-L137","documentation":"The final shape check in issueApproval(): after issuer checks pass, the record must have a truthy id, expiresAt strictly greater than issuedAt, an integer maxUses >= 1, and an integer uses in [0, maxUses]. Anything else throws Error('invalid-approval'). Note issuedAt defaults to engine now() when omitted.","triggerScenarios":"expiresAt supplied in seconds while issuedAt is epoch milliseconds (looks already-expired); expiresAt === issuedAt (TTL of zero); maxUses: 0 used to mean 'unlimited'; uses > maxUses when replaying a heavily-consumed approval into a new engine.","commonSituations":"Mixing seconds/milliseconds timestamps between systems; rehydrating approvals from persistence where counters were corrupted; hand-built approval fixtures in tests with placeholder zeros.","solutions":["Compute expiresAt as Date.now() + ttlMs (milliseconds) so it strictly exceeds issuedAt.","Use an integer maxUses >= 1; if you want unlimited uses, set a large finite cap — zero is invalid.","Validate persisted approval records (integer counters, expiry ordering) before re-issuing them into the engine."],"exampleFix":"// before\nengine.issueApproval({ id, issuedBy, principal, expiresAt: 1893456000, maxUses: 0, ... });\n\n// after\nengine.issueApproval({\n  id,\n  issuedBy,\n  principal,\n  expiresAt: Date.now() + 60 * 60_000,\n  maxUses: 10,\n  ...,\n});","handlingStrategy":"validation","validationCode":"function validApproval(a: { id?: string; expiresAt?: number; maxUses?: number; uses?: number }): boolean {\n  return !!a.id\n    && Number.isInteger(a.maxUses) && a.maxUses > 0\n    && Number.isInteger(a.uses) && a.uses >= 0 && a.uses <= a.maxUses\n    && a.expiresAt > Date.now();\n}\nif (!validApproval(approval)) throw new Error('approval record failed pre-check');","typeGuard":"function isWellFormedApproval(a: unknown): a is Omit<PolicyApproval, 'uses' | 'issuedAt'> {\n  const rec = a as any;\n  return typeof rec?.id === 'string' && rec.id.length > 0\n    && Number.isInteger(rec.maxUses) && rec.maxUses > 0\n    && Number.isInteger(rec.expiresAt) && rec.expiresAt > Date.now();\n}","tryCatchPattern":"try {\n  return engine.issueApproval(approval);\n} catch (err) {\n  if (err instanceof Error && err.message === 'invalid-approval') {\n    throw new BadRequest('approval needs id, future expiresAt (ms), integer maxUses>=1, 0<=uses<=maxUses');\n  }\n  throw err;\n}","preventionTips":["Always compute expiresAt as Date.now() + ttlMs in milliseconds — never seconds.","Treat maxUses as a required positive integer; 'unlimited' is a large finite cap, not 0.","Validate approvals rehydrated from persistence (counters/expiry) before re-issuing into a fresh engine."],"tags":["policy","approval","validation","timestamps"],"backgroundTag":"schema-validation-failed","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}