@yiaany/ghostapi 0.1.9 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/CONTRIBUTING.md +132 -119
  3. package/LICENSE +21 -21
  4. package/PROVENANCE.md +13 -0
  5. package/README.md +78 -84
  6. package/SECURITY.md +56 -53
  7. package/dist/actions/gateway.js +320 -72
  8. package/dist/actions/gateway.js.map +1 -1
  9. package/dist/actions/index.d.ts +2 -2
  10. package/dist/actions/index.js +1 -1
  11. package/dist/actions/index.js.map +1 -1
  12. package/dist/agents/agentPrompt.js +7 -5
  13. package/dist/agents/agentPrompt.js.map +1 -1
  14. package/dist/ai/aiClient.js +5 -5
  15. package/dist/ai/aiClient.js.map +1 -1
  16. package/dist/ai/aiGenerator.js +66 -26
  17. package/dist/ai/aiGenerator.js.map +1 -1
  18. package/dist/ai/genericInference.js +151 -36
  19. package/dist/ai/genericInference.js.map +1 -1
  20. package/dist/ai/prompts.js +55 -49
  21. package/dist/ai/prompts.js.map +1 -1
  22. package/dist/approvals/approvalInbox.js +649 -133
  23. package/dist/approvals/approvalInbox.js.map +1 -1
  24. package/dist/approvals/index.d.ts +2 -2
  25. package/dist/approvals/index.js +1 -1
  26. package/dist/approvals/index.js.map +1 -1
  27. package/dist/behavior/behaviorStore.js +34 -14
  28. package/dist/behavior/behaviorStore.js.map +1 -1
  29. package/dist/cache/index.js +14 -5
  30. package/dist/cache/index.js.map +1 -1
  31. package/dist/cli/index.js +226 -93
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/cli/openUrl.js +17 -3
  34. package/dist/cli/openUrl.js.map +1 -1
  35. package/dist/cli/parser.js +79 -18
  36. package/dist/cli/parser.js.map +1 -1
  37. package/dist/config/dataPaths.js +4 -2
  38. package/dist/config/dataPaths.js.map +1 -1
  39. package/dist/config/localConfig.js +5 -2
  40. package/dist/config/localConfig.js.map +1 -1
  41. package/dist/config/serverConfig.js +11 -5
  42. package/dist/config/serverConfig.js.map +1 -1
  43. package/dist/contracts/contract.js +311 -83
  44. package/dist/contracts/contract.js.map +1 -1
  45. package/dist/contracts/index.d.ts +2 -2
  46. package/dist/contracts/index.js +1 -1
  47. package/dist/contracts/index.js.map +1 -1
  48. package/dist/credentials/credentialBroker.js +391 -79
  49. package/dist/credentials/credentialBroker.js.map +1 -1
  50. package/dist/credentials/index.d.ts +2 -2
  51. package/dist/credentials/index.js +1 -1
  52. package/dist/credentials/index.js.map +1 -1
  53. package/dist/dashboard/app.js +823 -685
  54. package/dist/dashboard/dashboard.js.map +1 -1
  55. package/dist/dashboard/index.html +704 -267
  56. package/dist/dashboard/styles.css +1040 -349
  57. package/dist/egress/capabilities.js +55 -25
  58. package/dist/egress/capabilities.js.map +1 -1
  59. package/dist/egress/linuxBootstrap.js +47 -11
  60. package/dist/egress/linuxBootstrap.js.map +1 -1
  61. package/dist/egress/run.js +170 -31
  62. package/dist/egress/run.js.map +1 -1
  63. package/dist/errors/errorEngine.js +15 -3
  64. package/dist/errors/errorEngine.js.map +1 -1
  65. package/dist/errors/providerErrors.js.map +1 -1
  66. package/dist/evals/evals.js +304 -78
  67. package/dist/evals/evals.js.map +1 -1
  68. package/dist/evals/index.d.ts +2 -2
  69. package/dist/evals/index.js +1 -1
  70. package/dist/evals/index.js.map +1 -1
  71. package/dist/evidence/index.d.ts +2 -2
  72. package/dist/evidence/index.js +1 -1
  73. package/dist/evidence/index.js.map +1 -1
  74. package/dist/evidence/report.d.ts +1 -1
  75. package/dist/evidence/report.js +263 -79
  76. package/dist/evidence/report.js.map +1 -1
  77. package/dist/fault/faultLab.js +24 -9
  78. package/dist/fault/faultLab.js.map +1 -1
  79. package/dist/index.d.ts +42 -42
  80. package/dist/index.js +21 -21
  81. package/dist/index.js.map +1 -1
  82. package/dist/inventory/analysis.d.ts +1 -1
  83. package/dist/inventory/analysis.js +174 -50
  84. package/dist/inventory/analysis.js.map +1 -1
  85. package/dist/inventory/index.d.ts +4 -4
  86. package/dist/inventory/index.js +3 -3
  87. package/dist/inventory/index.js.map +1 -1
  88. package/dist/inventory/inventoryController.js +362 -115
  89. package/dist/inventory/inventoryController.js.map +1 -1
  90. package/dist/inventory/types.js +680 -140
  91. package/dist/inventory/types.js.map +1 -1
  92. package/dist/ledger/actionLedger.js +355 -78
  93. package/dist/ledger/actionLedger.js.map +1 -1
  94. package/dist/ledger/index.d.ts +2 -2
  95. package/dist/ledger/index.js +1 -1
  96. package/dist/ledger/index.js.map +1 -1
  97. package/dist/mcp/server.js +26 -11
  98. package/dist/mcp/server.js.map +1 -1
  99. package/dist/policy/index.d.ts +2 -2
  100. package/dist/policy/index.js +1 -1
  101. package/dist/policy/index.js.map +1 -1
  102. package/dist/policy/policy.js +112 -32
  103. package/dist/policy/policy.js.map +1 -1
  104. package/dist/productTelemetry/index.js +71 -20
  105. package/dist/productTelemetry/index.js.map +1 -1
  106. package/dist/providers/conformance.js +10 -5
  107. package/dist/providers/conformance.js.map +1 -1
  108. package/dist/providers/discord.js +2 -2
  109. package/dist/providers/generic.js +3 -3
  110. package/dist/providers/github.js +2 -2
  111. package/dist/providers/index.d.ts +3 -3
  112. package/dist/providers/index.js +2 -2
  113. package/dist/providers/index.js.map +1 -1
  114. package/dist/providers/openai.js +3 -3
  115. package/dist/providers/packs/resendPack.js +51 -25
  116. package/dist/providers/packs/resendPack.js.map +1 -1
  117. package/dist/providers/packs/stripePack.js +590 -121
  118. package/dist/providers/packs/stripePack.js.map +1 -1
  119. package/dist/providers/registry.js +4 -4
  120. package/dist/providers/registry.js.map +1 -1
  121. package/dist/providers/runtime.js +7 -6
  122. package/dist/providers/runtime.js.map +1 -1
  123. package/dist/providers/stripeWebhook.js +39 -10
  124. package/dist/providers/stripeWebhook.js.map +1 -1
  125. package/dist/providers/twilio.js +2 -2
  126. package/dist/proxy/cacheKey.js +9 -2
  127. package/dist/proxy/cacheKey.js.map +1 -1
  128. package/dist/proxy/providerDetector.js +22 -9
  129. package/dist/proxy/providerDetector.js.map +1 -1
  130. package/dist/proxy/proxyHandler.js +57 -22
  131. package/dist/proxy/proxyHandler.js.map +1 -1
  132. package/dist/proxy/requestNormalizer.js +4 -2
  133. package/dist/proxy/requestNormalizer.js.map +1 -1
  134. package/dist/reliability/costGovernance.js +211 -49
  135. package/dist/reliability/costGovernance.js.map +1 -1
  136. package/dist/reliability/index.d.ts +8 -8
  137. package/dist/reliability/index.js +4 -4
  138. package/dist/reliability/index.js.map +1 -1
  139. package/dist/reliability/reconciliation.js +274 -65
  140. package/dist/reliability/reconciliation.js.map +1 -1
  141. package/dist/reliability/runtimeHealth.js +127 -35
  142. package/dist/reliability/runtimeHealth.js.map +1 -1
  143. package/dist/reliability/slo.js +257 -50
  144. package/dist/reliability/slo.js.map +1 -1
  145. package/dist/report/safetyReport.d.ts +6 -0
  146. package/dist/report/safetyReport.js +56 -12
  147. package/dist/report/safetyReport.js.map +1 -1
  148. package/dist/rules/aiRules.js +5 -5
  149. package/dist/rules/aiRules.js.map +1 -1
  150. package/dist/safety/index.d.ts +2 -2
  151. package/dist/safety/index.js +1 -1
  152. package/dist/safety/index.js.map +1 -1
  153. package/dist/safety/safetyController.js +675 -165
  154. package/dist/safety/safetyController.js.map +1 -1
  155. package/dist/scenarios/scenarioBundle.js +204 -71
  156. package/dist/scenarios/scenarioBundle.js.map +1 -1
  157. package/dist/scenarios/scenarioStore.js +88 -29
  158. package/dist/scenarios/scenarioStore.js.map +1 -1
  159. package/dist/security/headerSanitizer.js +52 -10
  160. package/dist/security/headerSanitizer.js.map +1 -1
  161. package/dist/security/secrets.js +10 -5
  162. package/dist/security/secrets.js.map +1 -1
  163. package/dist/server/accessControl.js +37 -10
  164. package/dist/server/accessControl.js.map +1 -1
  165. package/dist/server/createServer.js +1 -1
  166. package/dist/server/createServer.js.map +1 -1
  167. package/dist/server/eventsStore.js +15 -5
  168. package/dist/server/eventsStore.js.map +1 -1
  169. package/dist/server/routes.js +61 -19
  170. package/dist/server/routes.js.map +1 -1
  171. package/dist/server/sse.js +1 -1
  172. package/dist/server/sse.js.map +1 -1
  173. package/dist/setup/sdkDetector.js +7 -2
  174. package/dist/setup/sdkDetector.js.map +1 -1
  175. package/dist/setup/setupGenerator.js +136 -48
  176. package/dist/setup/setupGenerator.js.map +1 -1
  177. package/dist/state/stateExtractor.js +3 -1
  178. package/dist/state/stateExtractor.js.map +1 -1
  179. package/dist/state/stateResolver.js +10 -7
  180. package/dist/state/stateResolver.js.map +1 -1
  181. package/dist/state/stateStore.js +13 -4
  182. package/dist/state/stateStore.js.map +1 -1
  183. package/dist/storage/fileStore.js +12 -4
  184. package/dist/storage/fileStore.js.map +1 -1
  185. package/dist/teamControl/controlPlane.js +846 -174
  186. package/dist/teamControl/controlPlane.js.map +1 -1
  187. package/dist/teamControl/deployment.js +19 -5
  188. package/dist/teamControl/deployment.js.map +1 -1
  189. package/dist/teamControl/index.d.ts +4 -4
  190. package/dist/teamControl/index.js +2 -2
  191. package/dist/teamControl/index.js.map +1 -1
  192. package/dist/tests/testGenerator.js +15 -7
  193. package/dist/tests/testGenerator.js.map +1 -1
  194. package/dist/trust/index.d.ts +2 -2
  195. package/dist/trust/index.js +1 -1
  196. package/dist/trust/index.js.map +1 -1
  197. package/dist/trust/trustLadder.js +309 -63
  198. package/dist/trust/trustLadder.js.map +1 -1
  199. package/dist/worlds/index.d.ts +2 -2
  200. package/dist/worlds/index.js +1 -1
  201. package/dist/worlds/index.js.map +1 -1
  202. package/dist/worlds/worlds.js +364 -65
  203. package/dist/worlds/worlds.js.map +1 -1
  204. package/docs/assets/dashboard.png +0 -0
  205. package/docs/ci.md +49 -49
  206. package/docs/github-actions.md +84 -84
  207. package/docs/mcp.md +58 -52
  208. package/docs/policy.md +63 -61
  209. package/docs/providers/authoring-packs.md +180 -175
  210. package/docs/providers/stripe-core-pack.md +84 -82
  211. package/docs/releases/README.md +13 -0
  212. package/docs/security/action-gateway-threat-model.md +34 -34
  213. package/docs/security/action-ledger-incident-replay-threat-model.md +42 -42
  214. package/docs/security/approval-inbox-threat-model.md +35 -35
  215. package/docs/security/credential-broker-threat-model.md +52 -52
  216. package/docs/security/egress-threat-model.md +126 -126
  217. package/docs/security/inventory-threat-model.md +104 -104
  218. package/docs/security/kill-switch-budgets-threat-model.md +37 -37
  219. package/docs/security/reliability-threat-model.md +111 -111
  220. package/docs/security/trust-ladder-threat-model.md +33 -33
  221. package/docs/telemetry.md +14 -0
  222. package/docs/usage.md +680 -571
  223. package/examples/README.md +10 -10
  224. package/examples/agent-instructions/README.md +39 -39
  225. package/examples/ci-smoke/README.md +21 -21
  226. package/examples/ci-smoke/ghostapi.policy.yaml +22 -22
  227. package/examples/ci-smoke/package.json +9 -9
  228. package/examples/ci-smoke/production-egress.mjs +4 -2
  229. package/examples/ci-smoke/safe.mjs +21 -17
  230. package/examples/evals/README.md +24 -24
  231. package/examples/evals/retry-after.eval.json +71 -52
  232. package/examples/generic-rest/README.md +27 -27
  233. package/examples/github-fetch/README.md +38 -35
  234. package/examples/openai-streaming/README.md +16 -16
  235. package/examples/openai-streaming/streaming-tool-call.mjs +44 -31
  236. package/examples/policy/ghostapi.policy.yaml +27 -27
  237. package/examples/record-replay/README.md +19 -19
  238. package/examples/record-replay/replay-requests.json +22 -22
  239. package/examples/record-replay/stripe-checkout.har.json +30 -30
  240. package/examples/resend-node/README.md +22 -22
  241. package/examples/stripe-node/README.md +35 -35
  242. package/examples/stripe-node/checkout-flow.mjs +87 -61
  243. package/examples/twilio-node/README.md +21 -21
  244. package/examples/worlds/README.md +19 -19
  245. package/examples/worlds/subscription-recovery.mjs +21 -12
  246. package/package.json +28 -6
  247. package/dist/landing/assets/index-BXR41fAD.css +0 -1
  248. package/dist/landing/assets/index-CxWsopzP.css +0 -1
  249. package/dist/landing/assets/index-D8_ggMRE.js +0 -55
  250. package/dist/landing/assets/index-DZlpyhqd.js +0 -55
  251. package/dist/landing/assets/index-Dk-Xezs3.js +0 -55
  252. package/dist/landing/index.html +0 -15
  253. package/dist/landing/landing.d.ts +0 -3
  254. package/dist/landing/landing.js +0 -12
  255. package/dist/landing/landing.js.map +0 -1
  256. package/docs/commercial/README.md +0 -29
  257. package/docs/commercial/data-inventory.md +0 -36
  258. package/docs/commercial/manual-invoicing.md +0 -44
  259. package/docs/commercial/metrics.md +0 -51
  260. package/docs/commercial/pricing.md +0 -103
  261. package/docs/commercial/privacy-policy-draft.md +0 -41
  262. package/docs/commercial/terms-topics-for-counsel.md +0 -51
  263. package/docs/design-partners/README.md +0 -52
  264. package/docs/design-partners/demo-narratives.md +0 -33
  265. package/docs/design-partners/design-partner-offer.md +0 -39
  266. package/docs/design-partners/discovery-questions.md +0 -28
  267. package/docs/design-partners/feedback-capture-template.md +0 -48
  268. package/docs/design-partners/icp-one-pager.md +0 -43
  269. package/docs/design-partners/onboarding-checklist.md +0 -33
  270. package/docs/design-partners/pilot-success-criteria.md +0 -27
  271. package/docs/design-partners/pricing-interview-script.md +0 -18
  272. package/docs/design-partners/telemetry-plan.md +0 -48
  273. package/docs/development/baseline.md +0 -114
  274. package/docs/development/onboarding-smoke.md +0 -37
  275. package/docs/development/verification-0.1.8.md +0 -25
  276. package/docs/enterprise-product-roadmap-ru.md +0 -1879
  277. package/docs/fundraising/README.md +0 -87
  278. package/docs/fundraising/data-room-checklist.md +0 -72
  279. package/docs/fundraising/demo-script.md +0 -52
  280. package/docs/fundraising/design-partners-50.md +0 -74
  281. package/docs/fundraising/launch-posts.md +0 -37
  282. package/docs/fundraising/metrics-and-evidence.md +0 -30
  283. package/docs/fundraising/roadmap-12-month.md +0 -25
  284. package/docs/fundraising/technical-due-diligence-index.md +0 -37
  285. package/docs/fundraising/yc-application.md +0 -25
  286. package/docs/hosted-pilot.md +0 -111
  287. package/docs/operations/disaster-recovery-runbook.md +0 -81
  288. package/docs/operations/kill-switch-runbook.md +0 -32
  289. package/docs/release-checklist.md +0 -83
  290. package/docs/release-migration-and-rollback.md +0 -63
  291. package/docs/release-readiness.md +0 -39
  292. package/docs/team-control-plane.md +0 -72
@@ -3,7 +3,7 @@ import { lstat, readFile } from "node:fs/promises";
3
3
  import { dirname } from "node:path";
4
4
  import { getDataPaths } from "../config/dataPaths.js";
5
5
  import { sanitizeSecretString } from "../security/secrets.js";
6
- import { atomicWriteJson, ensurePrivateDirectory, withFileLock } from "../storage/fileStore.js";
6
+ import { atomicWriteJson, ensurePrivateDirectory, withFileLock, } from "../storage/fileStore.js";
7
7
  const SCHEMA_VERSION = 1;
8
8
  const MAX_STORE_BYTES = 1024 * 1024;
9
9
  const MAX_CREDENTIALS = 1_000;
@@ -39,12 +39,19 @@ export class CredentialBroker {
39
39
  this.executor = options.executor;
40
40
  this.workloadVerifier = options.workloadVerifier;
41
41
  this.actionReceiptVerifier = options.actionReceiptVerifier;
42
- this.breakGlassAuthorizer = options.breakGlassAuthorizer ?? createDisabledBreakGlassAuthorizer();
42
+ this.breakGlassAuthorizer =
43
+ options.breakGlassAuthorizer ?? createDisabledBreakGlassAuthorizer();
43
44
  }
44
45
  async registerCredential(input) {
45
- const candidate = validateCredentialMetadata({ ...input, version: 1, createdAt: this.timestamp(), expiresAt: input.expiresAt });
46
+ const candidate = validateCredentialMetadata({
47
+ ...input,
48
+ version: 1,
49
+ createdAt: this.timestamp(),
50
+ expiresAt: input.expiresAt,
51
+ });
46
52
  return this.mutate((state) => {
47
- if (state.credentials.some((credential) => credential.tenantId === candidate.tenantId && credential.id === candidate.id))
53
+ if (state.credentials.some((credential) => credential.tenantId === candidate.tenantId &&
54
+ credential.id === candidate.id))
48
55
  throw new CredentialBrokerError("Credential already exists.");
49
56
  state.credentials.push(candidate);
50
57
  return clone(candidate);
@@ -64,7 +71,9 @@ export class CredentialBroker {
64
71
  credential.version += 1;
65
72
  credential.rotatedAt = now;
66
73
  for (const grant of state.grants)
67
- if (grant.tenantId === tenantId && grant.credentialId === credentialId && grant.revokedAt === undefined)
74
+ if (grant.tenantId === tenantId &&
75
+ grant.credentialId === credentialId &&
76
+ grant.revokedAt === undefined)
68
77
  grant.revokedAt = now;
69
78
  return clone(credential);
70
79
  });
@@ -77,7 +86,9 @@ export class CredentialBroker {
77
86
  if (credential.revokedAt === undefined)
78
87
  credential.revokedAt = now;
79
88
  for (const grant of state.grants)
80
- if (grant.tenantId === tenantId && grant.credentialId === credentialId && grant.revokedAt === undefined)
89
+ if (grant.tenantId === tenantId &&
90
+ grant.credentialId === credentialId &&
91
+ grant.revokedAt === undefined)
81
92
  grant.revokedAt = now;
82
93
  });
83
94
  }
@@ -89,13 +100,18 @@ export class CredentialBroker {
89
100
  const credential = findCredential(state, workload.tenantId, request.credentialId);
90
101
  assertCredentialUsable(credential, now);
91
102
  assertRequestMatchesCredential(request, credential, workload);
92
- const normalOwnerMatch = credential.ownerWorkloadId === workload.workloadId && credential.ownerWorkloadKind === workload.workloadKind;
93
- const breakGlass = normalOwnerMatch ? undefined : await this.breakGlassAuthorizer.authorize(input.breakGlassApproval, workload, request);
94
- if (breakGlass !== undefined && Date.parse(expiresAt) - Date.parse(now) > MAX_BREAK_GLASS_TTL_MS)
103
+ const normalOwnerMatch = credential.ownerWorkloadId === workload.workloadId &&
104
+ credential.ownerWorkloadKind === workload.workloadKind;
105
+ const breakGlass = normalOwnerMatch
106
+ ? undefined
107
+ : await this.breakGlassAuthorizer.authorize(input.breakGlassApproval, workload, request);
108
+ if (breakGlass !== undefined &&
109
+ Date.parse(expiresAt) - Date.parse(now) > MAX_BREAK_GLASS_TTL_MS)
95
110
  throw new CredentialBrokerError("Break-glass grants must expire within five minutes.");
96
111
  if (Date.parse(expiresAt) - Date.parse(now) > MAX_GRANT_TTL_MS)
97
112
  throw new CredentialBrokerError("Credential grants must expire within fifteen minutes.");
98
- if (Date.parse(expiresAt) > Date.parse(credential.expiresAt) || Date.parse(expiresAt) > Date.parse(workload.expiresAt))
113
+ if (Date.parse(expiresAt) > Date.parse(credential.expiresAt) ||
114
+ Date.parse(expiresAt) > Date.parse(workload.expiresAt))
99
115
  throw new CredentialBrokerError("Credential grant expiry exceeds its credential or workload lifetime.");
100
116
  const grant = {
101
117
  id: identifier(`grant-${randomUUID().replace(/-/g, "")}`, "Credential grant id"),
@@ -114,7 +130,7 @@ export class CredentialBroker {
114
130
  action: clone(request.action),
115
131
  issuedAt: now,
116
132
  expiresAt,
117
- ...(breakGlass === undefined ? {} : { breakGlass })
133
+ ...(breakGlass === undefined ? {} : { breakGlass }),
118
134
  };
119
135
  state.grants.push(grant);
120
136
  return clone(grant);
@@ -133,7 +149,8 @@ export class CredentialBroker {
133
149
  assertCredentialUsable(credential, now);
134
150
  if (credential.version !== grant.credentialVersion)
135
151
  throw new CredentialBrokerError("Credential grant was invalidated by rotation.");
136
- if (credential.provider !== this.executor.provider || !grant.scopes.every((scope) => this.executor.supportsScope(scope)))
152
+ if (credential.provider !== this.executor.provider ||
153
+ !grant.scopes.every((scope) => this.executor.supportsScope(scope)))
137
154
  throw new CredentialBrokerError("Credential executor denies the requested scope.");
138
155
  const prior = state.receipts.find((receipt) => receipt.grantId === grant.id);
139
156
  if (prior !== undefined) {
@@ -142,16 +159,32 @@ export class CredentialBroker {
142
159
  throw new CredentialBrokerError("Prior credential execution has no retry-safe outcome and will not retry automatically.");
143
160
  }
144
161
  await this.actionReceiptVerifier.verify(request.action, workload);
145
- const receipt = { id: identifier(`credential-use-${randomUUID().replace(/-/g, "")}`, "Credential use receipt id"), grantId: grant.id, credentialId: credential.id, credentialVersion: credential.version, tenantId: credential.tenantId, workloadId: workload.workloadId, action: clone(grant.action), status: "executing", executedAt: now };
162
+ const receipt = {
163
+ id: identifier(`credential-use-${randomUUID().replace(/-/g, "")}`, "Credential use receipt id"),
164
+ grantId: grant.id,
165
+ credentialId: credential.id,
166
+ credentialVersion: credential.version,
167
+ tenantId: credential.tenantId,
168
+ workloadId: workload.workloadId,
169
+ action: clone(grant.action),
170
+ status: "executing",
171
+ executedAt: now,
172
+ };
146
173
  state.receipts.push(receipt);
147
- return { grant: clone(grant), credential: clone(credential), receipt: clone(receipt) };
174
+ return {
175
+ grant: clone(grant),
176
+ credential: clone(credential),
177
+ receipt: clone(receipt),
178
+ };
148
179
  });
149
180
  if ("status" in started)
150
181
  return started;
151
182
  let secret;
152
183
  try {
153
184
  secret = await this.vault.readSecret(started.credential.vaultRef);
154
- if (!(secret instanceof Uint8Array) || secret.byteLength === 0 || secret.byteLength > 64 * 1024)
185
+ if (!(secret instanceof Uint8Array) ||
186
+ secret.byteLength === 0 ||
187
+ secret.byteLength > 64 * 1024)
155
188
  throw new CredentialBrokerError("Credential vault returned invalid secret material.");
156
189
  await this.assertExecutionActive(started.grant, input.identity);
157
190
  }
@@ -160,7 +193,12 @@ export class CredentialBroker {
160
193
  throw new CredentialBrokerError("Credential execution did not start.");
161
194
  }
162
195
  try {
163
- const result = await this.executor.execute({ secret, grant: clone(started.grant), workload: clone(workload), assertActive: async () => this.assertExecutionActive(started.grant, input.identity) });
196
+ const result = await this.executor.execute({
197
+ secret,
198
+ grant: clone(started.grant),
199
+ workload: clone(workload),
200
+ assertActive: async () => this.assertExecutionActive(started.grant, input.identity),
201
+ });
164
202
  return await this.completeExecution(started.receipt.id, "executed", result.providerRequestId);
165
203
  }
166
204
  catch {
@@ -179,12 +217,21 @@ export class CredentialBroker {
179
217
  const current = state.grants.find((candidate) => candidate.id === grant.id);
180
218
  if (current === undefined)
181
219
  throw new CredentialBrokerError("Credential grant was not found.");
182
- assertGrantUsable(current, verifiedWorkload, { credentialId: grant.credentialId, provider: grant.provider, scopes: grant.scopes, audience: grant.audience, action: grant.action }, this.timestamp());
220
+ assertGrantUsable(current, verifiedWorkload, {
221
+ credentialId: grant.credentialId,
222
+ provider: grant.provider,
223
+ scopes: grant.scopes,
224
+ audience: grant.audience,
225
+ action: grant.action,
226
+ }, this.timestamp());
183
227
  const credential = findCredential(state, verifiedWorkload.tenantId, current.credentialId);
184
228
  assertCredentialUsable(credential, this.timestamp());
185
- if (credential.version !== current.credentialVersion || credential.provider !== this.executor.provider || !current.scopes.every((scope) => this.executor.supportsScope(scope)))
229
+ if (credential.version !== current.credentialVersion ||
230
+ credential.provider !== this.executor.provider ||
231
+ !current.scopes.every((scope) => this.executor.supportsScope(scope)))
186
232
  throw new CredentialBrokerError("Credential grant is no longer executable.");
187
- if (state.receipts.find((receipt) => receipt.grantId === current.id)?.status !== "executing")
233
+ if (state.receipts.find((receipt) => receipt.grantId === current.id)
234
+ ?.status !== "executing")
188
235
  throw new CredentialBrokerError("Credential execution is no longer active.");
189
236
  });
190
237
  }
@@ -200,7 +247,8 @@ export class CredentialBroker {
200
247
  findCredential(state, receipt.tenantId, receipt.credentialId).lastUsedAt = now;
201
248
  }
202
249
  else {
203
- receipt.failureCode = status === "failed" ? "execution_not_started" : "unknown_outcome";
250
+ receipt.failureCode =
251
+ status === "failed" ? "execution_not_started" : "unknown_outcome";
204
252
  }
205
253
  return clone(receipt);
206
254
  });
@@ -209,7 +257,15 @@ export class CredentialBroker {
209
257
  const state = await this.read();
210
258
  const candidates = state.credentials.filter((credential) => credential.revokedAt === undefined);
211
259
  const results = await Promise.all(candidates.map(async (credential) => {
212
- const active = this.workloadVerifier.isActive === undefined ? false : await this.workloadVerifier.isActive({ tenantId: credential.tenantId, projectId: credential.projectId, environment: credential.environment, workloadId: credential.ownerWorkloadId, workloadKind: credential.ownerWorkloadKind });
260
+ const active = this.workloadVerifier.isActive === undefined
261
+ ? false
262
+ : await this.workloadVerifier.isActive({
263
+ tenantId: credential.tenantId,
264
+ projectId: credential.projectId,
265
+ environment: credential.environment,
266
+ workloadId: credential.ownerWorkloadId,
267
+ workloadKind: credential.ownerWorkloadKind,
268
+ });
213
269
  return active ? undefined : clone(credential);
214
270
  }));
215
271
  return results.filter((credential) => credential !== undefined);
@@ -260,7 +316,11 @@ export class CredentialBroker {
260
316
  }
261
317
  }
262
318
  export function createDisabledBreakGlassAuthorizer() {
263
- return { async authorize() { throw new CredentialBrokerError("Break-glass authorization is not configured."); } };
319
+ return {
320
+ async authorize() {
321
+ throw new CredentialBrokerError("Break-glass authorization is not configured.");
322
+ },
323
+ };
264
324
  }
265
325
  export function createTestActionReceiptVerifier(reference) {
266
326
  let calls = 0;
@@ -270,9 +330,11 @@ export function createTestActionReceiptVerifier(reference) {
270
330
  calls += 1;
271
331
  if (!sameAction(reference, candidate))
272
332
  throw new CredentialBrokerError("Action receipt does not authorize this credential use.");
273
- }
333
+ },
334
+ },
335
+ get calls() {
336
+ return calls;
274
337
  },
275
- get calls() { return calls; }
276
338
  };
277
339
  }
278
340
  export function createTestCredentialVault() {
@@ -285,13 +347,15 @@ export function createTestCredentialVault() {
285
347
  if (value === undefined)
286
348
  throw new CredentialBrokerError("Credential vault reference was not found.");
287
349
  return new Uint8Array(value);
288
- }
350
+ },
289
351
  },
290
352
  put(vaultRef, material) {
291
- if (!(material instanceof Uint8Array) || material.byteLength === 0 || material.byteLength > 64 * 1024)
353
+ if (!(material instanceof Uint8Array) ||
354
+ material.byteLength === 0 ||
355
+ material.byteLength > 64 * 1024)
292
356
  throw new CredentialBrokerError("Test credential material is invalid.");
293
357
  values.set(reference(vaultRef, "Credential vault reference"), new Uint8Array(material));
294
- }
358
+ },
295
359
  };
296
360
  }
297
361
  export function createTestCredentialExecutor() {
@@ -301,14 +365,19 @@ export function createTestCredentialExecutor() {
301
365
  provider: "test-provider",
302
366
  supportsScope: (scope) => scope === "test.execute",
303
367
  async execute(input) {
304
- if (input.grant.audience !== "ghostapi-server" || !input.grant.scopes.every((scope) => scope === "test.execute") || input.secret.byteLength === 0)
368
+ if (input.grant.audience !== "ghostapi-server" ||
369
+ !input.grant.scopes.every((scope) => scope === "test.execute") ||
370
+ input.secret.byteLength === 0)
305
371
  throw new CredentialBrokerError("Test provider rejected credential execution.");
306
372
  await input.assertActive();
307
- executions.push({ grantId: input.grant.id, secretLength: input.secret.byteLength });
373
+ executions.push({
374
+ grantId: input.grant.id,
375
+ secretLength: input.secret.byteLength,
376
+ });
308
377
  return { providerRequestId: `test-provider-${input.grant.id}` };
309
- }
378
+ },
310
379
  },
311
- executions
380
+ executions,
312
381
  };
313
382
  }
314
383
  export function createTestWorkloadIdentityProvider(options = {}) {
@@ -318,7 +387,9 @@ export function createTestWorkloadIdentityProvider(options = {}) {
318
387
  return {
319
388
  verifier: {
320
389
  async authenticate(identity) {
321
- if (identity === null || typeof identity !== "object" || !issued.has(identity))
390
+ if (identity === null ||
391
+ typeof identity !== "object" ||
392
+ !issued.has(identity))
322
393
  throw new CredentialBrokerError("Workload identity is not authenticated.");
323
394
  const workload = validateWorkloadIdentity(identity, timestampFrom(now));
324
395
  if (!active.has(workloadKey(workload)))
@@ -327,7 +398,7 @@ export function createTestWorkloadIdentityProvider(options = {}) {
327
398
  },
328
399
  async isActive(binding) {
329
400
  return active.has(workloadKey(binding));
330
- }
401
+ },
331
402
  },
332
403
  issue(input) {
333
404
  const workload = validateWorkloadIdentity({ schemaVersion: 1, kind: "ghostapi.workload-identity", ...input }, timestampFrom(now));
@@ -341,7 +412,7 @@ export function createTestWorkloadIdentityProvider(options = {}) {
341
412
  for (const key of [...active])
342
413
  if (key.endsWith(`:${id}`))
343
414
  active.delete(key);
344
- }
415
+ },
345
416
  };
346
417
  }
347
418
  export function createTestBreakGlassAuthorizer(options = {}) {
@@ -350,20 +421,28 @@ export function createTestBreakGlassAuthorizer(options = {}) {
350
421
  return {
351
422
  authorizer: {
352
423
  async authorize(approval, workload, request) {
353
- if (approval === null || typeof approval !== "object" || !issued.has(approval))
424
+ if (approval === null ||
425
+ typeof approval !== "object" ||
426
+ !issued.has(approval))
354
427
  throw new CredentialBrokerError("Break-glass approval is not trusted.");
355
428
  const parsed = validateBreakGlassApproval(approval, timestampFrom(now));
356
- if (parsed.approvedBy === workload.subjectId || parsed.approvedBy === workload.workloadId || !sameAction(parsed.action, request.action))
429
+ if (parsed.approvedBy === workload.subjectId ||
430
+ parsed.approvedBy === workload.workloadId ||
431
+ !sameAction(parsed.action, request.action))
357
432
  throw new CredentialBrokerError("Break-glass approval does not authorize this workload action.");
358
- return { approvalId: parsed.approvalId, approvedBy: parsed.approvedBy, reason: parsed.reason };
359
- }
433
+ return {
434
+ approvalId: parsed.approvalId,
435
+ approvedBy: parsed.approvedBy,
436
+ reason: parsed.reason,
437
+ };
438
+ },
360
439
  },
361
440
  issue(input) {
362
441
  const approval = validateBreakGlassApproval({ schemaVersion: 1, kind: "ghostapi.break-glass-approval", ...input }, timestampFrom(now));
363
442
  const frozen = Object.freeze(clone(approval));
364
443
  issued.add(frozen);
365
444
  return frozen;
366
- }
445
+ },
367
446
  };
368
447
  }
369
448
  function emptyState() {
@@ -379,8 +458,12 @@ function validateState(value) {
379
458
  const grants = array(state.grants, "Credential grants", MAX_GRANTS).map(validateGrant);
380
459
  unique(grants.map((grant) => grant.id), "Credential grant ids");
381
460
  for (const grant of grants) {
382
- const credential = credentials.find((candidate) => candidate.tenantId === grant.tenantId && candidate.id === grant.credentialId);
383
- if (credential === undefined || grant.credentialVersion > credential.version || grant.provider !== credential.provider || !grant.scopes.every((scope) => credential.allowedScopes.includes(scope)))
461
+ const credential = credentials.find((candidate) => candidate.tenantId === grant.tenantId &&
462
+ candidate.id === grant.credentialId);
463
+ if (credential === undefined ||
464
+ grant.credentialVersion > credential.version ||
465
+ grant.provider !== credential.provider ||
466
+ !grant.scopes.every((scope) => credential.allowedScopes.includes(scope)))
384
467
  throw new CredentialBrokerError("Credential grant references an invalid credential.");
385
468
  }
386
469
  const receipts = array(state.receipts, "Credential use receipts", MAX_RECEIPTS).map(validateReceipt);
@@ -388,87 +471,286 @@ function validateState(value) {
388
471
  unique(receipts.map((receipt) => receipt.grantId), "Credential use receipt grants");
389
472
  for (const receipt of receipts) {
390
473
  const grant = grants.find((candidate) => candidate.id === receipt.grantId);
391
- if (grant === undefined || receipt.credentialId !== grant.credentialId || receipt.credentialVersion !== grant.credentialVersion || receipt.tenantId !== grant.tenantId || receipt.workloadId !== grant.workloadId || !sameAction(receipt.action, grant.action))
474
+ if (grant === undefined ||
475
+ receipt.credentialId !== grant.credentialId ||
476
+ receipt.credentialVersion !== grant.credentialVersion ||
477
+ receipt.tenantId !== grant.tenantId ||
478
+ receipt.workloadId !== grant.workloadId ||
479
+ !sameAction(receipt.action, grant.action))
392
480
  throw new CredentialBrokerError("Credential use receipt references an invalid grant.");
393
481
  }
394
482
  return { schemaVersion: 1, credentials, grants, receipts };
395
483
  }
396
484
  function validateCredentialMetadata(value) {
397
485
  const metadata = object(value, "Credential metadata is invalid.");
398
- exactKeys(metadata, ["id", "tenantId", "projectId", "environment", "provider", "ownerWorkloadId", "ownerWorkloadKind", "vaultRef", "allowedScopes", "version", "createdAt", "expiresAt", "rotatedAt", "revokedAt", "lastUsedAt"], "Credential metadata", ["rotatedAt", "revokedAt", "lastUsedAt"]);
486
+ exactKeys(metadata, [
487
+ "id",
488
+ "tenantId",
489
+ "projectId",
490
+ "environment",
491
+ "provider",
492
+ "ownerWorkloadId",
493
+ "ownerWorkloadKind",
494
+ "vaultRef",
495
+ "allowedScopes",
496
+ "version",
497
+ "createdAt",
498
+ "expiresAt",
499
+ "rotatedAt",
500
+ "revokedAt",
501
+ "lastUsedAt",
502
+ ], "Credential metadata", ["rotatedAt", "revokedAt", "lastUsedAt"]);
399
503
  const createdAt = timestamp(metadata.createdAt, "Credential creation time");
400
504
  const expiresAt = timestamp(metadata.expiresAt, "Credential expiry");
401
505
  if (Date.parse(expiresAt) <= Date.parse(createdAt))
402
506
  throw new CredentialBrokerError("Credential expiry must follow creation.");
403
- return { id: identifier(metadata.id, "Credential id"), tenantId: identifier(metadata.tenantId, "Credential tenant id"), projectId: identifier(metadata.projectId, "Credential project id"), environment: identifier(metadata.environment, "Credential environment"), provider: identifier(metadata.provider, "Credential provider"), ownerWorkloadId: identifier(metadata.ownerWorkloadId, "Credential owner workload id"), ownerWorkloadKind: workloadKind(metadata.ownerWorkloadKind), vaultRef: reference(metadata.vaultRef, "Credential vault reference"), allowedScopes: scopes(metadata.allowedScopes, "Credential allowed scopes"), version: positiveInteger(metadata.version, "Credential version"), createdAt, expiresAt, ...(metadata.rotatedAt === undefined ? {} : { rotatedAt: timestamp(metadata.rotatedAt, "Credential rotation time") }), ...(metadata.revokedAt === undefined ? {} : { revokedAt: timestamp(metadata.revokedAt, "Credential revocation time") }), ...(metadata.lastUsedAt === undefined ? {} : { lastUsedAt: timestamp(metadata.lastUsedAt, "Credential last use time") }) };
507
+ return {
508
+ id: identifier(metadata.id, "Credential id"),
509
+ tenantId: identifier(metadata.tenantId, "Credential tenant id"),
510
+ projectId: identifier(metadata.projectId, "Credential project id"),
511
+ environment: identifier(metadata.environment, "Credential environment"),
512
+ provider: identifier(metadata.provider, "Credential provider"),
513
+ ownerWorkloadId: identifier(metadata.ownerWorkloadId, "Credential owner workload id"),
514
+ ownerWorkloadKind: workloadKind(metadata.ownerWorkloadKind),
515
+ vaultRef: reference(metadata.vaultRef, "Credential vault reference"),
516
+ allowedScopes: scopes(metadata.allowedScopes, "Credential allowed scopes"),
517
+ version: positiveInteger(metadata.version, "Credential version"),
518
+ createdAt,
519
+ expiresAt,
520
+ ...(metadata.rotatedAt === undefined
521
+ ? {}
522
+ : {
523
+ rotatedAt: timestamp(metadata.rotatedAt, "Credential rotation time"),
524
+ }),
525
+ ...(metadata.revokedAt === undefined
526
+ ? {}
527
+ : {
528
+ revokedAt: timestamp(metadata.revokedAt, "Credential revocation time"),
529
+ }),
530
+ ...(metadata.lastUsedAt === undefined
531
+ ? {}
532
+ : {
533
+ lastUsedAt: timestamp(metadata.lastUsedAt, "Credential last use time"),
534
+ }),
535
+ };
404
536
  }
405
537
  function validateGrant(value) {
406
538
  const grant = object(value, "Credential grant is invalid.");
407
- exactKeys(grant, ["id", "credentialId", "credentialVersion", "tenantId", "projectId", "environment", "workloadId", "subjectId", "workloadKind", "runId", "provider", "scopes", "audience", "action", "issuedAt", "expiresAt", "breakGlass", "revokedAt"], "Credential grant", ["breakGlass", "revokedAt"]);
539
+ exactKeys(grant, [
540
+ "id",
541
+ "credentialId",
542
+ "credentialVersion",
543
+ "tenantId",
544
+ "projectId",
545
+ "environment",
546
+ "workloadId",
547
+ "subjectId",
548
+ "workloadKind",
549
+ "runId",
550
+ "provider",
551
+ "scopes",
552
+ "audience",
553
+ "action",
554
+ "issuedAt",
555
+ "expiresAt",
556
+ "breakGlass",
557
+ "revokedAt",
558
+ ], "Credential grant", ["breakGlass", "revokedAt"]);
408
559
  const issuedAt = timestamp(grant.issuedAt, "Credential grant issue time");
409
560
  const expiresAt = timestamp(grant.expiresAt, "Credential grant expiry");
410
- if (Date.parse(expiresAt) <= Date.parse(issuedAt) || Date.parse(expiresAt) - Date.parse(issuedAt) > MAX_GRANT_TTL_MS)
561
+ if (Date.parse(expiresAt) <= Date.parse(issuedAt) ||
562
+ Date.parse(expiresAt) - Date.parse(issuedAt) > MAX_GRANT_TTL_MS)
411
563
  throw new CredentialBrokerError("Credential grant lifetime is invalid.");
412
- const breakGlass = grant.breakGlass === undefined ? undefined : breakGlassRecord(grant.breakGlass);
413
- if (breakGlass !== undefined && Date.parse(expiresAt) - Date.parse(issuedAt) > MAX_BREAK_GLASS_TTL_MS)
564
+ const breakGlass = grant.breakGlass === undefined
565
+ ? undefined
566
+ : breakGlassRecord(grant.breakGlass);
567
+ if (breakGlass !== undefined &&
568
+ Date.parse(expiresAt) - Date.parse(issuedAt) > MAX_BREAK_GLASS_TTL_MS)
414
569
  throw new CredentialBrokerError("Break-glass grant lifetime is invalid.");
415
570
  if (grant.audience !== "ghostapi-server")
416
571
  throw new CredentialBrokerError("Credential grant audience is invalid.");
417
- return { id: identifier(grant.id, "Credential grant id"), credentialId: identifier(grant.credentialId, "Credential id"), credentialVersion: positiveInteger(grant.credentialVersion, "Credential version"), tenantId: identifier(grant.tenantId, "Credential tenant id"), projectId: identifier(grant.projectId, "Credential project id"), environment: identifier(grant.environment, "Credential environment"), workloadId: identifier(grant.workloadId, "Credential workload id"), subjectId: identifier(grant.subjectId, "Credential subject id"), workloadKind: workloadKind(grant.workloadKind), runId: identifier(grant.runId, "Credential run id"), provider: identifier(grant.provider, "Credential provider"), scopes: scopes(grant.scopes, "Credential grant scopes"), audience: "ghostapi-server", action: actionReference(grant.action), issuedAt, expiresAt, ...(breakGlass === undefined ? {} : { breakGlass }), ...(grant.revokedAt === undefined ? {} : { revokedAt: timestamp(grant.revokedAt, "Credential grant revocation time") }) };
572
+ return {
573
+ id: identifier(grant.id, "Credential grant id"),
574
+ credentialId: identifier(grant.credentialId, "Credential id"),
575
+ credentialVersion: positiveInteger(grant.credentialVersion, "Credential version"),
576
+ tenantId: identifier(grant.tenantId, "Credential tenant id"),
577
+ projectId: identifier(grant.projectId, "Credential project id"),
578
+ environment: identifier(grant.environment, "Credential environment"),
579
+ workloadId: identifier(grant.workloadId, "Credential workload id"),
580
+ subjectId: identifier(grant.subjectId, "Credential subject id"),
581
+ workloadKind: workloadKind(grant.workloadKind),
582
+ runId: identifier(grant.runId, "Credential run id"),
583
+ provider: identifier(grant.provider, "Credential provider"),
584
+ scopes: scopes(grant.scopes, "Credential grant scopes"),
585
+ audience: "ghostapi-server",
586
+ action: actionReference(grant.action),
587
+ issuedAt,
588
+ expiresAt,
589
+ ...(breakGlass === undefined ? {} : { breakGlass }),
590
+ ...(grant.revokedAt === undefined
591
+ ? {}
592
+ : {
593
+ revokedAt: timestamp(grant.revokedAt, "Credential grant revocation time"),
594
+ }),
595
+ };
418
596
  }
419
597
  function validateReceipt(value) {
420
598
  const receipt = object(value, "Credential use receipt is invalid.");
421
- exactKeys(receipt, ["id", "grantId", "credentialId", "credentialVersion", "tenantId", "workloadId", "action", "status", "providerRequestId", "failureCode", "executedAt"], "Credential use receipt", ["providerRequestId", "failureCode"]);
422
- if (receipt.status !== "executing" && receipt.status !== "executed" && receipt.status !== "failed" && receipt.status !== "unknown")
599
+ exactKeys(receipt, [
600
+ "id",
601
+ "grantId",
602
+ "credentialId",
603
+ "credentialVersion",
604
+ "tenantId",
605
+ "workloadId",
606
+ "action",
607
+ "status",
608
+ "providerRequestId",
609
+ "failureCode",
610
+ "executedAt",
611
+ ], "Credential use receipt", ["providerRequestId", "failureCode"]);
612
+ if (receipt.status !== "executing" &&
613
+ receipt.status !== "executed" &&
614
+ receipt.status !== "failed" &&
615
+ receipt.status !== "unknown")
423
616
  throw new CredentialBrokerError("Credential use receipt status is invalid.");
424
- if (receipt.status === "executing" && (receipt.failureCode !== undefined || receipt.providerRequestId !== undefined))
617
+ if (receipt.status === "executing" &&
618
+ (receipt.failureCode !== undefined ||
619
+ receipt.providerRequestId !== undefined))
425
620
  throw new CredentialBrokerError("Executing credential receipt is invalid.");
426
- if (receipt.status === "executed" && (receipt.failureCode !== undefined || receipt.providerRequestId === undefined))
621
+ if (receipt.status === "executed" &&
622
+ (receipt.failureCode !== undefined ||
623
+ receipt.providerRequestId === undefined))
427
624
  throw new CredentialBrokerError("Executed credential receipt is invalid.");
428
- if (receipt.status === "failed" && (receipt.failureCode !== "execution_not_started" || receipt.providerRequestId !== undefined))
625
+ if (receipt.status === "failed" &&
626
+ (receipt.failureCode !== "execution_not_started" ||
627
+ receipt.providerRequestId !== undefined))
429
628
  throw new CredentialBrokerError("Failed credential receipt is invalid.");
430
- if (receipt.status === "unknown" && (receipt.failureCode !== "unknown_outcome" || receipt.providerRequestId !== undefined))
629
+ if (receipt.status === "unknown" &&
630
+ (receipt.failureCode !== "unknown_outcome" ||
631
+ receipt.providerRequestId !== undefined))
431
632
  throw new CredentialBrokerError("Unknown credential receipt is invalid.");
432
- return { id: identifier(receipt.id, "Credential use receipt id"), grantId: identifier(receipt.grantId, "Credential grant id"), credentialId: identifier(receipt.credentialId, "Credential id"), credentialVersion: positiveInteger(receipt.credentialVersion, "Credential version"), tenantId: identifier(receipt.tenantId, "Credential tenant id"), workloadId: identifier(receipt.workloadId, "Credential workload id"), action: actionReference(receipt.action), status: receipt.status, ...(receipt.providerRequestId === undefined ? {} : { providerRequestId: identifier(receipt.providerRequestId, "Provider request id") }), ...(receipt.failureCode === undefined ? {} : { failureCode: receipt.failureCode }), executedAt: timestamp(receipt.executedAt, "Credential execution time") };
633
+ return {
634
+ id: identifier(receipt.id, "Credential use receipt id"),
635
+ grantId: identifier(receipt.grantId, "Credential grant id"),
636
+ credentialId: identifier(receipt.credentialId, "Credential id"),
637
+ credentialVersion: positiveInteger(receipt.credentialVersion, "Credential version"),
638
+ tenantId: identifier(receipt.tenantId, "Credential tenant id"),
639
+ workloadId: identifier(receipt.workloadId, "Credential workload id"),
640
+ action: actionReference(receipt.action),
641
+ status: receipt.status,
642
+ ...(receipt.providerRequestId === undefined
643
+ ? {}
644
+ : {
645
+ providerRequestId: identifier(receipt.providerRequestId, "Provider request id"),
646
+ }),
647
+ ...(receipt.failureCode === undefined
648
+ ? {}
649
+ : {
650
+ failureCode: receipt.failureCode,
651
+ }),
652
+ executedAt: timestamp(receipt.executedAt, "Credential execution time"),
653
+ };
433
654
  }
434
655
  function validateAccessRequest(value) {
435
656
  const request = object(value, "Credential access request is invalid.");
436
657
  exactKeys(request, ["credentialId", "provider", "scopes", "audience", "action"], "Credential access request");
437
658
  if (request.audience !== "ghostapi-server")
438
659
  throw new CredentialBrokerError("Credential access request audience is invalid.");
439
- return { credentialId: identifier(request.credentialId, "Credential id"), provider: identifier(request.provider, "Credential provider"), scopes: scopes(request.scopes, "Credential request scopes"), audience: "ghostapi-server", action: actionReference(request.action) };
660
+ return {
661
+ credentialId: identifier(request.credentialId, "Credential id"),
662
+ provider: identifier(request.provider, "Credential provider"),
663
+ scopes: scopes(request.scopes, "Credential request scopes"),
664
+ audience: "ghostapi-server",
665
+ action: actionReference(request.action),
666
+ };
440
667
  }
441
668
  function validateWorkloadIdentity(value, now) {
442
669
  const identity = object(value, "Workload identity is invalid.");
443
- exactKeys(identity, ["schemaVersion", "kind", "tenantId", "projectId", "environment", "workloadId", "subjectId", "workloadKind", "runId", "issuedAt", "expiresAt"], "Workload identity");
444
- if (identity.schemaVersion !== SCHEMA_VERSION || identity.kind !== "ghostapi.workload-identity")
670
+ exactKeys(identity, [
671
+ "schemaVersion",
672
+ "kind",
673
+ "tenantId",
674
+ "projectId",
675
+ "environment",
676
+ "workloadId",
677
+ "subjectId",
678
+ "workloadKind",
679
+ "runId",
680
+ "issuedAt",
681
+ "expiresAt",
682
+ ], "Workload identity");
683
+ if (identity.schemaVersion !== SCHEMA_VERSION ||
684
+ identity.kind !== "ghostapi.workload-identity")
445
685
  throw new CredentialBrokerError("Unsupported workload identity schema.");
446
686
  const issuedAt = timestamp(identity.issuedAt, "Workload identity issue time");
447
687
  const expiresAt = timestamp(identity.expiresAt, "Workload identity expiry");
448
- if (Date.parse(expiresAt) <= Date.parse(issuedAt) || Date.parse(expiresAt) <= Date.parse(now))
688
+ if (Date.parse(expiresAt) <= Date.parse(issuedAt) ||
689
+ Date.parse(expiresAt) <= Date.parse(now))
449
690
  throw new CredentialBrokerError("Workload identity is expired.");
450
- return { schemaVersion: 1, kind: "ghostapi.workload-identity", tenantId: identifier(identity.tenantId, "Workload tenant id"), projectId: identifier(identity.projectId, "Workload project id"), environment: identifier(identity.environment, "Workload environment"), workloadId: identifier(identity.workloadId, "Workload id"), subjectId: identifier(identity.subjectId, "Workload subject id"), workloadKind: workloadKind(identity.workloadKind), runId: identifier(identity.runId, "Workload run id"), issuedAt, expiresAt };
691
+ return {
692
+ schemaVersion: 1,
693
+ kind: "ghostapi.workload-identity",
694
+ tenantId: identifier(identity.tenantId, "Workload tenant id"),
695
+ projectId: identifier(identity.projectId, "Workload project id"),
696
+ environment: identifier(identity.environment, "Workload environment"),
697
+ workloadId: identifier(identity.workloadId, "Workload id"),
698
+ subjectId: identifier(identity.subjectId, "Workload subject id"),
699
+ workloadKind: workloadKind(identity.workloadKind),
700
+ runId: identifier(identity.runId, "Workload run id"),
701
+ issuedAt,
702
+ expiresAt,
703
+ };
451
704
  }
452
705
  function validateBreakGlassApproval(value, now) {
453
706
  const approval = object(value, "Break-glass approval is invalid.");
454
- exactKeys(approval, ["schemaVersion", "kind", "approvalId", "action", "approvedBy", "reason", "issuedAt", "expiresAt"], "Break-glass approval");
455
- if (approval.schemaVersion !== SCHEMA_VERSION || approval.kind !== "ghostapi.break-glass-approval")
707
+ exactKeys(approval, [
708
+ "schemaVersion",
709
+ "kind",
710
+ "approvalId",
711
+ "action",
712
+ "approvedBy",
713
+ "reason",
714
+ "issuedAt",
715
+ "expiresAt",
716
+ ], "Break-glass approval");
717
+ if (approval.schemaVersion !== SCHEMA_VERSION ||
718
+ approval.kind !== "ghostapi.break-glass-approval")
456
719
  throw new CredentialBrokerError("Unsupported break-glass approval schema.");
457
720
  const issuedAt = timestamp(approval.issuedAt, "Break-glass issue time");
458
721
  const expiresAt = timestamp(approval.expiresAt, "Break-glass expiry");
459
- if (Date.parse(issuedAt) > Date.parse(now) || Date.parse(expiresAt) <= Date.parse(now) || Date.parse(expiresAt) - Date.parse(issuedAt) > MAX_BREAK_GLASS_TTL_MS)
722
+ if (Date.parse(issuedAt) > Date.parse(now) ||
723
+ Date.parse(expiresAt) <= Date.parse(now) ||
724
+ Date.parse(expiresAt) - Date.parse(issuedAt) > MAX_BREAK_GLASS_TTL_MS)
460
725
  throw new CredentialBrokerError("Break-glass approval is expired or exceeds five minutes.");
461
- return { schemaVersion: 1, kind: "ghostapi.break-glass-approval", approvalId: identifier(approval.approvalId, "Break-glass approval id"), action: actionReference(approval.action), approvedBy: identifier(approval.approvedBy, "Break-glass approver id"), reason: text(approval.reason, "Break-glass reason", 200), issuedAt, expiresAt };
726
+ return {
727
+ schemaVersion: 1,
728
+ kind: "ghostapi.break-glass-approval",
729
+ approvalId: identifier(approval.approvalId, "Break-glass approval id"),
730
+ action: actionReference(approval.action),
731
+ approvedBy: identifier(approval.approvedBy, "Break-glass approver id"),
732
+ reason: text(approval.reason, "Break-glass reason", 200),
733
+ issuedAt,
734
+ expiresAt,
735
+ };
462
736
  }
463
737
  function breakGlassRecord(value) {
464
738
  const record = object(value, "Break-glass record is invalid.");
465
739
  exactKeys(record, ["approvalId", "approvedBy", "reason"], "Break-glass record");
466
- return { approvalId: identifier(record.approvalId, "Break-glass approval id"), approvedBy: identifier(record.approvedBy, "Break-glass approver id"), reason: text(record.reason, "Break-glass reason", 200) };
740
+ return {
741
+ approvalId: identifier(record.approvalId, "Break-glass approval id"),
742
+ approvedBy: identifier(record.approvedBy, "Break-glass approver id"),
743
+ reason: text(record.reason, "Break-glass reason", 200),
744
+ };
467
745
  }
468
746
  function actionReference(value) {
469
747
  const action = object(value, "Credential action reference is invalid.");
470
748
  exactKeys(action, ["actionId", "actionHash", "actionReceiptHash"], "Credential action reference");
471
- return { actionId: identifier(action.actionId, "Credential action id"), actionHash: hash(action.actionHash, "Credential action hash"), actionReceiptHash: hash(action.actionReceiptHash, "Credential action receipt hash") };
749
+ return {
750
+ actionId: identifier(action.actionId, "Credential action id"),
751
+ actionHash: hash(action.actionHash, "Credential action hash"),
752
+ actionReceiptHash: hash(action.actionReceiptHash, "Credential action receipt hash"),
753
+ };
472
754
  }
473
755
  function assertCredentialUsable(credential, now) {
474
756
  if (credential.revokedAt !== undefined)
@@ -477,17 +759,31 @@ function assertCredentialUsable(credential, now) {
477
759
  throw new CredentialBrokerError("Credential is expired.");
478
760
  }
479
761
  function assertRequestMatchesCredential(request, credential, workload) {
480
- if (request.provider !== credential.provider || workload.tenantId !== credential.tenantId || workload.projectId !== credential.projectId || workload.environment !== credential.environment)
762
+ if (request.provider !== credential.provider ||
763
+ workload.tenantId !== credential.tenantId ||
764
+ workload.projectId !== credential.projectId ||
765
+ workload.environment !== credential.environment)
481
766
  throw new CredentialBrokerError("Credential request crosses a tenant, project, environment, or provider boundary.");
482
767
  if (!request.scopes.every((scope) => credential.allowedScopes.includes(scope)))
483
768
  throw new CredentialBrokerError("Credential request exceeds the credential scope.");
484
769
  }
485
770
  function assertGrantUsable(grant, workload, request, now) {
486
- if (grant.revokedAt !== undefined || Date.parse(grant.expiresAt) <= Date.parse(now))
771
+ if (grant.revokedAt !== undefined ||
772
+ Date.parse(grant.expiresAt) <= Date.parse(now))
487
773
  throw new CredentialBrokerError("Credential grant is expired or revoked.");
488
- if (grant.tenantId !== workload.tenantId || grant.projectId !== workload.projectId || grant.environment !== workload.environment || grant.workloadId !== workload.workloadId || grant.subjectId !== workload.subjectId || grant.workloadKind !== workload.workloadKind || grant.runId !== workload.runId)
774
+ if (grant.tenantId !== workload.tenantId ||
775
+ grant.projectId !== workload.projectId ||
776
+ grant.environment !== workload.environment ||
777
+ grant.workloadId !== workload.workloadId ||
778
+ grant.subjectId !== workload.subjectId ||
779
+ grant.workloadKind !== workload.workloadKind ||
780
+ grant.runId !== workload.runId)
489
781
  throw new CredentialBrokerError("Credential grant does not belong to this workload subject and run.");
490
- if (grant.credentialId !== request.credentialId || grant.provider !== request.provider || grant.audience !== request.audience || !sameScopes(grant.scopes, request.scopes) || !sameAction(grant.action, request.action))
782
+ if (grant.credentialId !== request.credentialId ||
783
+ grant.provider !== request.provider ||
784
+ grant.audience !== request.audience ||
785
+ !sameScopes(grant.scopes, request.scopes) ||
786
+ !sameAction(grant.action, request.action))
491
787
  throw new CredentialBrokerError("Credential request differs from the granted action.");
492
788
  }
493
789
  function findCredential(state, tenantId, credentialId) {
@@ -505,26 +801,35 @@ function scopes(value, label) {
505
801
  return parsed.sort();
506
802
  }
507
803
  function workloadKind(value) {
508
- if (value !== "agent_run" && value !== "ci_job" && value !== "production_service")
804
+ if (value !== "agent_run" &&
805
+ value !== "ci_job" &&
806
+ value !== "production_service")
509
807
  throw new CredentialBrokerError("Workload kind is invalid.");
510
808
  return value;
511
809
  }
512
810
  function sameScopes(left, right) {
513
- return left.length === right.length && left.every((scope, index) => scope === right[index]);
811
+ return (left.length === right.length &&
812
+ left.every((scope, index) => scope === right[index]));
514
813
  }
515
814
  function sameAction(left, right) {
516
- return left.actionId === right.actionId && left.actionHash === right.actionHash && left.actionReceiptHash === right.actionReceiptHash;
815
+ return (left.actionId === right.actionId &&
816
+ left.actionHash === right.actionHash &&
817
+ left.actionReceiptHash === right.actionReceiptHash);
517
818
  }
518
819
  function workloadKey(binding) {
519
820
  return `${binding.tenantId}:${binding.projectId}:${binding.environment}:${binding.workloadKind}:${binding.workloadId}`;
520
821
  }
521
822
  function identifier(value, label) {
522
- if (typeof value !== "string" || !IDENTIFIER.test(value) || sanitizeSecretString(value) !== value)
823
+ if (typeof value !== "string" ||
824
+ !IDENTIFIER.test(value) ||
825
+ sanitizeSecretString(value) !== value)
523
826
  throw new CredentialBrokerError(`${label} must be a safe stable identifier.`);
524
827
  return value;
525
828
  }
526
829
  function reference(value, label) {
527
- if (typeof value !== "string" || !REFERENCE.test(value) || sanitizeSecretString(value) !== value)
830
+ if (typeof value !== "string" ||
831
+ !REFERENCE.test(value) ||
832
+ sanitizeSecretString(value) !== value)
528
833
  throw new CredentialBrokerError(`${label} is invalid.`);
529
834
  return value;
530
835
  }
@@ -539,7 +844,9 @@ function positiveInteger(value, label) {
539
844
  return value;
540
845
  }
541
846
  function timestamp(value, label) {
542
- if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) || !Number.isFinite(Date.parse(value)))
847
+ if (typeof value !== "string" ||
848
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) ||
849
+ !Number.isFinite(Date.parse(value)))
543
850
  throw new CredentialBrokerError(`${label} must be an ISO UTC timestamp.`);
544
851
  return value;
545
852
  }
@@ -550,7 +857,11 @@ function futureTimestamp(value, now, label) {
550
857
  return parsed;
551
858
  }
552
859
  function text(value, label, maxLength) {
553
- if (typeof value !== "string" || value.length < 1 || value.length > maxLength || /[\u0000-\u001f]/.test(value) || sanitizeSecretString(value) !== value)
860
+ if (typeof value !== "string" ||
861
+ value.length < 1 ||
862
+ value.length > maxLength ||
863
+ /[\u0000-\u001f]/.test(value) ||
864
+ sanitizeSecretString(value) !== value)
554
865
  throw new CredentialBrokerError(`${label} is invalid.`);
555
866
  return value;
556
867
  }
@@ -566,7 +877,8 @@ function object(value, message) {
566
877
  }
567
878
  function exactKeys(value, keys, label, optional = []) {
568
879
  for (const key of Object.keys(value))
569
- if (!keys.includes(key) || (value[key] === undefined && !optional.includes(key)))
880
+ if (!keys.includes(key) ||
881
+ (value[key] === undefined && !optional.includes(key)))
570
882
  throw new CredentialBrokerError(`${label} contains unsupported field: ${key}`);
571
883
  }
572
884
  function unique(values, label) {