@yiaany/ghostapi 0.1.7 → 0.1.9

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 (322) hide show
  1. package/CHANGELOG.md +42 -2
  2. package/CONTRIBUTING.md +119 -117
  3. package/LICENSE +21 -21
  4. package/README.md +208 -181
  5. package/SECURITY.md +53 -37
  6. package/dist/actions/gateway.d.ts +179 -0
  7. package/dist/actions/gateway.js +514 -0
  8. package/dist/actions/gateway.js.map +1 -0
  9. package/dist/actions/index.d.ts +2 -0
  10. package/dist/actions/index.js +2 -0
  11. package/dist/actions/index.js.map +1 -0
  12. package/dist/ai/aiClient.js +4 -4
  13. package/dist/ai/aiClient.js.map +1 -1
  14. package/dist/ai/aiGenerator.d.ts +2 -2
  15. package/dist/ai/aiGenerator.js +21 -72
  16. package/dist/ai/aiGenerator.js.map +1 -1
  17. package/dist/ai/prompts.js +51 -51
  18. package/dist/ai/prompts.js.map +1 -1
  19. package/dist/approvals/approvalInbox.d.ts +150 -0
  20. package/dist/approvals/approvalInbox.js +412 -0
  21. package/dist/approvals/approvalInbox.js.map +1 -0
  22. package/dist/approvals/index.d.ts +2 -0
  23. package/dist/approvals/index.js +2 -0
  24. package/dist/approvals/index.js.map +1 -0
  25. package/dist/behavior/behaviorStore.d.ts +2 -0
  26. package/dist/behavior/behaviorStore.js +50 -46
  27. package/dist/behavior/behaviorStore.js.map +1 -1
  28. package/dist/cache/index.d.ts +2 -0
  29. package/dist/cache/index.js +58 -14
  30. package/dist/cache/index.js.map +1 -1
  31. package/dist/cli/index.js +438 -58
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/cli/openUrl.d.ts +10 -0
  34. package/dist/cli/openUrl.js +31 -0
  35. package/dist/cli/openUrl.js.map +1 -0
  36. package/dist/cli/parser.d.ts +155 -0
  37. package/dist/cli/parser.js +504 -2
  38. package/dist/cli/parser.js.map +1 -1
  39. package/dist/config/dataPaths.d.ts +31 -0
  40. package/dist/config/dataPaths.js +45 -0
  41. package/dist/config/dataPaths.js.map +1 -0
  42. package/dist/config/localConfig.d.ts +1 -2
  43. package/dist/config/localConfig.js +27 -21
  44. package/dist/config/localConfig.js.map +1 -1
  45. package/dist/config/serverConfig.d.ts +2 -0
  46. package/dist/config/serverConfig.js +8 -1
  47. package/dist/config/serverConfig.js.map +1 -1
  48. package/dist/contracts/contract.d.ts +76 -0
  49. package/dist/contracts/contract.js +603 -0
  50. package/dist/contracts/contract.js.map +1 -0
  51. package/dist/contracts/index.d.ts +2 -0
  52. package/dist/contracts/index.js +2 -0
  53. package/dist/contracts/index.js.map +1 -0
  54. package/dist/credentials/credentialBroker.d.ts +207 -0
  55. package/dist/credentials/credentialBroker.js +588 -0
  56. package/dist/credentials/credentialBroker.js.map +1 -0
  57. package/dist/credentials/index.d.ts +2 -0
  58. package/dist/credentials/index.js +2 -0
  59. package/dist/credentials/index.js.map +1 -0
  60. package/dist/dashboard/app.js +685 -615
  61. package/dist/dashboard/index.html +13 -7
  62. package/dist/dashboard/styles.css +349 -349
  63. package/dist/egress/capabilities.d.ts +32 -0
  64. package/dist/egress/capabilities.js +165 -0
  65. package/dist/egress/capabilities.js.map +1 -0
  66. package/dist/egress/linuxBootstrap.d.ts +1 -0
  67. package/dist/egress/linuxBootstrap.js +121 -0
  68. package/dist/egress/linuxBootstrap.js.map +1 -0
  69. package/dist/egress/run.d.ts +18 -0
  70. package/dist/egress/run.js +305 -0
  71. package/dist/egress/run.js.map +1 -0
  72. package/dist/errors/errorEngine.d.ts +2 -2
  73. package/dist/errors/errorEngine.js +3 -40
  74. package/dist/errors/errorEngine.js.map +1 -1
  75. package/dist/evals/evals.d.ts +126 -0
  76. package/dist/evals/evals.js +466 -0
  77. package/dist/evals/evals.js.map +1 -0
  78. package/dist/evals/index.d.ts +2 -0
  79. package/dist/evals/index.js +2 -0
  80. package/dist/evals/index.js.map +1 -0
  81. package/dist/evidence/index.d.ts +2 -0
  82. package/dist/evidence/index.js +2 -0
  83. package/dist/evidence/index.js.map +1 -0
  84. package/dist/evidence/report.d.ts +143 -0
  85. package/dist/evidence/report.js +556 -0
  86. package/dist/evidence/report.js.map +1 -0
  87. package/dist/fault/faultLab.d.ts +5 -4
  88. package/dist/fault/faultLab.js +66 -24
  89. package/dist/fault/faultLab.js.map +1 -1
  90. package/dist/index.d.ts +46 -0
  91. package/dist/index.js +23 -0
  92. package/dist/index.js.map +1 -1
  93. package/dist/inventory/analysis.d.ts +12 -0
  94. package/dist/inventory/analysis.js +457 -0
  95. package/dist/inventory/analysis.js.map +1 -0
  96. package/dist/inventory/index.d.ts +5 -0
  97. package/dist/inventory/index.js +4 -0
  98. package/dist/inventory/index.js.map +1 -0
  99. package/dist/inventory/inventoryController.d.ts +53 -0
  100. package/dist/inventory/inventoryController.js +1044 -0
  101. package/dist/inventory/inventoryController.js.map +1 -0
  102. package/dist/inventory/types.d.ts +579 -0
  103. package/dist/inventory/types.js +787 -0
  104. package/dist/inventory/types.js.map +1 -0
  105. package/dist/landing/assets/index-CxWsopzP.css +1 -0
  106. package/dist/landing/assets/index-DZlpyhqd.js +55 -0
  107. package/dist/landing/assets/index-Dk-Xezs3.js +55 -0
  108. package/dist/landing/index.html +15 -15
  109. package/dist/ledger/actionLedger.d.ts +125 -0
  110. package/dist/ledger/actionLedger.js +594 -0
  111. package/dist/ledger/actionLedger.js.map +1 -0
  112. package/dist/ledger/index.d.ts +2 -0
  113. package/dist/ledger/index.js +2 -0
  114. package/dist/ledger/index.js.map +1 -0
  115. package/dist/mcp/server.js +4 -3
  116. package/dist/mcp/server.js.map +1 -1
  117. package/dist/policy/index.d.ts +3 -0
  118. package/dist/policy/index.js +2 -0
  119. package/dist/policy/index.js.map +1 -0
  120. package/dist/policy/policy.d.ts +14 -0
  121. package/dist/policy/policy.js +282 -0
  122. package/dist/policy/policy.js.map +1 -0
  123. package/dist/policy/types.d.ts +52 -0
  124. package/dist/policy/types.js +2 -0
  125. package/dist/policy/types.js.map +1 -0
  126. package/dist/productTelemetry/index.d.ts +16 -0
  127. package/dist/productTelemetry/index.js +142 -0
  128. package/dist/productTelemetry/index.js.map +1 -0
  129. package/dist/providers/conformance.d.ts +7 -0
  130. package/dist/providers/conformance.js +48 -0
  131. package/dist/providers/conformance.js.map +1 -0
  132. package/dist/providers/index.d.ts +4 -2
  133. package/dist/providers/index.js +3 -1
  134. package/dist/providers/index.js.map +1 -1
  135. package/dist/providers/packs/resendPack.d.ts +2 -0
  136. package/dist/providers/packs/resendPack.js +147 -0
  137. package/dist/providers/packs/resendPack.js.map +1 -0
  138. package/dist/providers/packs/stripePack.d.ts +2 -0
  139. package/dist/providers/packs/stripePack.js +852 -0
  140. package/dist/providers/packs/stripePack.js.map +1 -0
  141. package/dist/providers/registry.d.ts +7 -3
  142. package/dist/providers/registry.js +55 -4
  143. package/dist/providers/registry.js.map +1 -1
  144. package/dist/providers/resend.js +2 -11
  145. package/dist/providers/resend.js.map +1 -1
  146. package/dist/providers/runtime.d.ts +8 -0
  147. package/dist/providers/runtime.js +48 -0
  148. package/dist/providers/runtime.js.map +1 -0
  149. package/dist/providers/stripeWebhook.d.ts +13 -0
  150. package/dist/providers/stripeWebhook.js +50 -0
  151. package/dist/providers/stripeWebhook.js.map +1 -0
  152. package/dist/providers/types.d.ts +153 -0
  153. package/dist/proxy/cacheKey.js +3 -3
  154. package/dist/proxy/cacheKey.js.map +1 -1
  155. package/dist/proxy/providerDetector.d.ts +2 -7
  156. package/dist/proxy/providerDetector.js +4 -4
  157. package/dist/proxy/providerDetector.js.map +1 -1
  158. package/dist/proxy/proxyHandler.js +116 -12
  159. package/dist/proxy/proxyHandler.js.map +1 -1
  160. package/dist/proxy/requestNormalizer.d.ts +0 -1
  161. package/dist/proxy/requestNormalizer.js +2 -5
  162. package/dist/proxy/requestNormalizer.js.map +1 -1
  163. package/dist/reliability/costGovernance.d.ts +175 -0
  164. package/dist/reliability/costGovernance.js +515 -0
  165. package/dist/reliability/costGovernance.js.map +1 -0
  166. package/dist/reliability/index.d.ts +8 -0
  167. package/dist/reliability/index.js +5 -0
  168. package/dist/reliability/index.js.map +1 -0
  169. package/dist/reliability/reconciliation.d.ts +157 -0
  170. package/dist/reliability/reconciliation.js +522 -0
  171. package/dist/reliability/reconciliation.js.map +1 -0
  172. package/dist/reliability/runtimeHealth.d.ts +60 -0
  173. package/dist/reliability/runtimeHealth.js +356 -0
  174. package/dist/reliability/runtimeHealth.js.map +1 -0
  175. package/dist/reliability/slo.d.ts +131 -0
  176. package/dist/reliability/slo.js +375 -0
  177. package/dist/reliability/slo.js.map +1 -0
  178. package/dist/report/safetyReport.js +7 -3
  179. package/dist/report/safetyReport.js.map +1 -1
  180. package/dist/safety/index.d.ts +2 -0
  181. package/dist/safety/index.js +2 -0
  182. package/dist/safety/index.js.map +1 -0
  183. package/dist/safety/safetyController.d.ts +187 -0
  184. package/dist/safety/safetyController.js +477 -0
  185. package/dist/safety/safetyController.js.map +1 -0
  186. package/dist/scenarios/scenarioBundle.d.ts +75 -0
  187. package/dist/scenarios/scenarioBundle.js +750 -0
  188. package/dist/scenarios/scenarioBundle.js.map +1 -0
  189. package/dist/scenarios/scenarioStore.d.ts +1 -0
  190. package/dist/scenarios/scenarioStore.js +47 -32
  191. package/dist/scenarios/scenarioStore.js.map +1 -1
  192. package/dist/security/headerSanitizer.d.ts +2 -0
  193. package/dist/security/headerSanitizer.js +36 -1
  194. package/dist/security/headerSanitizer.js.map +1 -1
  195. package/dist/security/secrets.js +24 -4
  196. package/dist/security/secrets.js.map +1 -1
  197. package/dist/server/accessControl.d.ts +5 -0
  198. package/dist/server/accessControl.js +100 -0
  199. package/dist/server/accessControl.js.map +1 -0
  200. package/dist/server/createServer.d.ts +0 -5
  201. package/dist/server/createServer.js +9 -29
  202. package/dist/server/createServer.js.map +1 -1
  203. package/dist/server/eventsStore.d.ts +6 -1
  204. package/dist/server/eventsStore.js +83 -15
  205. package/dist/server/eventsStore.js.map +1 -1
  206. package/dist/server/routes.js +42 -10
  207. package/dist/server/routes.js.map +1 -1
  208. package/dist/server/sse.d.ts +2 -1
  209. package/dist/server/sse.js +15 -3
  210. package/dist/server/sse.js.map +1 -1
  211. package/dist/setup/setupGenerator.js +53 -6
  212. package/dist/setup/setupGenerator.js.map +1 -1
  213. package/dist/state/stateStore.d.ts +5 -0
  214. package/dist/state/stateStore.js +48 -62
  215. package/dist/state/stateStore.js.map +1 -1
  216. package/dist/storage/fileStore.d.ts +10 -0
  217. package/dist/storage/fileStore.js +131 -0
  218. package/dist/storage/fileStore.js.map +1 -0
  219. package/dist/teamControl/controlPlane.d.ts +255 -0
  220. package/dist/teamControl/controlPlane.js +918 -0
  221. package/dist/teamControl/controlPlane.js.map +1 -0
  222. package/dist/teamControl/deployment.d.ts +33 -0
  223. package/dist/teamControl/deployment.js +67 -0
  224. package/dist/teamControl/deployment.js.map +1 -0
  225. package/dist/teamControl/index.d.ts +4 -0
  226. package/dist/teamControl/index.js +3 -0
  227. package/dist/teamControl/index.js.map +1 -0
  228. package/dist/tests/testGenerator.js +4 -1
  229. package/dist/tests/testGenerator.js.map +1 -1
  230. package/dist/trust/index.d.ts +2 -0
  231. package/dist/trust/index.js +2 -0
  232. package/dist/trust/index.js.map +1 -0
  233. package/dist/trust/trustLadder.d.ts +182 -0
  234. package/dist/trust/trustLadder.js +560 -0
  235. package/dist/trust/trustLadder.js.map +1 -0
  236. package/dist/worlds/index.d.ts +2 -0
  237. package/dist/worlds/index.js +2 -0
  238. package/dist/worlds/index.js.map +1 -0
  239. package/dist/worlds/worlds.d.ts +160 -0
  240. package/dist/worlds/worlds.js +491 -0
  241. package/dist/worlds/worlds.js.map +1 -0
  242. package/docs/assets/dashboard.png +0 -0
  243. package/docs/ci.md +49 -0
  244. package/docs/commercial/README.md +29 -0
  245. package/docs/commercial/data-inventory.md +36 -0
  246. package/docs/commercial/manual-invoicing.md +44 -0
  247. package/docs/commercial/metrics.md +51 -0
  248. package/docs/commercial/pricing.md +103 -0
  249. package/docs/commercial/privacy-policy-draft.md +41 -0
  250. package/docs/commercial/terms-topics-for-counsel.md +51 -0
  251. package/docs/design-partners/README.md +52 -0
  252. package/docs/design-partners/demo-narratives.md +33 -0
  253. package/docs/design-partners/design-partner-offer.md +39 -0
  254. package/docs/design-partners/discovery-questions.md +28 -0
  255. package/docs/design-partners/feedback-capture-template.md +48 -0
  256. package/docs/design-partners/icp-one-pager.md +43 -0
  257. package/docs/design-partners/onboarding-checklist.md +33 -0
  258. package/docs/design-partners/pilot-success-criteria.md +27 -0
  259. package/docs/design-partners/pricing-interview-script.md +18 -0
  260. package/docs/design-partners/telemetry-plan.md +48 -0
  261. package/docs/development/baseline.md +114 -0
  262. package/docs/development/onboarding-smoke.md +37 -0
  263. package/docs/development/verification-0.1.8.md +25 -0
  264. package/docs/enterprise-product-roadmap-ru.md +1879 -0
  265. package/docs/fundraising/README.md +87 -0
  266. package/docs/fundraising/data-room-checklist.md +72 -0
  267. package/docs/fundraising/demo-script.md +52 -0
  268. package/docs/fundraising/design-partners-50.md +74 -0
  269. package/docs/fundraising/launch-posts.md +37 -0
  270. package/docs/fundraising/metrics-and-evidence.md +30 -0
  271. package/docs/fundraising/roadmap-12-month.md +25 -0
  272. package/docs/fundraising/technical-due-diligence-index.md +37 -0
  273. package/docs/fundraising/yc-application.md +25 -0
  274. package/docs/github-actions.md +84 -0
  275. package/docs/hosted-pilot.md +111 -0
  276. package/docs/mcp.md +52 -50
  277. package/docs/operations/disaster-recovery-runbook.md +81 -0
  278. package/docs/operations/kill-switch-runbook.md +32 -0
  279. package/docs/policy.md +61 -0
  280. package/docs/providers/authoring-packs.md +175 -0
  281. package/docs/providers/stripe-core-pack.md +82 -0
  282. package/docs/release-checklist.md +83 -71
  283. package/docs/release-migration-and-rollback.md +63 -0
  284. package/docs/release-readiness.md +39 -0
  285. package/docs/security/action-gateway-threat-model.md +34 -0
  286. package/docs/security/action-ledger-incident-replay-threat-model.md +42 -0
  287. package/docs/security/approval-inbox-threat-model.md +35 -0
  288. package/docs/security/credential-broker-threat-model.md +52 -0
  289. package/docs/security/egress-threat-model.md +126 -0
  290. package/docs/security/inventory-threat-model.md +104 -0
  291. package/docs/security/kill-switch-budgets-threat-model.md +37 -0
  292. package/docs/security/reliability-threat-model.md +111 -0
  293. package/docs/security/trust-ladder-threat-model.md +33 -0
  294. package/docs/team-control-plane.md +72 -0
  295. package/docs/usage.md +571 -57
  296. package/examples/README.md +10 -0
  297. package/examples/agent-instructions/README.md +39 -39
  298. package/examples/ci-smoke/README.md +21 -0
  299. package/examples/ci-smoke/ghostapi.policy.yaml +22 -0
  300. package/examples/ci-smoke/package.json +9 -0
  301. package/examples/ci-smoke/production-egress.mjs +2 -0
  302. package/examples/ci-smoke/safe.mjs +17 -0
  303. package/examples/evals/README.md +24 -0
  304. package/examples/evals/retry-after.eval.json +52 -0
  305. package/examples/generic-rest/README.md +27 -27
  306. package/examples/github-fetch/README.md +35 -35
  307. package/examples/openai-streaming/README.md +16 -0
  308. package/examples/openai-streaming/streaming-tool-call.mjs +31 -0
  309. package/examples/policy/ghostapi.policy.yaml +27 -0
  310. package/examples/record-replay/README.md +19 -0
  311. package/examples/record-replay/replay-requests.json +22 -0
  312. package/examples/record-replay/stripe-checkout.har.json +30 -0
  313. package/examples/resend-node/README.md +22 -22
  314. package/examples/stripe-node/README.md +35 -29
  315. package/examples/stripe-node/checkout-flow.mjs +61 -0
  316. package/examples/twilio-node/README.md +21 -21
  317. package/examples/worlds/README.md +19 -0
  318. package/examples/worlds/subscription-recovery.mjs +12 -0
  319. package/package.json +27 -12
  320. package/dist/providers/stripe.d.ts +0 -2
  321. package/dist/providers/stripe.js +0 -15
  322. package/dist/providers/stripe.js.map +0 -1
@@ -0,0 +1,918 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { lstat, readFile } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import { getDataPaths } from "../config/dataPaths.js";
5
+ import { validateEvidenceReport } from "../evidence/index.js";
6
+ import { isSecretFieldName, sanitizeSecretString } from "../security/secrets.js";
7
+ import { atomicWriteJson, ensurePrivateDirectory, withFileLock } from "../storage/fileStore.js";
8
+ const SCHEMA_VERSION = 3;
9
+ const MAX_STORE_BYTES = 1024 * 1024;
10
+ const TOKEN_PREFIX = "gapi_team_";
11
+ const IDENTIFIER = /^[a-z0-9][a-z0-9_-]{0,63}$/;
12
+ const MAX_EVIDENCE_PER_ORGANIZATION = 100;
13
+ const MAX_AUDIT_PER_ORGANIZATION = 1_000;
14
+ const EVIDENCE_RETENTION_DAYS = 30;
15
+ const AUDIT_RETENTION_DAYS = 90;
16
+ const MAX_SERVICE_TOKEN_SCOPES = 20;
17
+ const authenticatedServiceActors = new WeakMap();
18
+ const READ_PERMISSIONS = ["project.read", "environment.read", "scenario.read", "evidence.read", "policy.read"];
19
+ const PUBLISH_PERMISSIONS = ["scenario.publish", "evidence.upload"];
20
+ const OWNER_PERMISSIONS = ["member.manage", "project.manage", "environment.manage", "policy.manage", "token.manage", "service_account.manage", "audit.read", "audit.export", "data.delete", "retention.manage", ...READ_PERMISSIONS, ...PUBLISH_PERMISSIONS];
21
+ const ADMIN_PERMISSIONS = OWNER_PERMISSIONS.filter((permission) => permission !== "member.manage");
22
+ const DEVELOPER_PERMISSIONS = [...READ_PERMISSIONS, ...PUBLISH_PERMISSIONS];
23
+ const VIEWER_PERMISSIONS = [...READ_PERMISSIONS];
24
+ const SERVICE_ACCOUNT_PERMISSIONS = [...DEVELOPER_PERMISSIONS.filter((permission) => permission !== "policy.read")];
25
+ export const TEAM_PERMISSION_MATRIX = Object.freeze({
26
+ owner: Object.freeze([...OWNER_PERMISSIONS]),
27
+ admin: Object.freeze([...ADMIN_PERMISSIONS]),
28
+ developer: Object.freeze([...DEVELOPER_PERMISSIONS]),
29
+ viewer: Object.freeze([...VIEWER_PERMISSIONS]),
30
+ service_account: Object.freeze([...SERVICE_ACCOUNT_PERMISSIONS])
31
+ });
32
+ export class TeamControlPlaneError extends Error {
33
+ constructor(message) {
34
+ super(message);
35
+ this.name = "TeamControlPlaneError";
36
+ }
37
+ }
38
+ export function createLocalTeamControlPlane(options = {}) {
39
+ return new LocalTeamControlPlane(options);
40
+ }
41
+ export class LocalTeamControlPlane {
42
+ path;
43
+ now;
44
+ randomTokenBytes;
45
+ constructor(options = {}) {
46
+ this.path = options.path ?? getDataPaths().teamControlPlane;
47
+ this.now = options.now ?? (() => new Date());
48
+ this.randomTokenBytes = options.randomTokenBytes ?? randomBytes;
49
+ }
50
+ async bootstrapOrganization(input) {
51
+ const organizationId = identifier(input.organizationId, "Organization id");
52
+ const ownerId = identifier(input.ownerId, "Owner id");
53
+ const name = text(input.name, "Organization name", 120);
54
+ return this.mutate((state, now) => {
55
+ if (state.organizations.some((organization) => organization.id === organizationId))
56
+ throw new TeamControlPlaneError("Organization already exists.");
57
+ const organization = { id: organizationId, name, createdAt: now };
58
+ state.organizations.push(organization);
59
+ state.members.push({ id: ownerId, organizationId, role: "owner", createdAt: now });
60
+ state.auditAnchors.push({ organizationId, sequence: 0, hash: auditGenesisHash(organizationId) });
61
+ appendAudit(state, organizationId, ownerId, "organization.bootstrap", organizationId, now);
62
+ return organization;
63
+ });
64
+ }
65
+ async addMember(actor, input) {
66
+ const memberId = identifier(input.memberId, "Member id");
67
+ if (input.role !== "admin" && input.role !== "developer" && input.role !== "viewer")
68
+ throw new TeamControlPlaneError("Member role is invalid.");
69
+ return this.mutate((state, now) => {
70
+ requirePermission(state, actor, "member.manage");
71
+ if (state.members.some((member) => member.organizationId === actor.organizationId && member.id === memberId))
72
+ throw new TeamControlPlaneError("Member already exists.");
73
+ const member = { id: memberId, organizationId: actor.organizationId, role: input.role, createdAt: now };
74
+ state.members.push(member);
75
+ appendAudit(state, actor.organizationId, actorId(actor), "member.add", memberId, now);
76
+ return member;
77
+ });
78
+ }
79
+ async createServiceAccount(actor, input) {
80
+ const serviceAccountId = identifier(input.serviceAccountId, "Service account id");
81
+ const name = text(input.name, "Service account name", 120);
82
+ return this.mutate((state, now) => {
83
+ requirePermission(state, actor, "service_account.manage");
84
+ if (state.serviceAccounts.some((account) => account.organizationId === actor.organizationId && account.id === serviceAccountId))
85
+ throw new TeamControlPlaneError("Service account already exists.");
86
+ const account = { id: serviceAccountId, organizationId: actor.organizationId, name, createdBy: actorId(actor), createdAt: now };
87
+ state.serviceAccounts.push(account);
88
+ appendAudit(state, actor.organizationId, actorId(actor), "service_account.create", serviceAccountId, now);
89
+ return account;
90
+ });
91
+ }
92
+ async disableServiceAccount(actor, serviceAccountId) {
93
+ const id = identifier(serviceAccountId, "Service account id");
94
+ await this.mutate((state, now) => {
95
+ requirePermission(state, actor, "service_account.manage");
96
+ const account = state.serviceAccounts.find((candidate) => candidate.organizationId === actor.organizationId && candidate.id === id);
97
+ if (account === undefined)
98
+ throw new TeamControlPlaneError("Resource not found.");
99
+ if (account.disabledAt === undefined) {
100
+ account.disabledAt = now;
101
+ for (const token of state.tokens) {
102
+ if (token.organizationId === actor.organizationId && token.type === "service" && token.subjectId === id && token.revokedAt === undefined)
103
+ token.revokedAt = now;
104
+ }
105
+ appendAudit(state, actor.organizationId, actorId(actor), "service_account.disable", id, now);
106
+ }
107
+ });
108
+ }
109
+ async registerProject(actor, input) {
110
+ const projectId = identifier(input.projectId, "Project id");
111
+ const name = text(input.name, "Project name", 120);
112
+ return this.mutate((state, now) => {
113
+ requirePermission(state, actor, "project.manage");
114
+ if (state.projects.some((project) => project.organizationId === actor.organizationId && project.id === projectId))
115
+ throw new TeamControlPlaneError("Project already exists.");
116
+ const project = { id: projectId, organizationId: actor.organizationId, name, createdAt: now };
117
+ state.projects.push(project);
118
+ appendAudit(state, actor.organizationId, actorId(actor), "project.register", projectId, now);
119
+ return project;
120
+ });
121
+ }
122
+ async createEnvironment(actor, input) {
123
+ const environmentId = identifier(input.environmentId, "Environment id");
124
+ const projectId = identifier(input.projectId, "Project id");
125
+ const name = text(input.name, "Environment name", 80);
126
+ if (input.kind !== "development" && input.kind !== "ci")
127
+ throw new TeamControlPlaneError("Environment kind is invalid.");
128
+ return this.mutate((state, now) => {
129
+ requirePermission(state, actor, "environment.manage");
130
+ requireProject(state, actor.organizationId, projectId);
131
+ if (state.environments.some((environment) => environment.organizationId === actor.organizationId && environment.projectId === projectId && environment.id === environmentId))
132
+ throw new TeamControlPlaneError("Environment already exists.");
133
+ const environment = { id: environmentId, organizationId: actor.organizationId, projectId, name, kind: input.kind, createdAt: now };
134
+ state.environments.push(environment);
135
+ appendAudit(state, actor.organizationId, actorId(actor), "environment.create", environmentId, now);
136
+ return environment;
137
+ });
138
+ }
139
+ async issueToken(actor, input) {
140
+ const expiresAt = futureTimestamp(input.expiresAt, this.now());
141
+ return this.mutate((state, now) => {
142
+ requirePermission(state, actor, "token.manage");
143
+ const memberId = input.memberId === undefined ? userActorId(actor) : identifier(input.memberId, "Token member id");
144
+ const subject = requireMember(state, { organizationId: actor.organizationId, memberId });
145
+ if (!canManageMemberToken(actorRole(state, actor), subject.role))
146
+ throw new TeamControlPlaneError("Access denied.");
147
+ const issued = issueTokenRecord(state, { organizationId: actor.organizationId, type: "user", subjectId: memberId, issuedBy: actorId(actor), expiresAt }, now, this.randomTokenBytes);
148
+ appendAudit(state, actor.organizationId, actorId(actor), "token.issue", issued.tokenId, now);
149
+ return issued;
150
+ });
151
+ }
152
+ async issueServiceToken(actor, input) {
153
+ const serviceAccountId = identifier(input.serviceAccountId, "Service account id");
154
+ const expiresAt = futureTimestamp(input.expiresAt, this.now());
155
+ return this.mutate((state, now) => {
156
+ requirePermission(state, actor, "token.manage");
157
+ requireServiceAccount(state, actor.organizationId, serviceAccountId);
158
+ const scope = validateTokenScope(input.scope, state, actor.organizationId);
159
+ const issued = issueTokenRecord(state, { organizationId: actor.organizationId, type: "service", subjectId: serviceAccountId, issuedBy: actorId(actor), expiresAt, scope }, now, this.randomTokenBytes);
160
+ appendAudit(state, actor.organizationId, actorId(actor), "token.issue_service", issued.tokenId, now);
161
+ return issued;
162
+ });
163
+ }
164
+ async rotateToken(actor, tokenId, input = {}) {
165
+ const id = identifier(tokenId, "Token id");
166
+ return this.mutate((state, now) => {
167
+ requirePermission(state, actor, "token.manage");
168
+ const oldToken = state.tokens.find((candidate) => candidate.id === id && candidate.organizationId === actor.organizationId);
169
+ if (oldToken === undefined)
170
+ throw new TeamControlPlaneError("Resource not found.");
171
+ if (oldToken.revokedAt !== undefined)
172
+ throw new TeamControlPlaneError("Token is already revoked.");
173
+ if (oldToken.type === "user" && !canManageMemberToken(actorRole(state, actor), requireMember(state, { organizationId: actor.organizationId, memberId: oldToken.subjectId }).role))
174
+ throw new TeamControlPlaneError("Access denied.");
175
+ const expiresAt = input.expiresAt === undefined ? oldToken.expiresAt : futureTimestamp(input.expiresAt, this.now());
176
+ if (new Date(expiresAt).getTime() <= new Date(now).getTime())
177
+ throw new TeamControlPlaneError("Token expiry must be within the next 90 days.");
178
+ oldToken.revokedAt = now;
179
+ const issued = issueTokenRecord(state, { organizationId: oldToken.organizationId, type: oldToken.type, subjectId: oldToken.subjectId, issuedBy: actorId(actor), expiresAt, ...(oldToken.scope === undefined ? {} : { scope: oldToken.scope }) }, now, this.randomTokenBytes);
180
+ appendAudit(state, actor.organizationId, actorId(actor), "token.rotate", `${id}->${issued.tokenId}`, now);
181
+ return issued;
182
+ });
183
+ }
184
+ async revokeToken(actor, tokenId) {
185
+ const id = identifier(tokenId, "Token id");
186
+ await this.mutate((state, now) => {
187
+ requirePermission(state, actor, "token.manage");
188
+ const token = state.tokens.find((candidate) => candidate.id === id && candidate.organizationId === actor.organizationId);
189
+ if (token === undefined)
190
+ throw new TeamControlPlaneError("Resource not found.");
191
+ if (token.type === "user" && !canManageMemberToken(actorRole(state, actor), requireMember(state, { organizationId: actor.organizationId, memberId: token.subjectId }).role))
192
+ throw new TeamControlPlaneError("Access denied.");
193
+ if (token.revokedAt === undefined)
194
+ token.revokedAt = now;
195
+ appendAudit(state, actor.organizationId, actorId(actor), "token.revoke", id, now);
196
+ });
197
+ }
198
+ async authenticateToken(token) {
199
+ if (!token.startsWith(TOKEN_PREFIX) || token.length > 256)
200
+ throw new TeamControlPlaneError("Invalid or expired token.");
201
+ const state = await this.read();
202
+ const digest = hash(token);
203
+ const record = state.tokens.find((candidate) => equalHash(candidate.digest, digest));
204
+ if (record === undefined || record.revokedAt !== undefined || new Date(record.expiresAt).getTime() <= this.now().getTime())
205
+ throw new TeamControlPlaneError("Invalid or expired token.");
206
+ if (record.type === "user") {
207
+ requireMember(state, { organizationId: record.organizationId, memberId: record.subjectId });
208
+ return { organizationId: record.organizationId, memberId: record.subjectId };
209
+ }
210
+ if (!state.serviceAccounts.some((account) => account.organizationId === record.organizationId && account.id === record.subjectId && account.disabledAt === undefined))
211
+ throw new TeamControlPlaneError("Invalid or expired token.");
212
+ validateTokenScope(record.scope, state, record.organizationId);
213
+ const actor = Object.freeze({ organizationId: record.organizationId, serviceAccountId: record.subjectId, actorType: "service_account", tokenId: record.id });
214
+ authenticatedServiceActors.set(actor, this.now);
215
+ return actor;
216
+ }
217
+ async publishScenario(actor, input) {
218
+ const projectId = identifier(input.projectId, "Project id");
219
+ const environmentId = identifier(input.environmentId, "Environment id");
220
+ const scenarioId = identifier(input.scenarioId, "Scenario id");
221
+ const version = positiveInteger(input.version, "Scenario version");
222
+ const title = text(input.title, "Scenario title", 160);
223
+ const metadata = safeJson(input.metadata, "Scenario metadata", 16 * 1024);
224
+ return this.mutate((state, now) => {
225
+ requireScopedPermission(state, actor, "scenario.publish", projectId, environmentId);
226
+ requireProject(state, actor.organizationId, projectId);
227
+ requireEnvironment(state, actor.organizationId, projectId, environmentId);
228
+ if (state.scenarios.some((scenario) => scenario.organizationId === actor.organizationId && scenario.projectId === projectId && scenario.environmentId === environmentId && scenario.scenarioId === scenarioId && scenario.version === version))
229
+ throw new TeamControlPlaneError("Scenario version already exists.");
230
+ const scenario = { organizationId: actor.organizationId, projectId, environmentId, scenarioId, version, title, metadata, createdBy: actorId(actor), createdAt: now };
231
+ state.scenarios.push(scenario);
232
+ appendAudit(state, actor.organizationId, actorId(actor), "scenario.publish", `${scenarioId}@${version}`, now);
233
+ return scenario;
234
+ });
235
+ }
236
+ async uploadSanitizedEvidence(actor, input) {
237
+ const evidenceId = identifier(input.evidenceId, "Evidence id");
238
+ const projectId = identifier(input.projectId, "Project id");
239
+ const environmentId = identifier(input.environmentId, "Environment id");
240
+ const report = validateEvidenceReport(input.report);
241
+ rejectRawEvidence(report);
242
+ return this.mutate((state, now) => {
243
+ requireScopedPermission(state, actor, "evidence.upload", projectId, environmentId);
244
+ requireProject(state, actor.organizationId, projectId);
245
+ requireEnvironment(state, actor.organizationId, projectId, environmentId);
246
+ if (state.evidence.some((evidence) => evidence.organizationId === actor.organizationId && evidence.id === evidenceId))
247
+ throw new TeamControlPlaneError("Evidence already exists.");
248
+ const evidence = {
249
+ id: evidenceId, organizationId: actor.organizationId, projectId, environmentId, runId: runId(report.run.id), evidenceHash: report.artifact.logicalHash, status: report.run.status,
250
+ passed: report.summary.passed, failCount: report.summary.failCount, warningCount: report.summary.warningCount, providers: list(report.coverage.providers, "Evidence providers"), scenarios: list(report.coverage.scenarios, "Evidence scenarios", true),
251
+ enforcement: { mode: report.enforcement.mode, isolated: report.enforcement.isolated, degraded: report.enforcement.degraded }, uploadedBy: actorId(actor), uploadedAt: now, expiresAt: addDays(now, EVIDENCE_RETENTION_DAYS)
252
+ };
253
+ state.evidence.push(evidence);
254
+ appendAudit(state, actor.organizationId, actorId(actor), "evidence.upload", evidenceId, now);
255
+ return evidence;
256
+ });
257
+ }
258
+ async deleteEvidence(actor, input) {
259
+ const projectId = identifier(input.projectId, "Project id");
260
+ const evidenceId = identifier(input.evidenceId, "Evidence id");
261
+ await this.mutate((state, now) => {
262
+ requirePermission(state, actor, "data.delete");
263
+ requireProject(state, actor.organizationId, projectId);
264
+ const index = state.evidence.findIndex((evidence) => evidence.organizationId === actor.organizationId && evidence.projectId === projectId && evidence.id === evidenceId);
265
+ if (index === -1)
266
+ throw new TeamControlPlaneError("Resource not found.");
267
+ state.evidence.splice(index, 1);
268
+ appendAudit(state, actor.organizationId, actorId(actor), "evidence.delete", evidenceId, now);
269
+ });
270
+ }
271
+ async distributePolicy(actor, input) {
272
+ const version = positiveInteger(input.version, "Policy version");
273
+ const policy = safeJson(input.policy, "Team policy", 32 * 1024);
274
+ validatePolicy(policy);
275
+ return this.mutate((state, now) => {
276
+ requirePermission(state, actor, "policy.manage");
277
+ if (state.policies.some((candidate) => candidate.organizationId === actor.organizationId && candidate.version === version))
278
+ throw new TeamControlPlaneError("Policy version already exists.");
279
+ const distributed = { organizationId: actor.organizationId, version, policy, hash: stableHash(policy), distributedBy: actorId(actor), distributedAt: now };
280
+ state.policies.push(distributed);
281
+ appendAudit(state, actor.organizationId, actorId(actor), "policy.distribute", `policy@${version}`, now);
282
+ return distributed;
283
+ });
284
+ }
285
+ async listProjects(actor) {
286
+ const state = await this.read();
287
+ requirePermission(state, actor, "project.read");
288
+ return state.projects.filter((project) => project.organizationId === actor.organizationId && (!isServiceActor(actor) || serviceTokenScopes(state, actor).some((scope) => scope.projectId === project.id && scope.permissions.includes("project.read")))).map((project) => structuredClone(project));
289
+ }
290
+ async listEvidence(actor, projectId) {
291
+ const id = identifier(projectId, "Project id");
292
+ const state = await this.read();
293
+ if (isServiceActor(actor)) {
294
+ if (!serviceTokenScopes(state, actor).some((scope) => scope.projectId === id && scope.permissions.includes("evidence.read")))
295
+ throw new TeamControlPlaneError("Access denied.");
296
+ }
297
+ else {
298
+ requirePermission(state, actor, "evidence.read");
299
+ }
300
+ requireProject(state, actor.organizationId, id);
301
+ return state.evidence.filter((evidence) => evidence.organizationId === actor.organizationId && evidence.projectId === id && canReadEvidence(state, actor, evidence)).map((evidence) => structuredClone(evidence));
302
+ }
303
+ async listScenarioVersions(actor, input) {
304
+ const projectId = identifier(input.projectId, "Project id");
305
+ const environmentId = identifier(input.environmentId, "Environment id");
306
+ const scenarioId = identifier(input.scenarioId, "Scenario id");
307
+ const state = await this.read();
308
+ requireScopedPermission(state, actor, "scenario.read", projectId, environmentId);
309
+ requireEnvironment(state, actor.organizationId, projectId, environmentId);
310
+ return state.scenarios.filter((scenario) => scenario.organizationId === actor.organizationId && scenario.projectId === projectId && scenario.environmentId === environmentId && scenario.scenarioId === scenarioId).sort((left, right) => left.version - right.version).map((scenario) => structuredClone(scenario));
311
+ }
312
+ async getLatestPolicy(actor) {
313
+ const state = await this.read();
314
+ requirePermission(state, actor, "policy.read");
315
+ const policy = state.policies.filter((candidate) => candidate.organizationId === actor.organizationId).sort((left, right) => right.version - left.version)[0];
316
+ return policy === undefined ? null : structuredClone(policy);
317
+ }
318
+ async listAudit(actor) {
319
+ const state = await this.read();
320
+ requirePermission(state, actor, "audit.read");
321
+ return state.audit.filter((record) => record.organizationId === actor.organizationId).map((record) => structuredClone(record));
322
+ }
323
+ async exportAudit(actor) {
324
+ const state = await this.read();
325
+ requirePermission(state, actor, "audit.export");
326
+ const anchor = state.auditAnchors.find((candidate) => candidate.organizationId === actor.organizationId);
327
+ if (anchor === undefined)
328
+ throw new TeamControlPlaneError("Resource not found.");
329
+ const records = state.audit.filter((record) => record.organizationId === actor.organizationId).map((record) => structuredClone(record));
330
+ return { organizationId: actor.organizationId, anchor: structuredClone(anchor), records, integrity: verifyAuditChain(actor.organizationId, anchor, records) };
331
+ }
332
+ async pruneRetention(actor) {
333
+ return this.mutate((state, now) => {
334
+ requirePermission(state, actor, "retention.manage");
335
+ const beforeEvidence = state.evidence.length;
336
+ const beforeAudit = state.audit.length;
337
+ retain(state, now);
338
+ appendAudit(state, actor.organizationId, actorId(actor), "retention.prune", "team-control-plane", now);
339
+ retain(state, now);
340
+ return { evidenceRemoved: beforeEvidence - state.evidence.length, auditRemoved: beforeAudit + 1 - state.audit.length };
341
+ }, false);
342
+ }
343
+ async deleteProject(actor, projectId) {
344
+ const id = identifier(projectId, "Project id");
345
+ await this.mutate((state, now) => {
346
+ requirePermission(state, actor, "data.delete");
347
+ requireProject(state, actor.organizationId, id);
348
+ state.projects = state.projects.filter((project) => !(project.organizationId === actor.organizationId && project.id === id));
349
+ state.environments = state.environments.filter((environment) => !(environment.organizationId === actor.organizationId && environment.projectId === id));
350
+ state.scenarios = state.scenarios.filter((scenario) => !(scenario.organizationId === actor.organizationId && scenario.projectId === id));
351
+ state.evidence = state.evidence.filter((evidence) => !(evidence.organizationId === actor.organizationId && evidence.projectId === id));
352
+ state.tokens = state.tokens.filter((token) => !(token.organizationId === actor.organizationId && token.type === "service" && token.scope?.some((scope) => scope.projectId === id)));
353
+ appendAudit(state, actor.organizationId, actorId(actor), "project.delete", id, now);
354
+ });
355
+ }
356
+ async readStateForTesting() {
357
+ return this.read();
358
+ }
359
+ async read() {
360
+ await ensurePrivateDirectory(dirname(this.path));
361
+ const info = await lstat(this.path).catch((error) => isErrorCode(error, "ENOENT") ? null : Promise.reject(error));
362
+ if (info === null)
363
+ return emptyState();
364
+ if (!info.isFile() || info.isSymbolicLink())
365
+ throw new TeamControlPlaneError("Team control-plane store must be a regular non-symlink file.");
366
+ const source = await readFile(this.path, "utf8");
367
+ if (Buffer.byteLength(source, "utf8") > MAX_STORE_BYTES)
368
+ throw new TeamControlPlaneError(`Team control-plane store exceeds ${MAX_STORE_BYTES} bytes.`);
369
+ try {
370
+ const state = migrateTeamControlPlane(JSON.parse(source));
371
+ for (const organization of state.organizations) {
372
+ const anchor = state.auditAnchors.find((candidate) => candidate.organizationId === organization.id);
373
+ if (anchor === undefined || !verifyAuditChain(organization.id, anchor, state.audit.filter((record) => record.organizationId === organization.id)).valid)
374
+ throw new TeamControlPlaneError("Audit integrity verification failed.");
375
+ }
376
+ return state;
377
+ }
378
+ catch (error) {
379
+ if (error instanceof TeamControlPlaneError)
380
+ throw error;
381
+ throw new TeamControlPlaneError("Team control-plane store is not valid JSON.");
382
+ }
383
+ }
384
+ async mutate(operation, autoRetain = true) {
385
+ return withFileLock(this.path, async () => {
386
+ const state = await this.read();
387
+ const now = this.now().toISOString();
388
+ const result = operation(state, now);
389
+ if (autoRetain)
390
+ retain(state, now);
391
+ await atomicWriteJson(this.path, validateState(state));
392
+ return result;
393
+ });
394
+ }
395
+ }
396
+ export function verifyAuditExport(value) {
397
+ try {
398
+ const exportValue = object(value, "Audit export is invalid.");
399
+ exactKeys(exportValue, ["organizationId", "anchor", "records", "integrity"], "Audit export");
400
+ const organizationId = identifier(exportValue.organizationId, "Audit export organization id");
401
+ const anchor = auditAnchor(exportValue.anchor, new Set([organizationId]));
402
+ const records = array(exportValue.records, "Audit export records", MAX_AUDIT_PER_ORGANIZATION).map((record) => auditRecord(record, new Set([organizationId]), [], []));
403
+ return verifyAuditChain(organizationId, anchor, records);
404
+ }
405
+ catch (error) {
406
+ return { valid: false, reason: error instanceof Error ? error.message : "Audit export is invalid." };
407
+ }
408
+ }
409
+ export function migrateTeamControlPlane(value) {
410
+ const state = object(value, "Team control-plane store must be an object.");
411
+ if (state.schemaVersion === 1) {
412
+ exactKeys(state, ["schemaVersion", "organizations", "members", "projects", "environments", "tokens", "scenarios", "evidence", "policies"], "Team control-plane v1 store");
413
+ return migrateV2State({ ...state, schemaVersion: 2, audit: [] });
414
+ }
415
+ if (state.schemaVersion === 2)
416
+ return migrateV2State(state);
417
+ return validateState(state);
418
+ }
419
+ function migrateV2State(value) {
420
+ exactKeys(value, ["schemaVersion", "organizations", "members", "projects", "environments", "tokens", "scenarios", "evidence", "policies", "audit"], "Team control-plane v2 store");
421
+ if (value.schemaVersion !== 2)
422
+ throw new TeamControlPlaneError("Unsupported team control-plane schema version.");
423
+ const members = array(value.members, "Members", 1_000).map((raw) => {
424
+ const entry = object(raw, "Member is invalid.");
425
+ exactKeys(entry, ["id", "organizationId", "role", "createdAt"], "Member");
426
+ if (entry.role !== "owner" && entry.role !== "admin" && entry.role !== "member")
427
+ throw new TeamControlPlaneError("Member is invalid.");
428
+ return { ...entry, role: entry.role === "member" ? "developer" : entry.role };
429
+ });
430
+ const tokens = array(value.tokens, "Tokens", 10_000).map((raw) => {
431
+ const entry = object(raw, "Token is invalid.");
432
+ exactKeys(entry, ["id", "organizationId", "memberId", "digest", "issuedBy", "issuedAt", "expiresAt", "revokedAt"], "Token", ["revokedAt"]);
433
+ const { memberId, ...rest } = entry;
434
+ return { ...rest, type: "user", subjectId: memberId };
435
+ });
436
+ const base = {
437
+ schemaVersion: 3,
438
+ organizations: value.organizations,
439
+ members,
440
+ serviceAccounts: [],
441
+ projects: value.projects,
442
+ environments: value.environments,
443
+ tokens,
444
+ scenarios: value.scenarios,
445
+ evidence: value.evidence,
446
+ policies: value.policies,
447
+ auditAnchors: [],
448
+ audit: []
449
+ };
450
+ const validatedBase = validateState({ ...base, auditAnchors: array(value.organizations, "Organizations", 100).map((raw) => ({ organizationId: identifier(object(raw, "Organization is invalid.").id, "Organization id"), sequence: 0, hash: auditGenesisHash(identifier(object(raw, "Organization is invalid.").id, "Organization id")) })) });
451
+ const legacyAudit = array(value.audit, "Audit records", 100_000).map((raw) => legacyAuditRecord(raw, new Set(validatedBase.organizations.map((organization) => organization.id)), validatedBase.members));
452
+ for (const record of legacyAudit)
453
+ appendAudit(validatedBase, record.organizationId, record.actorId, record.action, record.resource, record.timestamp);
454
+ return validateState(validatedBase);
455
+ }
456
+ function emptyState() {
457
+ return { schemaVersion: 3, organizations: [], members: [], serviceAccounts: [], projects: [], environments: [], tokens: [], scenarios: [], evidence: [], policies: [], auditAnchors: [], audit: [] };
458
+ }
459
+ function validateState(value) {
460
+ const state = object(value, "Team control-plane store must be an object.");
461
+ exactKeys(state, ["schemaVersion", "organizations", "members", "serviceAccounts", "projects", "environments", "tokens", "scenarios", "evidence", "policies", "auditAnchors", "audit"], "Team control-plane store");
462
+ if (state.schemaVersion !== SCHEMA_VERSION)
463
+ throw new TeamControlPlaneError("Unsupported team control-plane schema version.");
464
+ const organizations = array(state.organizations, "Organizations", 100).map((entry) => {
465
+ const organization = object(entry, "Organization is invalid.");
466
+ exactKeys(organization, ["id", "name", "createdAt"], "Organization");
467
+ return { id: identifier(organization.id, "Organization id"), name: text(organization.name, "Organization name", 120), createdAt: timestamp(organization.createdAt, "Organization createdAt") };
468
+ });
469
+ unique(organizations.map((entry) => entry.id), "Organization ids");
470
+ const organizationIds = new Set(organizations.map((entry) => entry.id));
471
+ const members = array(state.members, "Members", 1_000).map((entry) => member(entry, organizationIds));
472
+ unique(members.map((entry) => scopedKey(entry.organizationId, entry.id)), "Organization member ids");
473
+ for (const organization of organizations)
474
+ if (!members.some((candidate) => candidate.organizationId === organization.id && candidate.role === "owner"))
475
+ throw new TeamControlPlaneError("Each organization requires an owner.");
476
+ const serviceAccounts = array(state.serviceAccounts, "Service accounts", 1_000).map((entry) => serviceAccount(entry, organizationIds, members));
477
+ unique(serviceAccounts.map((entry) => scopedKey(entry.organizationId, entry.id)), "Organization service account ids");
478
+ const actorIds = actorReferenceIds(members, serviceAccounts);
479
+ const projects = array(state.projects, "Projects", 1_000).map((entry) => project(entry, organizationIds));
480
+ unique(projects.map((entry) => scopedKey(entry.organizationId, entry.id)), "Organization project ids");
481
+ const projectOwners = new Set(projects.map((entry) => scopedKey(entry.organizationId, entry.id)));
482
+ const environments = array(state.environments, "Environments", 2_000).map((entry) => environment(entry, organizationIds, projectOwners));
483
+ unique(environments.map((entry) => scopedKey(entry.organizationId, entry.projectId, entry.id)), "Project environment ids");
484
+ const environmentsById = new Map(environments.map((entry) => [scopedKey(entry.organizationId, entry.projectId, entry.id), entry]));
485
+ const tokens = array(state.tokens, "Tokens", 10_000).map((entry) => token(entry, organizationIds, members, serviceAccounts, projects, environments));
486
+ unique(tokens.map((entry) => entry.id), "Token ids");
487
+ const scenarios = array(state.scenarios, "Scenarios", 10_000).map((entry) => scenario(entry, organizationIds, projectOwners, environmentsById, actorIds));
488
+ unique(scenarios.map((entry) => scopedKey(entry.organizationId, entry.projectId, entry.environmentId, entry.scenarioId, String(entry.version))), "Scenario versions");
489
+ const evidence = array(state.evidence, "Evidence", 10_000).map((entry) => evidenceRecord(entry, organizationIds, projectOwners, environmentsById, actorIds));
490
+ unique(evidence.map((entry) => scopedKey(entry.organizationId, entry.id)), "Organization evidence ids");
491
+ const policies = array(state.policies, "Policies", 10_000).map((entry) => policyVersion(entry, organizationIds, actorIds));
492
+ unique(policies.map((entry) => scopedKey(entry.organizationId, String(entry.version))), "Policy versions");
493
+ const auditAnchors = array(state.auditAnchors, "Audit anchors", 100).map((entry) => auditAnchor(entry, organizationIds));
494
+ unique(auditAnchors.map((entry) => entry.organizationId), "Audit anchor organization ids");
495
+ if (auditAnchors.length !== organizations.length || auditAnchors.some((anchor) => !organizationIds.has(anchor.organizationId)))
496
+ throw new TeamControlPlaneError("Each organization requires an audit anchor.");
497
+ const auditRecords = array(state.audit, "Audit records", 100_000).map((entry) => auditRecord(entry, organizationIds, members, serviceAccounts));
498
+ for (const organization of organizations) {
499
+ const anchor = auditAnchors.find((candidate) => candidate.organizationId === organization.id);
500
+ if (anchor === undefined || !verifyAuditChain(organization.id, anchor, auditRecords.filter((record) => record.organizationId === organization.id)).valid)
501
+ throw new TeamControlPlaneError("Audit chain is invalid.");
502
+ }
503
+ return { schemaVersion: 3, organizations, members, serviceAccounts, projects, environments, tokens, scenarios, evidence, policies, auditAnchors, audit: auditRecords };
504
+ }
505
+ function member(value, organizations) {
506
+ const entry = object(value, "Member is invalid.");
507
+ exactKeys(entry, ["id", "organizationId", "role", "createdAt"], "Member");
508
+ const organizationId = identifier(entry.organizationId, "Member organization id");
509
+ if (!organizations.has(organizationId) || (entry.role !== "owner" && entry.role !== "admin" && entry.role !== "developer" && entry.role !== "viewer"))
510
+ throw new TeamControlPlaneError("Member is invalid.");
511
+ return { id: identifier(entry.id, "Member id"), organizationId, role: entry.role, createdAt: timestamp(entry.createdAt, "Member createdAt") };
512
+ }
513
+ function serviceAccount(value, organizations, members) {
514
+ const entry = object(value, "Service account is invalid.");
515
+ exactKeys(entry, ["id", "organizationId", "name", "createdBy", "createdAt", "disabledAt"], "Service account", ["disabledAt"]);
516
+ const organizationId = identifier(entry.organizationId, "Service account organization id");
517
+ if (!organizations.has(organizationId) || !members.some((member) => member.organizationId === organizationId && member.id === entry.createdBy))
518
+ throw new TeamControlPlaneError("Service account is invalid.");
519
+ return { id: identifier(entry.id, "Service account id"), organizationId, name: text(entry.name, "Service account name", 120), createdBy: identifier(entry.createdBy, "Service account creator id"), createdAt: timestamp(entry.createdAt, "Service account createdAt"), ...(entry.disabledAt === undefined ? {} : { disabledAt: timestamp(entry.disabledAt, "Service account disabledAt") }) };
520
+ }
521
+ function project(value, organizations) {
522
+ const entry = object(value, "Project is invalid.");
523
+ exactKeys(entry, ["id", "organizationId", "name", "createdAt"], "Project");
524
+ const organizationId = identifier(entry.organizationId, "Project organization id");
525
+ if (!organizations.has(organizationId))
526
+ throw new TeamControlPlaneError("Project organization is invalid.");
527
+ return { id: identifier(entry.id, "Project id"), organizationId, name: text(entry.name, "Project name", 120), createdAt: timestamp(entry.createdAt, "Project createdAt") };
528
+ }
529
+ function environment(value, organizations, projects) {
530
+ const entry = object(value, "Environment is invalid.");
531
+ exactKeys(entry, ["id", "organizationId", "projectId", "name", "kind", "createdAt"], "Environment");
532
+ const organizationId = identifier(entry.organizationId, "Environment organization id");
533
+ const projectId = identifier(entry.projectId, "Environment project id");
534
+ if (!organizations.has(organizationId) || !projects.has(scopedKey(organizationId, projectId)) || (entry.kind !== "development" && entry.kind !== "ci"))
535
+ throw new TeamControlPlaneError("Environment is invalid.");
536
+ return { id: identifier(entry.id, "Environment id"), organizationId, projectId, name: text(entry.name, "Environment name", 80), kind: entry.kind, createdAt: timestamp(entry.createdAt, "Environment createdAt") };
537
+ }
538
+ function token(value, organizations, members, serviceAccounts, projects, environments) {
539
+ const entry = object(value, "Token is invalid.");
540
+ exactKeys(entry, ["id", "organizationId", "type", "subjectId", "digest", "issuedBy", "issuedAt", "expiresAt", "scope", "revokedAt"], "Token", ["scope", "revokedAt"]);
541
+ const organizationId = identifier(entry.organizationId, "Token organization id");
542
+ const subjectId = identifier(entry.subjectId, "Token subject id");
543
+ if (!organizations.has(organizationId) || (entry.type !== "user" && entry.type !== "service") || typeof entry.digest !== "string" || !/^[a-f0-9]{64}$/.test(entry.digest))
544
+ throw new TeamControlPlaneError("Token is invalid.");
545
+ if (entry.type === "user" && !members.some((member) => member.organizationId === organizationId && member.id === subjectId))
546
+ throw new TeamControlPlaneError("Token is invalid.");
547
+ const serviceAccount = entry.type === "service" ? serviceAccounts.find((account) => account.organizationId === organizationId && account.id === subjectId) : undefined;
548
+ if (entry.type === "service" && serviceAccount === undefined)
549
+ throw new TeamControlPlaneError("Token is invalid.");
550
+ const scope = entry.type === "service" ? validateTokenScope(entry.scope, { projects, environments }, organizationId) : undefined;
551
+ if (entry.type === "user" && entry.scope !== undefined)
552
+ throw new TeamControlPlaneError("Token is invalid.");
553
+ if (!actorReferenceExists(organizationId, entry.issuedBy, members, serviceAccounts))
554
+ throw new TeamControlPlaneError("Token is invalid.");
555
+ const issuedAt = timestamp(entry.issuedAt, "Token issuedAt");
556
+ const expiresAt = timestamp(entry.expiresAt, "Token expiresAt");
557
+ if (new Date(expiresAt).getTime() > new Date(issuedAt).getTime() + 90 * 24 * 60 * 60 * 1000)
558
+ throw new TeamControlPlaneError("Token is invalid.");
559
+ const revokedAt = entry.revokedAt === undefined ? undefined : timestamp(entry.revokedAt, "Token revokedAt");
560
+ if (entry.type === "service" && serviceAccount?.disabledAt !== undefined && revokedAt === undefined)
561
+ throw new TeamControlPlaneError("Token is invalid.");
562
+ return { id: identifier(entry.id, "Token id"), organizationId, type: entry.type, subjectId, digest: entry.digest, issuedBy: actorReference(entry.issuedBy, "Token issuer id"), issuedAt, expiresAt, ...(scope === undefined ? {} : { scope }), ...(revokedAt === undefined ? {} : { revokedAt }) };
563
+ }
564
+ function scenario(value, organizations, projects, environments, actors) {
565
+ const entry = object(value, "Scenario is invalid.");
566
+ exactKeys(entry, ["organizationId", "projectId", "environmentId", "scenarioId", "version", "title", "metadata", "createdBy", "createdAt"], "Scenario");
567
+ const organizationId = identifier(entry.organizationId, "Scenario organization id");
568
+ const projectId = identifier(entry.projectId, "Scenario project id");
569
+ const environmentId = identifier(entry.environmentId, "Scenario environment id");
570
+ if (!organizations.has(organizationId) || !projects.has(scopedKey(organizationId, projectId)) || environments.get(scopedKey(organizationId, projectId, environmentId)) === undefined || !actors.has(scopedKey(organizationId, actorReference(entry.createdBy, "Scenario creator id"))))
571
+ throw new TeamControlPlaneError("Scenario is invalid.");
572
+ return { organizationId, projectId, environmentId, scenarioId: identifier(entry.scenarioId, "Scenario id"), version: positiveInteger(entry.version, "Scenario version"), title: text(entry.title, "Scenario title", 160), metadata: safeJson(entry.metadata, "Scenario metadata", 16 * 1024), createdBy: actorReference(entry.createdBy, "Scenario creator id"), createdAt: timestamp(entry.createdAt, "Scenario createdAt") };
573
+ }
574
+ function evidenceRecord(value, organizations, projects, environments, actors) {
575
+ const entry = object(value, "Evidence is invalid.");
576
+ exactKeys(entry, ["id", "organizationId", "projectId", "environmentId", "runId", "evidenceHash", "status", "passed", "failCount", "warningCount", "providers", "scenarios", "enforcement", "uploadedBy", "uploadedAt", "expiresAt"], "Evidence");
577
+ const organizationId = identifier(entry.organizationId, "Evidence organization id");
578
+ const projectId = identifier(entry.projectId, "Evidence project id");
579
+ const environmentId = identifier(entry.environmentId, "Evidence environment id");
580
+ const enforcement = object(entry.enforcement, "Evidence enforcement is invalid.");
581
+ exactKeys(enforcement, ["mode", "isolated", "degraded"], "Evidence enforcement");
582
+ if (!organizations.has(organizationId) || !projects.has(scopedKey(organizationId, projectId)) || environments.get(scopedKey(organizationId, projectId, environmentId)) === undefined || !actors.has(scopedKey(organizationId, actorReference(entry.uploadedBy, "Evidence uploader id"))) || typeof entry.evidenceHash !== "string" || !/^[a-f0-9]{64}$/.test(entry.evidenceHash) || !runStatus(entry.status) || typeof entry.passed !== "boolean" || !nonNegativeInteger(entry.failCount) || !nonNegativeInteger(entry.warningCount) || !evidenceMode(enforcement.mode) || typeof enforcement.isolated !== "boolean" || typeof enforcement.degraded !== "boolean")
583
+ throw new TeamControlPlaneError("Evidence is invalid.");
584
+ return { id: identifier(entry.id, "Evidence id"), organizationId, projectId, environmentId, runId: runId(entry.runId), evidenceHash: entry.evidenceHash, status: entry.status, passed: entry.passed, failCount: entry.failCount, warningCount: entry.warningCount, providers: list(entry.providers, "Evidence providers"), scenarios: list(entry.scenarios, "Evidence scenarios", true), enforcement: { mode: enforcement.mode, isolated: enforcement.isolated, degraded: enforcement.degraded }, uploadedBy: actorReference(entry.uploadedBy, "Evidence uploader id"), uploadedAt: timestamp(entry.uploadedAt, "Evidence uploadedAt"), expiresAt: timestamp(entry.expiresAt, "Evidence expiresAt") };
585
+ }
586
+ function policyVersion(value, organizations, actors) {
587
+ const entry = object(value, "Policy is invalid.");
588
+ exactKeys(entry, ["organizationId", "version", "policy", "hash", "distributedBy", "distributedAt"], "Policy");
589
+ const organizationId = identifier(entry.organizationId, "Policy organization id");
590
+ const policy = safeJson(entry.policy, "Team policy", 32 * 1024);
591
+ validatePolicy(policy);
592
+ if (!organizations.has(organizationId) || !actors.has(scopedKey(organizationId, actorReference(entry.distributedBy, "Policy distributor id"))) || typeof entry.hash !== "string" || entry.hash !== stableHash(policy))
593
+ throw new TeamControlPlaneError("Policy is invalid.");
594
+ return { organizationId, version: positiveInteger(entry.version, "Policy version"), policy, hash: entry.hash, distributedBy: actorReference(entry.distributedBy, "Policy distributor id"), distributedAt: timestamp(entry.distributedAt, "Policy distributedAt") };
595
+ }
596
+ function auditAnchor(value, organizations) {
597
+ const entry = object(value, "Audit anchor is invalid.");
598
+ exactKeys(entry, ["organizationId", "sequence", "hash"], "Audit anchor");
599
+ const organizationId = identifier(entry.organizationId, "Audit anchor organization id");
600
+ if (!organizations.has(organizationId) || !nonNegativeInteger(entry.sequence) || typeof entry.hash !== "string" || !/^[a-f0-9]{64}$/.test(entry.hash))
601
+ throw new TeamControlPlaneError("Audit anchor is invalid.");
602
+ return { organizationId, sequence: entry.sequence, hash: entry.hash };
603
+ }
604
+ function auditRecord(value, organizations, members, serviceAccounts) {
605
+ const entry = object(value, "Audit record is invalid.");
606
+ exactKeys(entry, ["organizationId", "sequence", "actorId", "action", "resource", "timestamp", "previousHash", "recordHash"], "Audit record");
607
+ const organizationId = identifier(entry.organizationId, "Audit organization id");
608
+ const actorIdValue = actorReference(entry.actorId, "Audit actor id");
609
+ const sequence = positiveInteger(entry.sequence, "Audit sequence");
610
+ if (!organizations.has(organizationId) || ((members.length > 0 || serviceAccounts.length > 0) && !actorReferenceExists(organizationId, actorIdValue, members, serviceAccounts)) || typeof entry.previousHash !== "string" || !/^[a-f0-9]{64}$/.test(entry.previousHash) || typeof entry.recordHash !== "string" || !/^[a-f0-9]{64}$/.test(entry.recordHash))
611
+ throw new TeamControlPlaneError("Audit record is invalid.");
612
+ return { organizationId, sequence, actorId: actorIdValue, action: text(entry.action, "Audit action", 80), resource: text(entry.resource, "Audit resource", 160), timestamp: timestamp(entry.timestamp, "Audit timestamp"), previousHash: entry.previousHash, recordHash: entry.recordHash };
613
+ }
614
+ function legacyAuditRecord(value, organizations, members) {
615
+ const entry = object(value, "Audit record is invalid.");
616
+ exactKeys(entry, ["organizationId", "actorId", "action", "resource", "timestamp"], "Audit record");
617
+ const organizationId = identifier(entry.organizationId, "Audit organization id");
618
+ const actorIdValue = identifier(entry.actorId, "Audit actor id");
619
+ if (!organizations.has(organizationId) || !members.some((member) => member.organizationId === organizationId && member.id === actorIdValue))
620
+ throw new TeamControlPlaneError("Audit record is invalid.");
621
+ return { organizationId, actorId: actorIdValue, action: text(entry.action, "Audit action", 80), resource: text(entry.resource, "Audit resource", 160), timestamp: timestamp(entry.timestamp, "Audit timestamp") };
622
+ }
623
+ function requirePermission(state, actor, permission) {
624
+ const role = actorRole(state, actor);
625
+ if (!TEAM_PERMISSION_MATRIX[role].includes(permission))
626
+ throw new TeamControlPlaneError("Access denied.");
627
+ }
628
+ function requireScopedPermission(state, actor, permission, projectId, environmentId) {
629
+ requirePermission(state, actor, permission);
630
+ if (isServiceActor(actor) && !serviceTokenScopes(state, actor).some((scope) => scope.projectId === projectId && scope.environmentId === environmentId && scope.permissions.includes(permission)))
631
+ throw new TeamControlPlaneError("Access denied.");
632
+ }
633
+ const MEMBER_TOKEN_MANAGEMENT_MATRIX = Object.freeze({
634
+ owner: ["owner", "admin", "developer", "viewer"],
635
+ admin: ["admin", "developer", "viewer"],
636
+ developer: [],
637
+ viewer: []
638
+ });
639
+ function canManageMemberToken(actor, subject) {
640
+ return actor !== "service_account" && MEMBER_TOKEN_MANAGEMENT_MATRIX[actor].includes(subject);
641
+ }
642
+ function actorRole(state, actor) {
643
+ const organizationId = identifier(actor.organizationId, "Organization id");
644
+ if (isServiceActor(actor)) {
645
+ if (!authenticatedServiceActors.has(actor))
646
+ throw new TeamControlPlaneError("Access denied.");
647
+ const serviceAccountId = identifier(actor.serviceAccountId, "Service account id");
648
+ const account = state.serviceAccounts.find((candidate) => candidate.organizationId === organizationId && candidate.id === serviceAccountId && candidate.disabledAt === undefined);
649
+ if (account === undefined)
650
+ throw new TeamControlPlaneError("Access denied.");
651
+ serviceTokenScopes(state, actor);
652
+ return "service_account";
653
+ }
654
+ const memberId = identifier(actor.memberId, "Member id");
655
+ const member = state.members.find((candidate) => candidate.organizationId === organizationId && candidate.id === memberId);
656
+ if (member === undefined)
657
+ throw new TeamControlPlaneError("Access denied.");
658
+ return member.role;
659
+ }
660
+ function requireMember(state, actor) {
661
+ const organizationId = identifier(actor.organizationId, "Organization id");
662
+ const memberId = identifier(actor.memberId, "Member id");
663
+ const member = state.members.find((candidate) => candidate.organizationId === organizationId && candidate.id === memberId);
664
+ if (member === undefined)
665
+ throw new TeamControlPlaneError("Access denied.");
666
+ return member;
667
+ }
668
+ function requireServiceAccount(state, organizationId, serviceAccountId) {
669
+ const account = state.serviceAccounts.find((candidate) => candidate.organizationId === organizationId && candidate.id === serviceAccountId && candidate.disabledAt === undefined);
670
+ if (account === undefined)
671
+ throw new TeamControlPlaneError("Resource not found.");
672
+ return account;
673
+ }
674
+ function requireProject(state, organizationId, projectId) {
675
+ if (!state.projects.some((candidate) => candidate.organizationId === organizationId && candidate.id === projectId))
676
+ throw new TeamControlPlaneError("Resource not found.");
677
+ }
678
+ function requireEnvironment(state, organizationId, projectId, environmentId) {
679
+ if (!state.environments.some((candidate) => candidate.organizationId === organizationId && candidate.projectId === projectId && candidate.id === environmentId))
680
+ throw new TeamControlPlaneError("Resource not found.");
681
+ }
682
+ function issueTokenRecord(state, input, issuedAt, random) {
683
+ for (let attempts = 0; attempts < 8; attempts += 1) {
684
+ const tokenId = `tok_${random(8).toString("hex")}`;
685
+ const token = `${TOKEN_PREFIX}${random(32).toString("base64url")}`;
686
+ const digest = hash(token);
687
+ if (state.tokens.some((candidate) => candidate.id === tokenId || equalHash(candidate.digest, digest)))
688
+ continue;
689
+ state.tokens.push({ id: tokenId, organizationId: input.organizationId, type: input.type, subjectId: input.subjectId, digest, issuedBy: input.issuedBy, issuedAt, expiresAt: input.expiresAt, ...(input.scope === undefined ? {} : { scope: structuredClone(input.scope) }) });
690
+ return { tokenId, token, expiresAt: input.expiresAt };
691
+ }
692
+ throw new TeamControlPlaneError("Unable to allocate a unique token.");
693
+ }
694
+ function appendAudit(state, organizationId, actorIdValue, action, resource, timestampValue) {
695
+ const anchor = state.auditAnchors.find((candidate) => candidate.organizationId === organizationId);
696
+ if (anchor === undefined)
697
+ throw new TeamControlPlaneError("Audit anchor is missing.");
698
+ const records = state.audit.filter((record) => record.organizationId === organizationId);
699
+ const previous = records.at(-1);
700
+ const sequence = previous === undefined ? anchor.sequence + 1 : previous.sequence + 1;
701
+ const previousHash = previous === undefined ? anchor.hash : previous.recordHash;
702
+ const recordWithoutHash = { organizationId, sequence, actorId: actorIdValue, action, resource, timestamp: timestampValue, previousHash };
703
+ state.audit.push({ ...recordWithoutHash, recordHash: stableHash(recordWithoutHash) });
704
+ }
705
+ function verifyAuditChain(organizationId, anchor, records) {
706
+ try {
707
+ if (anchor.organizationId !== organizationId)
708
+ return { valid: false, reason: "Audit anchor organization does not match." };
709
+ let expectedSequence = anchor.sequence + 1;
710
+ let previousHash = anchor.hash;
711
+ for (const record of records) {
712
+ if (record.organizationId !== organizationId || record.sequence !== expectedSequence || record.previousHash !== previousHash)
713
+ return { valid: false, reason: "Audit sequence or previous hash is invalid." };
714
+ const expectedHash = stableHash({ organizationId: record.organizationId, sequence: record.sequence, actorId: record.actorId, action: record.action, resource: record.resource, timestamp: record.timestamp, previousHash: record.previousHash });
715
+ if (!equalHash(record.recordHash, expectedHash))
716
+ return { valid: false, reason: "Audit record hash is invalid." };
717
+ expectedSequence += 1;
718
+ previousHash = record.recordHash;
719
+ }
720
+ return { valid: true };
721
+ }
722
+ catch {
723
+ return { valid: false, reason: "Audit chain is invalid." };
724
+ }
725
+ }
726
+ function retain(state, now) {
727
+ state.evidence = state.evidence.filter((entry) => entry.expiresAt > now);
728
+ for (const organization of state.organizations) {
729
+ state.evidence = newest(state.evidence, organization.id, MAX_EVIDENCE_PER_ORGANIZATION, (entry) => entry.uploadedAt);
730
+ pruneAudit(state, organization.id, now);
731
+ }
732
+ }
733
+ function pruneAudit(state, organizationId, now) {
734
+ const cutoff = addDays(now, -AUDIT_RETENTION_DAYS);
735
+ const all = state.audit.filter((entry) => entry.organizationId === organizationId).sort((left, right) => left.sequence - right.sequence);
736
+ const retained = all.filter((entry) => entry.timestamp >= cutoff).slice(-MAX_AUDIT_PER_ORGANIZATION);
737
+ if (retained.length === all.length)
738
+ return;
739
+ const removed = all.slice(0, all.length - retained.length);
740
+ const anchor = state.auditAnchors.find((entry) => entry.organizationId === organizationId);
741
+ if (anchor === undefined)
742
+ throw new TeamControlPlaneError("Audit anchor is missing.");
743
+ const boundary = removed.at(-1);
744
+ if (boundary !== undefined) {
745
+ anchor.sequence = boundary.sequence;
746
+ anchor.hash = boundary.recordHash;
747
+ }
748
+ state.audit = state.audit.filter((entry) => entry.organizationId !== organizationId || retained.includes(entry));
749
+ }
750
+ function newest(entries, organizationId, max, key) {
751
+ const allowed = new Set(entries.filter((entry) => entry.organizationId === organizationId).sort((left, right) => key(right).localeCompare(key(left))).slice(0, max));
752
+ return entries.filter((entry) => entry.organizationId !== organizationId || allowed.has(entry));
753
+ }
754
+ function validateTokenScope(value, state, organizationId) {
755
+ const scopes = array(value, "Service token scope", MAX_SERVICE_TOKEN_SCOPES).map((raw) => {
756
+ const entry = object(raw, "Service token scope is invalid.");
757
+ exactKeys(entry, ["projectId", "environmentId", "permissions"], "Service token scope");
758
+ const projectId = identifier(entry.projectId, "Service token scope project id");
759
+ const environmentId = identifier(entry.environmentId, "Service token scope environment id");
760
+ if (!state.projects.some((project) => project.organizationId === organizationId && project.id === projectId) || !state.environments.some((environment) => environment.organizationId === organizationId && environment.projectId === projectId && environment.id === environmentId))
761
+ throw new TeamControlPlaneError("Service token scope is invalid.");
762
+ const permissions = array(entry.permissions, "Service token permissions", SERVICE_ACCOUNT_PERMISSIONS.length).map((permission) => {
763
+ if (typeof permission !== "string" || !isScopedPermission(permission) || !SERVICE_ACCOUNT_PERMISSIONS.includes(permission))
764
+ throw new TeamControlPlaneError("Service token permissions are invalid.");
765
+ return permission;
766
+ });
767
+ if (permissions.length === 0)
768
+ throw new TeamControlPlaneError("Service token permissions are invalid.");
769
+ unique(permissions, "Service token permissions");
770
+ return { projectId, environmentId, permissions: permissions.sort() };
771
+ });
772
+ if (scopes.length === 0)
773
+ throw new TeamControlPlaneError("Service token scope is invalid.");
774
+ unique(scopes.map((scope) => scopedKey(scope.projectId, scope.environmentId)), "Service token scope");
775
+ return scopes.sort((left, right) => scopedKey(left.projectId, left.environmentId).localeCompare(scopedKey(right.projectId, right.environmentId)));
776
+ }
777
+ function canReadEvidence(state, actor, evidence) {
778
+ return !isServiceActor(actor) || serviceTokenScopes(state, actor).some((scope) => scope.projectId === evidence.projectId && scope.environmentId === evidence.environmentId && scope.permissions.includes("evidence.read"));
779
+ }
780
+ function isServiceActor(actor) {
781
+ return actor.actorType === "service_account";
782
+ }
783
+ function serviceTokenScopes(state, actor) {
784
+ const clock = authenticatedServiceActors.get(actor);
785
+ if (clock === undefined)
786
+ throw new TeamControlPlaneError("Access denied.");
787
+ const tokenId = identifier(actor.tokenId, "Token id");
788
+ const token = state.tokens.find((candidate) => candidate.id === tokenId && candidate.organizationId === actor.organizationId && candidate.type === "service" && candidate.subjectId === actor.serviceAccountId);
789
+ if (token === undefined || token.revokedAt !== undefined || new Date(token.expiresAt).getTime() <= clock().getTime())
790
+ throw new TeamControlPlaneError("Access denied.");
791
+ return validateTokenScope(token.scope, state, actor.organizationId);
792
+ }
793
+ function actorId(actor) {
794
+ return isServiceActor(actor) ? `service:${identifier(actor.serviceAccountId, "Service account id")}` : identifier(actor.memberId, "Member id");
795
+ }
796
+ function userActorId(actor) {
797
+ if (isServiceActor(actor))
798
+ throw new TeamControlPlaneError("Service accounts cannot impersonate a human member.");
799
+ return identifier(actor.memberId, "Member id");
800
+ }
801
+ function actorReference(value, label) {
802
+ if (typeof value !== "string")
803
+ throw new TeamControlPlaneError(`${label} is invalid.`);
804
+ if (value.startsWith("service:"))
805
+ return `service:${identifier(value.slice("service:".length), label)}`;
806
+ return identifier(value, label);
807
+ }
808
+ function actorReferenceIds(members, serviceAccounts) {
809
+ return new Set([...members.map((member) => scopedKey(member.organizationId, member.id)), ...serviceAccounts.map((account) => scopedKey(account.organizationId, `service:${account.id}`))]);
810
+ }
811
+ function actorReferenceExists(organizationId, actorIdValue, members, serviceAccounts) {
812
+ const id = actorReference(actorIdValue, "Actor id");
813
+ return members.some((member) => member.organizationId === organizationId && member.id === id) || (id.startsWith("service:") && serviceAccounts.some((account) => account.organizationId === organizationId && account.id === id.slice("service:".length)));
814
+ }
815
+ function rejectRawEvidence(report) {
816
+ const encoded = JSON.stringify(report);
817
+ if (Buffer.byteLength(encoded, "utf8") > 512 * 1024)
818
+ throw new TeamControlPlaneError("Evidence artifact exceeds 524288 bytes.");
819
+ const prohibited = new Set(["authorization", "cookie", "set-cookie", "rawbody"]);
820
+ const visit = (value, key) => {
821
+ if (key !== undefined && prohibited.has(key.toLowerCase()))
822
+ throw new TeamControlPlaneError("Evidence artifact contains an unsafe raw field.");
823
+ if (typeof value === "string" && sanitizeSecretString(value) !== value)
824
+ throw new TeamControlPlaneError("Evidence artifact contains a secret-shaped value.");
825
+ if (Array.isArray(value))
826
+ for (const item of value)
827
+ visit(item, key);
828
+ if (value !== null && typeof value === "object" && !Array.isArray(value))
829
+ for (const [childKey, child] of Object.entries(value))
830
+ visit(child, childKey);
831
+ };
832
+ visit(report);
833
+ }
834
+ function safeJson(value, label, maxBytes) {
835
+ const encoded = JSON.stringify(value);
836
+ if (encoded === undefined || Buffer.byteLength(encoded, "utf8") > maxBytes)
837
+ throw new TeamControlPlaneError(`${label} exceeds ${maxBytes} bytes.`);
838
+ inspectJson(value, label, 0);
839
+ return JSON.parse(encoded);
840
+ }
841
+ function inspectJson(value, label, depth, key) {
842
+ if (depth > 8)
843
+ throw new TeamControlPlaneError(`${label} is too deeply nested.`);
844
+ if (key !== undefined && isSecretFieldName(key))
845
+ throw new TeamControlPlaneError(`${label} contains a secret-shaped field.`);
846
+ if (typeof value === "string") {
847
+ if (value.length > 2_000 || sanitizeSecretString(value) !== value)
848
+ throw new TeamControlPlaneError(`${label} contains an unsafe string value.`);
849
+ return;
850
+ }
851
+ if (value === null || typeof value === "boolean" || typeof value === "number")
852
+ return;
853
+ if (Array.isArray(value)) {
854
+ if (value.length > 100)
855
+ throw new TeamControlPlaneError(`${label} has too many entries.`);
856
+ for (const entry of value)
857
+ inspectJson(entry, label, depth + 1, key);
858
+ return;
859
+ }
860
+ if (!plainObject(value))
861
+ throw new TeamControlPlaneError(`${label} must contain JSON values only.`);
862
+ const entries = Object.entries(value);
863
+ if (entries.length > 100)
864
+ throw new TeamControlPlaneError(`${label} has too many fields.`);
865
+ for (const [childKey, child] of entries)
866
+ inspectJson(child, label, depth + 1, childKey);
867
+ }
868
+ function validatePolicy(policy) {
869
+ const candidate = policy;
870
+ if (candidate.version !== 1 || !plainObject(candidate.network) || !plainObject(candidate.credentials) || !Array.isArray(candidate.requiredScenarios) || !plainObject(candidate.enforcement) || !plainObject(candidate.reports))
871
+ throw new TeamControlPlaneError("Team policy does not match GhostAPI policy schema v1.");
872
+ }
873
+ function hash(value) { return createHash("sha256").update(value).digest("hex"); }
874
+ function stableHash(value) { return createHash("sha256").update(stableStringify(value)).digest("hex"); }
875
+ function auditGenesisHash(organizationId) { return hash(`ghostapi-team-audit-v3:${organizationId}`); }
876
+ function equalHash(left, right) { return /^[a-f0-9]{64}$/.test(left) && /^[a-f0-9]{64}$/.test(right) && timingSafeEqual(Buffer.from(left, "hex"), Buffer.from(right, "hex")); }
877
+ function stableStringify(value) {
878
+ if (value === null || typeof value !== "object")
879
+ return JSON.stringify(value);
880
+ if (Array.isArray(value))
881
+ return `[${value.map(stableStringify).join(",")}]`;
882
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
883
+ }
884
+ function scopedKey(...parts) { return parts.join("\u0000"); }
885
+ function identifier(value, label) { if (typeof value !== "string" || !IDENTIFIER.test(value))
886
+ throw new TeamControlPlaneError(`${label} must be 1-64 lowercase letters, numbers, underscores, or hyphens.`); return value; }
887
+ function runId(value) { if (typeof value !== "string" || value.length === 0 || value.length > 128 || /[\u0000-\u001f]/.test(value) || sanitizeSecretString(value) !== value)
888
+ throw new TeamControlPlaneError("Evidence run id is invalid."); return value; }
889
+ function text(value, label, max) { if (typeof value !== "string" || value.trim() === "" || value.length > max || /[\u0000-\u001f]/.test(value) || sanitizeSecretString(value) !== value)
890
+ throw new TeamControlPlaneError(`${label} is invalid.`); return value.trim(); }
891
+ function timestamp(value, label) { if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) || Number.isNaN(new Date(value).getTime()))
892
+ throw new TeamControlPlaneError(`${label} is invalid.`); return value; }
893
+ function futureTimestamp(value, now) { const result = timestamp(value, "Token expiry"); const milliseconds = new Date(result).getTime(); if (milliseconds <= now.getTime() || milliseconds > now.getTime() + 90 * 24 * 60 * 60 * 1000)
894
+ throw new TeamControlPlaneError("Token expiry must be within the next 90 days."); return result; }
895
+ function positiveInteger(value, label) { if (!nonNegativeInteger(value) || value < 1 || value > 100_000)
896
+ throw new TeamControlPlaneError(`${label} is invalid.`); return value; }
897
+ function nonNegativeInteger(value) { return typeof value === "number" && Number.isInteger(value) && value >= 0; }
898
+ function list(value, label, dotted = false) { if (!Array.isArray(value) || value.length > 100)
899
+ throw new TeamControlPlaneError(`${label} are invalid.`); const result = value.map((entry) => { if (typeof entry !== "string" || (dotted ? !/^[a-z0-9][a-z0-9._-]{0,127}$/.test(entry) : !IDENTIFIER.test(entry)))
900
+ throw new TeamControlPlaneError(`${label} are invalid.`); return entry; }); unique(result, label); return result.sort(); }
901
+ function object(value, message) { if (!plainObject(value))
902
+ throw new TeamControlPlaneError(message); return value; }
903
+ function plainObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype; }
904
+ function array(value, label, max) { if (!Array.isArray(value) || value.length > max)
905
+ throw new TeamControlPlaneError(`${label} are invalid.`); return value; }
906
+ function exactKeys(value, allowed, label, optional = []) { for (const key of Object.keys(value))
907
+ if (!allowed.includes(key))
908
+ throw new TeamControlPlaneError(`${label} contains unsupported field: ${key}.`); for (const key of allowed)
909
+ if (!optional.includes(key) && !(key in value))
910
+ throw new TeamControlPlaneError(`${label} is missing field: ${key}.`); }
911
+ function unique(values, label) { if (new Set(values).size !== values.length)
912
+ throw new TeamControlPlaneError(`${label} must be unique.`); }
913
+ function runStatus(value) { return value === "unknown" || value === "preparing" || value === "running" || value === "failed-to-start" || value === "finished"; }
914
+ function evidenceMode(value) { return value === "linux-network-namespace" || value === "proxy-guidance" || value === "unknown"; }
915
+ function isScopedPermission(value) { return READ_PERMISSIONS.includes(value) || PUBLISH_PERMISSIONS.includes(value); }
916
+ function addDays(value, days) { return new Date(new Date(value).getTime() + days * 24 * 60 * 60 * 1000).toISOString(); }
917
+ function isErrorCode(error, code) { return error instanceof Error && "code" in error && error.code === code; }
918
+ //# sourceMappingURL=controlPlane.js.map