@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
@@ -2,9 +2,9 @@ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
2
  import { lstat, readFile } from "node:fs/promises";
3
3
  import { dirname } from "node:path";
4
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";
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
8
  const SCHEMA_VERSION = 3;
9
9
  const MAX_STORE_BYTES = 1024 * 1024;
10
10
  const TOKEN_PREFIX = "gapi_team_";
@@ -15,19 +15,46 @@ const EVIDENCE_RETENTION_DAYS = 30;
15
15
  const AUDIT_RETENTION_DAYS = 90;
16
16
  const MAX_SERVICE_TOKEN_SCOPES = 20;
17
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];
18
+ const READ_PERMISSIONS = [
19
+ "project.read",
20
+ "environment.read",
21
+ "scenario.read",
22
+ "evidence.read",
23
+ "policy.read",
24
+ ];
25
+ const PUBLISH_PERMISSIONS = [
26
+ "scenario.publish",
27
+ "evidence.upload",
28
+ ];
29
+ const OWNER_PERMISSIONS = [
30
+ "member.manage",
31
+ "project.manage",
32
+ "environment.manage",
33
+ "policy.manage",
34
+ "token.manage",
35
+ "service_account.manage",
36
+ "audit.read",
37
+ "audit.export",
38
+ "data.delete",
39
+ "retention.manage",
40
+ ...READ_PERMISSIONS,
41
+ ...PUBLISH_PERMISSIONS,
42
+ ];
21
43
  const ADMIN_PERMISSIONS = OWNER_PERMISSIONS.filter((permission) => permission !== "member.manage");
22
- const DEVELOPER_PERMISSIONS = [...READ_PERMISSIONS, ...PUBLISH_PERMISSIONS];
44
+ const DEVELOPER_PERMISSIONS = [
45
+ ...READ_PERMISSIONS,
46
+ ...PUBLISH_PERMISSIONS,
47
+ ];
23
48
  const VIEWER_PERMISSIONS = [...READ_PERMISSIONS];
24
- const SERVICE_ACCOUNT_PERMISSIONS = [...DEVELOPER_PERMISSIONS.filter((permission) => permission !== "policy.read")];
49
+ const SERVICE_ACCOUNT_PERMISSIONS = [
50
+ ...DEVELOPER_PERMISSIONS.filter((permission) => permission !== "policy.read"),
51
+ ];
25
52
  export const TEAM_PERMISSION_MATRIX = Object.freeze({
26
53
  owner: Object.freeze([...OWNER_PERMISSIONS]),
27
54
  admin: Object.freeze([...ADMIN_PERMISSIONS]),
28
55
  developer: Object.freeze([...DEVELOPER_PERMISSIONS]),
29
56
  viewer: Object.freeze([...VIEWER_PERMISSIONS]),
30
- service_account: Object.freeze([...SERVICE_ACCOUNT_PERMISSIONS])
57
+ service_account: Object.freeze([...SERVICE_ACCOUNT_PERMISSIONS]),
31
58
  });
32
59
  export class TeamControlPlaneError extends Error {
33
60
  constructor(message) {
@@ -56,21 +83,38 @@ export class LocalTeamControlPlane {
56
83
  throw new TeamControlPlaneError("Organization already exists.");
57
84
  const organization = { id: organizationId, name, createdAt: now };
58
85
  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) });
86
+ state.members.push({
87
+ id: ownerId,
88
+ organizationId,
89
+ role: "owner",
90
+ createdAt: now,
91
+ });
92
+ state.auditAnchors.push({
93
+ organizationId,
94
+ sequence: 0,
95
+ hash: auditGenesisHash(organizationId),
96
+ });
61
97
  appendAudit(state, organizationId, ownerId, "organization.bootstrap", organizationId, now);
62
98
  return organization;
63
99
  });
64
100
  }
65
101
  async addMember(actor, input) {
66
102
  const memberId = identifier(input.memberId, "Member id");
67
- if (input.role !== "admin" && input.role !== "developer" && input.role !== "viewer")
103
+ if (input.role !== "admin" &&
104
+ input.role !== "developer" &&
105
+ input.role !== "viewer")
68
106
  throw new TeamControlPlaneError("Member role is invalid.");
69
107
  return this.mutate((state, now) => {
70
108
  requirePermission(state, actor, "member.manage");
71
- if (state.members.some((member) => member.organizationId === actor.organizationId && member.id === memberId))
109
+ if (state.members.some((member) => member.organizationId === actor.organizationId &&
110
+ member.id === memberId))
72
111
  throw new TeamControlPlaneError("Member already exists.");
73
- const member = { id: memberId, organizationId: actor.organizationId, role: input.role, createdAt: now };
112
+ const member = {
113
+ id: memberId,
114
+ organizationId: actor.organizationId,
115
+ role: input.role,
116
+ createdAt: now,
117
+ };
74
118
  state.members.push(member);
75
119
  appendAudit(state, actor.organizationId, actorId(actor), "member.add", memberId, now);
76
120
  return member;
@@ -81,9 +125,16 @@ export class LocalTeamControlPlane {
81
125
  const name = text(input.name, "Service account name", 120);
82
126
  return this.mutate((state, now) => {
83
127
  requirePermission(state, actor, "service_account.manage");
84
- if (state.serviceAccounts.some((account) => account.organizationId === actor.organizationId && account.id === serviceAccountId))
128
+ if (state.serviceAccounts.some((account) => account.organizationId === actor.organizationId &&
129
+ account.id === serviceAccountId))
85
130
  throw new TeamControlPlaneError("Service account already exists.");
86
- const account = { id: serviceAccountId, organizationId: actor.organizationId, name, createdBy: actorId(actor), createdAt: now };
131
+ const account = {
132
+ id: serviceAccountId,
133
+ organizationId: actor.organizationId,
134
+ name,
135
+ createdBy: actorId(actor),
136
+ createdAt: now,
137
+ };
87
138
  state.serviceAccounts.push(account);
88
139
  appendAudit(state, actor.organizationId, actorId(actor), "service_account.create", serviceAccountId, now);
89
140
  return account;
@@ -93,13 +144,17 @@ export class LocalTeamControlPlane {
93
144
  const id = identifier(serviceAccountId, "Service account id");
94
145
  await this.mutate((state, now) => {
95
146
  requirePermission(state, actor, "service_account.manage");
96
- const account = state.serviceAccounts.find((candidate) => candidate.organizationId === actor.organizationId && candidate.id === id);
147
+ const account = state.serviceAccounts.find((candidate) => candidate.organizationId === actor.organizationId &&
148
+ candidate.id === id);
97
149
  if (account === undefined)
98
150
  throw new TeamControlPlaneError("Resource not found.");
99
151
  if (account.disabledAt === undefined) {
100
152
  account.disabledAt = now;
101
153
  for (const token of state.tokens) {
102
- if (token.organizationId === actor.organizationId && token.type === "service" && token.subjectId === id && token.revokedAt === undefined)
154
+ if (token.organizationId === actor.organizationId &&
155
+ token.type === "service" &&
156
+ token.subjectId === id &&
157
+ token.revokedAt === undefined)
103
158
  token.revokedAt = now;
104
159
  }
105
160
  appendAudit(state, actor.organizationId, actorId(actor), "service_account.disable", id, now);
@@ -111,9 +166,15 @@ export class LocalTeamControlPlane {
111
166
  const name = text(input.name, "Project name", 120);
112
167
  return this.mutate((state, now) => {
113
168
  requirePermission(state, actor, "project.manage");
114
- if (state.projects.some((project) => project.organizationId === actor.organizationId && project.id === projectId))
169
+ if (state.projects.some((project) => project.organizationId === actor.organizationId &&
170
+ project.id === projectId))
115
171
  throw new TeamControlPlaneError("Project already exists.");
116
- const project = { id: projectId, organizationId: actor.organizationId, name, createdAt: now };
172
+ const project = {
173
+ id: projectId,
174
+ organizationId: actor.organizationId,
175
+ name,
176
+ createdAt: now,
177
+ };
117
178
  state.projects.push(project);
118
179
  appendAudit(state, actor.organizationId, actorId(actor), "project.register", projectId, now);
119
180
  return project;
@@ -128,9 +189,18 @@ export class LocalTeamControlPlane {
128
189
  return this.mutate((state, now) => {
129
190
  requirePermission(state, actor, "environment.manage");
130
191
  requireProject(state, actor.organizationId, projectId);
131
- if (state.environments.some((environment) => environment.organizationId === actor.organizationId && environment.projectId === projectId && environment.id === environmentId))
192
+ if (state.environments.some((environment) => environment.organizationId === actor.organizationId &&
193
+ environment.projectId === projectId &&
194
+ environment.id === environmentId))
132
195
  throw new TeamControlPlaneError("Environment already exists.");
133
- const environment = { id: environmentId, organizationId: actor.organizationId, projectId, name, kind: input.kind, createdAt: now };
196
+ const environment = {
197
+ id: environmentId,
198
+ organizationId: actor.organizationId,
199
+ projectId,
200
+ name,
201
+ kind: input.kind,
202
+ createdAt: now,
203
+ };
134
204
  state.environments.push(environment);
135
205
  appendAudit(state, actor.organizationId, actorId(actor), "environment.create", environmentId, now);
136
206
  return environment;
@@ -140,11 +210,22 @@ export class LocalTeamControlPlane {
140
210
  const expiresAt = futureTimestamp(input.expiresAt, this.now());
141
211
  return this.mutate((state, now) => {
142
212
  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 });
213
+ const memberId = input.memberId === undefined
214
+ ? userActorId(actor)
215
+ : identifier(input.memberId, "Token member id");
216
+ const subject = requireMember(state, {
217
+ organizationId: actor.organizationId,
218
+ memberId,
219
+ });
145
220
  if (!canManageMemberToken(actorRole(state, actor), subject.role))
146
221
  throw new TeamControlPlaneError("Access denied.");
147
- const issued = issueTokenRecord(state, { organizationId: actor.organizationId, type: "user", subjectId: memberId, issuedBy: actorId(actor), expiresAt }, now, this.randomTokenBytes);
222
+ const issued = issueTokenRecord(state, {
223
+ organizationId: actor.organizationId,
224
+ type: "user",
225
+ subjectId: memberId,
226
+ issuedBy: actorId(actor),
227
+ expiresAt,
228
+ }, now, this.randomTokenBytes);
148
229
  appendAudit(state, actor.organizationId, actorId(actor), "token.issue", issued.tokenId, now);
149
230
  return issued;
150
231
  });
@@ -156,7 +237,14 @@ export class LocalTeamControlPlane {
156
237
  requirePermission(state, actor, "token.manage");
157
238
  requireServiceAccount(state, actor.organizationId, serviceAccountId);
158
239
  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);
240
+ const issued = issueTokenRecord(state, {
241
+ organizationId: actor.organizationId,
242
+ type: "service",
243
+ subjectId: serviceAccountId,
244
+ issuedBy: actorId(actor),
245
+ expiresAt,
246
+ scope,
247
+ }, now, this.randomTokenBytes);
160
248
  appendAudit(state, actor.organizationId, actorId(actor), "token.issue_service", issued.tokenId, now);
161
249
  return issued;
162
250
  });
@@ -165,18 +253,32 @@ export class LocalTeamControlPlane {
165
253
  const id = identifier(tokenId, "Token id");
166
254
  return this.mutate((state, now) => {
167
255
  requirePermission(state, actor, "token.manage");
168
- const oldToken = state.tokens.find((candidate) => candidate.id === id && candidate.organizationId === actor.organizationId);
256
+ const oldToken = state.tokens.find((candidate) => candidate.id === id &&
257
+ candidate.organizationId === actor.organizationId);
169
258
  if (oldToken === undefined)
170
259
  throw new TeamControlPlaneError("Resource not found.");
171
260
  if (oldToken.revokedAt !== undefined)
172
261
  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))
262
+ if (oldToken.type === "user" &&
263
+ !canManageMemberToken(actorRole(state, actor), requireMember(state, {
264
+ organizationId: actor.organizationId,
265
+ memberId: oldToken.subjectId,
266
+ }).role))
174
267
  throw new TeamControlPlaneError("Access denied.");
175
- const expiresAt = input.expiresAt === undefined ? oldToken.expiresAt : futureTimestamp(input.expiresAt, this.now());
268
+ const expiresAt = input.expiresAt === undefined
269
+ ? oldToken.expiresAt
270
+ : futureTimestamp(input.expiresAt, this.now());
176
271
  if (new Date(expiresAt).getTime() <= new Date(now).getTime())
177
272
  throw new TeamControlPlaneError("Token expiry must be within the next 90 days.");
178
273
  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);
274
+ const issued = issueTokenRecord(state, {
275
+ organizationId: oldToken.organizationId,
276
+ type: oldToken.type,
277
+ subjectId: oldToken.subjectId,
278
+ issuedBy: actorId(actor),
279
+ expiresAt,
280
+ ...(oldToken.scope === undefined ? {} : { scope: oldToken.scope }),
281
+ }, now, this.randomTokenBytes);
180
282
  appendAudit(state, actor.organizationId, actorId(actor), "token.rotate", `${id}->${issued.tokenId}`, now);
181
283
  return issued;
182
284
  });
@@ -185,10 +287,15 @@ export class LocalTeamControlPlane {
185
287
  const id = identifier(tokenId, "Token id");
186
288
  await this.mutate((state, now) => {
187
289
  requirePermission(state, actor, "token.manage");
188
- const token = state.tokens.find((candidate) => candidate.id === id && candidate.organizationId === actor.organizationId);
290
+ const token = state.tokens.find((candidate) => candidate.id === id &&
291
+ candidate.organizationId === actor.organizationId);
189
292
  if (token === undefined)
190
293
  throw new TeamControlPlaneError("Resource not found.");
191
- if (token.type === "user" && !canManageMemberToken(actorRole(state, actor), requireMember(state, { organizationId: actor.organizationId, memberId: token.subjectId }).role))
294
+ if (token.type === "user" &&
295
+ !canManageMemberToken(actorRole(state, actor), requireMember(state, {
296
+ organizationId: actor.organizationId,
297
+ memberId: token.subjectId,
298
+ }).role))
192
299
  throw new TeamControlPlaneError("Access denied.");
193
300
  if (token.revokedAt === undefined)
194
301
  token.revokedAt = now;
@@ -201,16 +308,31 @@ export class LocalTeamControlPlane {
201
308
  const state = await this.read();
202
309
  const digest = hash(token);
203
310
  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())
311
+ if (record === undefined ||
312
+ record.revokedAt !== undefined ||
313
+ new Date(record.expiresAt).getTime() <= this.now().getTime())
205
314
  throw new TeamControlPlaneError("Invalid or expired token.");
206
315
  if (record.type === "user") {
207
- requireMember(state, { organizationId: record.organizationId, memberId: record.subjectId });
208
- return { organizationId: record.organizationId, memberId: record.subjectId };
316
+ requireMember(state, {
317
+ organizationId: record.organizationId,
318
+ memberId: record.subjectId,
319
+ });
320
+ return {
321
+ organizationId: record.organizationId,
322
+ memberId: record.subjectId,
323
+ };
209
324
  }
210
- if (!state.serviceAccounts.some((account) => account.organizationId === record.organizationId && account.id === record.subjectId && account.disabledAt === undefined))
325
+ if (!state.serviceAccounts.some((account) => account.organizationId === record.organizationId &&
326
+ account.id === record.subjectId &&
327
+ account.disabledAt === undefined))
211
328
  throw new TeamControlPlaneError("Invalid or expired token.");
212
329
  validateTokenScope(record.scope, state, record.organizationId);
213
- const actor = Object.freeze({ organizationId: record.organizationId, serviceAccountId: record.subjectId, actorType: "service_account", tokenId: record.id });
330
+ const actor = Object.freeze({
331
+ organizationId: record.organizationId,
332
+ serviceAccountId: record.subjectId,
333
+ actorType: "service_account",
334
+ tokenId: record.id,
335
+ });
214
336
  authenticatedServiceActors.set(actor, this.now);
215
337
  return actor;
216
338
  }
@@ -225,9 +347,23 @@ export class LocalTeamControlPlane {
225
347
  requireScopedPermission(state, actor, "scenario.publish", projectId, environmentId);
226
348
  requireProject(state, actor.organizationId, projectId);
227
349
  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))
350
+ if (state.scenarios.some((scenario) => scenario.organizationId === actor.organizationId &&
351
+ scenario.projectId === projectId &&
352
+ scenario.environmentId === environmentId &&
353
+ scenario.scenarioId === scenarioId &&
354
+ scenario.version === version))
229
355
  throw new TeamControlPlaneError("Scenario version already exists.");
230
- const scenario = { organizationId: actor.organizationId, projectId, environmentId, scenarioId, version, title, metadata, createdBy: actorId(actor), createdAt: now };
356
+ const scenario = {
357
+ organizationId: actor.organizationId,
358
+ projectId,
359
+ environmentId,
360
+ scenarioId,
361
+ version,
362
+ title,
363
+ metadata,
364
+ createdBy: actorId(actor),
365
+ createdAt: now,
366
+ };
231
367
  state.scenarios.push(scenario);
232
368
  appendAudit(state, actor.organizationId, actorId(actor), "scenario.publish", `${scenarioId}@${version}`, now);
233
369
  return scenario;
@@ -243,12 +379,30 @@ export class LocalTeamControlPlane {
243
379
  requireScopedPermission(state, actor, "evidence.upload", projectId, environmentId);
244
380
  requireProject(state, actor.organizationId, projectId);
245
381
  requireEnvironment(state, actor.organizationId, projectId, environmentId);
246
- if (state.evidence.some((evidence) => evidence.organizationId === actor.organizationId && evidence.id === evidenceId))
382
+ if (state.evidence.some((evidence) => evidence.organizationId === actor.organizationId &&
383
+ evidence.id === evidenceId))
247
384
  throw new TeamControlPlaneError("Evidence already exists.");
248
385
  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)
386
+ id: evidenceId,
387
+ organizationId: actor.organizationId,
388
+ projectId,
389
+ environmentId,
390
+ runId: runId(report.run.id),
391
+ evidenceHash: report.artifact.logicalHash,
392
+ status: report.run.status,
393
+ passed: report.summary.passed,
394
+ failCount: report.summary.failCount,
395
+ warningCount: report.summary.warningCount,
396
+ providers: list(report.coverage.providers, "Evidence providers"),
397
+ scenarios: list(report.coverage.scenarios, "Evidence scenarios", true),
398
+ enforcement: {
399
+ mode: report.enforcement.mode,
400
+ isolated: report.enforcement.isolated,
401
+ degraded: report.enforcement.degraded,
402
+ },
403
+ uploadedBy: actorId(actor),
404
+ uploadedAt: now,
405
+ expiresAt: addDays(now, EVIDENCE_RETENTION_DAYS),
252
406
  };
253
407
  state.evidence.push(evidence);
254
408
  appendAudit(state, actor.organizationId, actorId(actor), "evidence.upload", evidenceId, now);
@@ -261,7 +415,9 @@ export class LocalTeamControlPlane {
261
415
  await this.mutate((state, now) => {
262
416
  requirePermission(state, actor, "data.delete");
263
417
  requireProject(state, actor.organizationId, projectId);
264
- const index = state.evidence.findIndex((evidence) => evidence.organizationId === actor.organizationId && evidence.projectId === projectId && evidence.id === evidenceId);
418
+ const index = state.evidence.findIndex((evidence) => evidence.organizationId === actor.organizationId &&
419
+ evidence.projectId === projectId &&
420
+ evidence.id === evidenceId);
265
421
  if (index === -1)
266
422
  throw new TeamControlPlaneError("Resource not found.");
267
423
  state.evidence.splice(index, 1);
@@ -274,9 +430,17 @@ export class LocalTeamControlPlane {
274
430
  validatePolicy(policy);
275
431
  return this.mutate((state, now) => {
276
432
  requirePermission(state, actor, "policy.manage");
277
- if (state.policies.some((candidate) => candidate.organizationId === actor.organizationId && candidate.version === version))
433
+ if (state.policies.some((candidate) => candidate.organizationId === actor.organizationId &&
434
+ candidate.version === version))
278
435
  throw new TeamControlPlaneError("Policy version already exists.");
279
- const distributed = { organizationId: actor.organizationId, version, policy, hash: stableHash(policy), distributedBy: actorId(actor), distributedAt: now };
436
+ const distributed = {
437
+ organizationId: actor.organizationId,
438
+ version,
439
+ policy,
440
+ hash: stableHash(policy),
441
+ distributedBy: actorId(actor),
442
+ distributedAt: now,
443
+ };
280
444
  state.policies.push(distributed);
281
445
  appendAudit(state, actor.organizationId, actorId(actor), "policy.distribute", `policy@${version}`, now);
282
446
  return distributed;
@@ -285,20 +449,30 @@ export class LocalTeamControlPlane {
285
449
  async listProjects(actor) {
286
450
  const state = await this.read();
287
451
  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));
452
+ return state.projects
453
+ .filter((project) => project.organizationId === actor.organizationId &&
454
+ (!isServiceActor(actor) ||
455
+ serviceTokenScopes(state, actor).some((scope) => scope.projectId === project.id &&
456
+ scope.permissions.includes("project.read"))))
457
+ .map((project) => structuredClone(project));
289
458
  }
290
459
  async listEvidence(actor, projectId) {
291
460
  const id = identifier(projectId, "Project id");
292
461
  const state = await this.read();
293
462
  if (isServiceActor(actor)) {
294
- if (!serviceTokenScopes(state, actor).some((scope) => scope.projectId === id && scope.permissions.includes("evidence.read")))
463
+ if (!serviceTokenScopes(state, actor).some((scope) => scope.projectId === id &&
464
+ scope.permissions.includes("evidence.read")))
295
465
  throw new TeamControlPlaneError("Access denied.");
296
466
  }
297
467
  else {
298
468
  requirePermission(state, actor, "evidence.read");
299
469
  }
300
470
  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));
471
+ return state.evidence
472
+ .filter((evidence) => evidence.organizationId === actor.organizationId &&
473
+ evidence.projectId === id &&
474
+ canReadEvidence(state, actor, evidence))
475
+ .map((evidence) => structuredClone(evidence));
302
476
  }
303
477
  async listScenarioVersions(actor, input) {
304
478
  const projectId = identifier(input.projectId, "Project id");
@@ -307,18 +481,28 @@ export class LocalTeamControlPlane {
307
481
  const state = await this.read();
308
482
  requireScopedPermission(state, actor, "scenario.read", projectId, environmentId);
309
483
  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));
484
+ return state.scenarios
485
+ .filter((scenario) => scenario.organizationId === actor.organizationId &&
486
+ scenario.projectId === projectId &&
487
+ scenario.environmentId === environmentId &&
488
+ scenario.scenarioId === scenarioId)
489
+ .sort((left, right) => left.version - right.version)
490
+ .map((scenario) => structuredClone(scenario));
311
491
  }
312
492
  async getLatestPolicy(actor) {
313
493
  const state = await this.read();
314
494
  requirePermission(state, actor, "policy.read");
315
- const policy = state.policies.filter((candidate) => candidate.organizationId === actor.organizationId).sort((left, right) => right.version - left.version)[0];
495
+ const policy = state.policies
496
+ .filter((candidate) => candidate.organizationId === actor.organizationId)
497
+ .sort((left, right) => right.version - left.version)[0];
316
498
  return policy === undefined ? null : structuredClone(policy);
317
499
  }
318
500
  async listAudit(actor) {
319
501
  const state = await this.read();
320
502
  requirePermission(state, actor, "audit.read");
321
- return state.audit.filter((record) => record.organizationId === actor.organizationId).map((record) => structuredClone(record));
503
+ return state.audit
504
+ .filter((record) => record.organizationId === actor.organizationId)
505
+ .map((record) => structuredClone(record));
322
506
  }
323
507
  async exportAudit(actor) {
324
508
  const state = await this.read();
@@ -326,8 +510,15 @@ export class LocalTeamControlPlane {
326
510
  const anchor = state.auditAnchors.find((candidate) => candidate.organizationId === actor.organizationId);
327
511
  if (anchor === undefined)
328
512
  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) };
513
+ const records = state.audit
514
+ .filter((record) => record.organizationId === actor.organizationId)
515
+ .map((record) => structuredClone(record));
516
+ return {
517
+ organizationId: actor.organizationId,
518
+ anchor: structuredClone(anchor),
519
+ records,
520
+ integrity: verifyAuditChain(actor.organizationId, anchor, records),
521
+ };
331
522
  }
332
523
  async pruneRetention(actor) {
333
524
  return this.mutate((state, now) => {
@@ -337,7 +528,10 @@ export class LocalTeamControlPlane {
337
528
  retain(state, now);
338
529
  appendAudit(state, actor.organizationId, actorId(actor), "retention.prune", "team-control-plane", now);
339
530
  retain(state, now);
340
- return { evidenceRemoved: beforeEvidence - state.evidence.length, auditRemoved: beforeAudit + 1 - state.audit.length };
531
+ return {
532
+ evidenceRemoved: beforeEvidence - state.evidence.length,
533
+ auditRemoved: beforeAudit + 1 - state.audit.length,
534
+ };
341
535
  }, false);
342
536
  }
343
537
  async deleteProject(actor, projectId) {
@@ -346,10 +540,15 @@ export class LocalTeamControlPlane {
346
540
  requirePermission(state, actor, "data.delete");
347
541
  requireProject(state, actor.organizationId, id);
348
542
  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)));
543
+ state.environments = state.environments.filter((environment) => !(environment.organizationId === actor.organizationId &&
544
+ environment.projectId === id));
545
+ state.scenarios = state.scenarios.filter((scenario) => !(scenario.organizationId === actor.organizationId &&
546
+ scenario.projectId === id));
547
+ state.evidence = state.evidence.filter((evidence) => !(evidence.organizationId === actor.organizationId &&
548
+ evidence.projectId === id));
549
+ state.tokens = state.tokens.filter((token) => !(token.organizationId === actor.organizationId &&
550
+ token.type === "service" &&
551
+ token.scope?.some((scope) => scope.projectId === id)));
353
552
  appendAudit(state, actor.organizationId, actorId(actor), "project.delete", id, now);
354
553
  });
355
554
  }
@@ -370,7 +569,8 @@ export class LocalTeamControlPlane {
370
569
  const state = migrateTeamControlPlane(JSON.parse(source));
371
570
  for (const organization of state.organizations) {
372
571
  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)
572
+ if (anchor === undefined ||
573
+ !verifyAuditChain(organization.id, anchor, state.audit.filter((record) => record.organizationId === organization.id)).valid)
374
574
  throw new TeamControlPlaneError("Audit integrity verification failed.");
375
575
  }
376
576
  return state;
@@ -403,13 +603,26 @@ export function verifyAuditExport(value) {
403
603
  return verifyAuditChain(organizationId, anchor, records);
404
604
  }
405
605
  catch (error) {
406
- return { valid: false, reason: error instanceof Error ? error.message : "Audit export is invalid." };
606
+ return {
607
+ valid: false,
608
+ reason: error instanceof Error ? error.message : "Audit export is invalid.",
609
+ };
407
610
  }
408
611
  }
409
612
  export function migrateTeamControlPlane(value) {
410
613
  const state = object(value, "Team control-plane store must be an object.");
411
614
  if (state.schemaVersion === 1) {
412
- exactKeys(state, ["schemaVersion", "organizations", "members", "projects", "environments", "tokens", "scenarios", "evidence", "policies"], "Team control-plane v1 store");
615
+ exactKeys(state, [
616
+ "schemaVersion",
617
+ "organizations",
618
+ "members",
619
+ "projects",
620
+ "environments",
621
+ "tokens",
622
+ "scenarios",
623
+ "evidence",
624
+ "policies",
625
+ ], "Team control-plane v1 store");
413
626
  return migrateV2State({ ...state, schemaVersion: 2, audit: [] });
414
627
  }
415
628
  if (state.schemaVersion === 2)
@@ -417,19 +630,44 @@ export function migrateTeamControlPlane(value) {
417
630
  return validateState(state);
418
631
  }
419
632
  function migrateV2State(value) {
420
- exactKeys(value, ["schemaVersion", "organizations", "members", "projects", "environments", "tokens", "scenarios", "evidence", "policies", "audit"], "Team control-plane v2 store");
633
+ exactKeys(value, [
634
+ "schemaVersion",
635
+ "organizations",
636
+ "members",
637
+ "projects",
638
+ "environments",
639
+ "tokens",
640
+ "scenarios",
641
+ "evidence",
642
+ "policies",
643
+ "audit",
644
+ ], "Team control-plane v2 store");
421
645
  if (value.schemaVersion !== 2)
422
646
  throw new TeamControlPlaneError("Unsupported team control-plane schema version.");
423
647
  const members = array(value.members, "Members", 1_000).map((raw) => {
424
648
  const entry = object(raw, "Member is invalid.");
425
649
  exactKeys(entry, ["id", "organizationId", "role", "createdAt"], "Member");
426
- if (entry.role !== "owner" && entry.role !== "admin" && entry.role !== "member")
650
+ if (entry.role !== "owner" &&
651
+ entry.role !== "admin" &&
652
+ entry.role !== "member")
427
653
  throw new TeamControlPlaneError("Member is invalid.");
428
- return { ...entry, role: entry.role === "member" ? "developer" : entry.role };
654
+ return {
655
+ ...entry,
656
+ role: entry.role === "member" ? "developer" : entry.role,
657
+ };
429
658
  });
430
659
  const tokens = array(value.tokens, "Tokens", 10_000).map((raw) => {
431
660
  const entry = object(raw, "Token is invalid.");
432
- exactKeys(entry, ["id", "organizationId", "memberId", "digest", "issuedBy", "issuedAt", "expiresAt", "revokedAt"], "Token", ["revokedAt"]);
661
+ exactKeys(entry, [
662
+ "id",
663
+ "organizationId",
664
+ "memberId",
665
+ "digest",
666
+ "issuedBy",
667
+ "issuedAt",
668
+ "expiresAt",
669
+ "revokedAt",
670
+ ], "Token", ["revokedAt"]);
433
671
  const { memberId, ...rest } = entry;
434
672
  return { ...rest, type: "user", subjectId: memberId };
435
673
  });
@@ -445,33 +683,71 @@ function migrateV2State(value) {
445
683
  evidence: value.evidence,
446
684
  policies: value.policies,
447
685
  auditAnchors: [],
448
- audit: []
686
+ audit: [],
449
687
  };
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")) })) });
688
+ const validatedBase = validateState({
689
+ ...base,
690
+ auditAnchors: array(value.organizations, "Organizations", 100).map((raw) => ({
691
+ organizationId: identifier(object(raw, "Organization is invalid.").id, "Organization id"),
692
+ sequence: 0,
693
+ hash: auditGenesisHash(identifier(object(raw, "Organization is invalid.").id, "Organization id")),
694
+ })),
695
+ });
451
696
  const legacyAudit = array(value.audit, "Audit records", 100_000).map((raw) => legacyAuditRecord(raw, new Set(validatedBase.organizations.map((organization) => organization.id)), validatedBase.members));
452
697
  for (const record of legacyAudit)
453
698
  appendAudit(validatedBase, record.organizationId, record.actorId, record.action, record.resource, record.timestamp);
454
699
  return validateState(validatedBase);
455
700
  }
456
701
  function emptyState() {
457
- return { schemaVersion: 3, organizations: [], members: [], serviceAccounts: [], projects: [], environments: [], tokens: [], scenarios: [], evidence: [], policies: [], auditAnchors: [], audit: [] };
702
+ return {
703
+ schemaVersion: 3,
704
+ organizations: [],
705
+ members: [],
706
+ serviceAccounts: [],
707
+ projects: [],
708
+ environments: [],
709
+ tokens: [],
710
+ scenarios: [],
711
+ evidence: [],
712
+ policies: [],
713
+ auditAnchors: [],
714
+ audit: [],
715
+ };
458
716
  }
459
717
  function validateState(value) {
460
718
  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");
719
+ exactKeys(state, [
720
+ "schemaVersion",
721
+ "organizations",
722
+ "members",
723
+ "serviceAccounts",
724
+ "projects",
725
+ "environments",
726
+ "tokens",
727
+ "scenarios",
728
+ "evidence",
729
+ "policies",
730
+ "auditAnchors",
731
+ "audit",
732
+ ], "Team control-plane store");
462
733
  if (state.schemaVersion !== SCHEMA_VERSION)
463
734
  throw new TeamControlPlaneError("Unsupported team control-plane schema version.");
464
735
  const organizations = array(state.organizations, "Organizations", 100).map((entry) => {
465
736
  const organization = object(entry, "Organization is invalid.");
466
737
  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") };
738
+ return {
739
+ id: identifier(organization.id, "Organization id"),
740
+ name: text(organization.name, "Organization name", 120),
741
+ createdAt: timestamp(organization.createdAt, "Organization createdAt"),
742
+ };
468
743
  });
469
744
  unique(organizations.map((entry) => entry.id), "Organization ids");
470
745
  const organizationIds = new Set(organizations.map((entry) => entry.id));
471
746
  const members = array(state.members, "Members", 1_000).map((entry) => member(entry, organizationIds));
472
747
  unique(members.map((entry) => scopedKey(entry.organizationId, entry.id)), "Organization member ids");
473
748
  for (const organization of organizations)
474
- if (!members.some((candidate) => candidate.organizationId === organization.id && candidate.role === "owner"))
749
+ if (!members.some((candidate) => candidate.organizationId === organization.id &&
750
+ candidate.role === "owner"))
475
751
  throw new TeamControlPlaneError("Each organization requires an owner.");
476
752
  const serviceAccounts = array(state.serviceAccounts, "Service accounts", 1_000).map((entry) => serviceAccount(entry, organizationIds, members));
477
753
  unique(serviceAccounts.map((entry) => scopedKey(entry.organizationId, entry.id)), "Organization service account ids");
@@ -481,7 +757,10 @@ function validateState(value) {
481
757
  const projectOwners = new Set(projects.map((entry) => scopedKey(entry.organizationId, entry.id)));
482
758
  const environments = array(state.environments, "Environments", 2_000).map((entry) => environment(entry, organizationIds, projectOwners));
483
759
  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]));
760
+ const environmentsById = new Map(environments.map((entry) => [
761
+ scopedKey(entry.organizationId, entry.projectId, entry.id),
762
+ entry,
763
+ ]));
485
764
  const tokens = array(state.tokens, "Tokens", 10_000).map((entry) => token(entry, organizationIds, members, serviceAccounts, projects, environments));
486
765
  unique(tokens.map((entry) => entry.id), "Token ids");
487
766
  const scenarios = array(state.scenarios, "Scenarios", 10_000).map((entry) => scenario(entry, organizationIds, projectOwners, environmentsById, actorIds));
@@ -492,31 +771,68 @@ function validateState(value) {
492
771
  unique(policies.map((entry) => scopedKey(entry.organizationId, String(entry.version))), "Policy versions");
493
772
  const auditAnchors = array(state.auditAnchors, "Audit anchors", 100).map((entry) => auditAnchor(entry, organizationIds));
494
773
  unique(auditAnchors.map((entry) => entry.organizationId), "Audit anchor organization ids");
495
- if (auditAnchors.length !== organizations.length || auditAnchors.some((anchor) => !organizationIds.has(anchor.organizationId)))
774
+ if (auditAnchors.length !== organizations.length ||
775
+ auditAnchors.some((anchor) => !organizationIds.has(anchor.organizationId)))
496
776
  throw new TeamControlPlaneError("Each organization requires an audit anchor.");
497
777
  const auditRecords = array(state.audit, "Audit records", 100_000).map((entry) => auditRecord(entry, organizationIds, members, serviceAccounts));
498
778
  for (const organization of organizations) {
499
779
  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)
780
+ if (anchor === undefined ||
781
+ !verifyAuditChain(organization.id, anchor, auditRecords.filter((record) => record.organizationId === organization.id)).valid)
501
782
  throw new TeamControlPlaneError("Audit chain is invalid.");
502
783
  }
503
- return { schemaVersion: 3, organizations, members, serviceAccounts, projects, environments, tokens, scenarios, evidence, policies, auditAnchors, audit: auditRecords };
784
+ return {
785
+ schemaVersion: 3,
786
+ organizations,
787
+ members,
788
+ serviceAccounts,
789
+ projects,
790
+ environments,
791
+ tokens,
792
+ scenarios,
793
+ evidence,
794
+ policies,
795
+ auditAnchors,
796
+ audit: auditRecords,
797
+ };
504
798
  }
505
799
  function member(value, organizations) {
506
800
  const entry = object(value, "Member is invalid.");
507
801
  exactKeys(entry, ["id", "organizationId", "role", "createdAt"], "Member");
508
802
  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"))
803
+ if (!organizations.has(organizationId) ||
804
+ (entry.role !== "owner" &&
805
+ entry.role !== "admin" &&
806
+ entry.role !== "developer" &&
807
+ entry.role !== "viewer"))
510
808
  throw new TeamControlPlaneError("Member is invalid.");
511
- return { id: identifier(entry.id, "Member id"), organizationId, role: entry.role, createdAt: timestamp(entry.createdAt, "Member createdAt") };
809
+ return {
810
+ id: identifier(entry.id, "Member id"),
811
+ organizationId,
812
+ role: entry.role,
813
+ createdAt: timestamp(entry.createdAt, "Member createdAt"),
814
+ };
512
815
  }
513
816
  function serviceAccount(value, organizations, members) {
514
817
  const entry = object(value, "Service account is invalid.");
515
818
  exactKeys(entry, ["id", "organizationId", "name", "createdBy", "createdAt", "disabledAt"], "Service account", ["disabledAt"]);
516
819
  const organizationId = identifier(entry.organizationId, "Service account organization id");
517
- if (!organizations.has(organizationId) || !members.some((member) => member.organizationId === organizationId && member.id === entry.createdBy))
820
+ if (!organizations.has(organizationId) ||
821
+ !members.some((member) => member.organizationId === organizationId &&
822
+ member.id === entry.createdBy))
518
823
  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") }) };
824
+ return {
825
+ id: identifier(entry.id, "Service account id"),
826
+ organizationId,
827
+ name: text(entry.name, "Service account name", 120),
828
+ createdBy: identifier(entry.createdBy, "Service account creator id"),
829
+ createdAt: timestamp(entry.createdAt, "Service account createdAt"),
830
+ ...(entry.disabledAt === undefined
831
+ ? {}
832
+ : {
833
+ disabledAt: timestamp(entry.disabledAt, "Service account disabledAt"),
834
+ }),
835
+ };
520
836
  }
521
837
  function project(value, organizations) {
522
838
  const entry = object(value, "Project is invalid.");
@@ -524,101 +840,277 @@ function project(value, organizations) {
524
840
  const organizationId = identifier(entry.organizationId, "Project organization id");
525
841
  if (!organizations.has(organizationId))
526
842
  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") };
843
+ return {
844
+ id: identifier(entry.id, "Project id"),
845
+ organizationId,
846
+ name: text(entry.name, "Project name", 120),
847
+ createdAt: timestamp(entry.createdAt, "Project createdAt"),
848
+ };
528
849
  }
529
850
  function environment(value, organizations, projects) {
530
851
  const entry = object(value, "Environment is invalid.");
531
852
  exactKeys(entry, ["id", "organizationId", "projectId", "name", "kind", "createdAt"], "Environment");
532
853
  const organizationId = identifier(entry.organizationId, "Environment organization id");
533
854
  const projectId = identifier(entry.projectId, "Environment project id");
534
- if (!organizations.has(organizationId) || !projects.has(scopedKey(organizationId, projectId)) || (entry.kind !== "development" && entry.kind !== "ci"))
855
+ if (!organizations.has(organizationId) ||
856
+ !projects.has(scopedKey(organizationId, projectId)) ||
857
+ (entry.kind !== "development" && entry.kind !== "ci"))
535
858
  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") };
859
+ return {
860
+ id: identifier(entry.id, "Environment id"),
861
+ organizationId,
862
+ projectId,
863
+ name: text(entry.name, "Environment name", 80),
864
+ kind: entry.kind,
865
+ createdAt: timestamp(entry.createdAt, "Environment createdAt"),
866
+ };
537
867
  }
538
868
  function token(value, organizations, members, serviceAccounts, projects, environments) {
539
869
  const entry = object(value, "Token is invalid.");
540
- exactKeys(entry, ["id", "organizationId", "type", "subjectId", "digest", "issuedBy", "issuedAt", "expiresAt", "scope", "revokedAt"], "Token", ["scope", "revokedAt"]);
870
+ exactKeys(entry, [
871
+ "id",
872
+ "organizationId",
873
+ "type",
874
+ "subjectId",
875
+ "digest",
876
+ "issuedBy",
877
+ "issuedAt",
878
+ "expiresAt",
879
+ "scope",
880
+ "revokedAt",
881
+ ], "Token", ["scope", "revokedAt"]);
541
882
  const organizationId = identifier(entry.organizationId, "Token organization id");
542
883
  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))
884
+ if (!organizations.has(organizationId) ||
885
+ (entry.type !== "user" && entry.type !== "service") ||
886
+ typeof entry.digest !== "string" ||
887
+ !/^[a-f0-9]{64}$/.test(entry.digest))
544
888
  throw new TeamControlPlaneError("Token is invalid.");
545
- if (entry.type === "user" && !members.some((member) => member.organizationId === organizationId && member.id === subjectId))
889
+ if (entry.type === "user" &&
890
+ !members.some((member) => member.organizationId === organizationId && member.id === subjectId))
546
891
  throw new TeamControlPlaneError("Token is invalid.");
547
- const serviceAccount = entry.type === "service" ? serviceAccounts.find((account) => account.organizationId === organizationId && account.id === subjectId) : undefined;
892
+ const serviceAccount = entry.type === "service"
893
+ ? serviceAccounts.find((account) => account.organizationId === organizationId &&
894
+ account.id === subjectId)
895
+ : undefined;
548
896
  if (entry.type === "service" && serviceAccount === undefined)
549
897
  throw new TeamControlPlaneError("Token is invalid.");
550
- const scope = entry.type === "service" ? validateTokenScope(entry.scope, { projects, environments }, organizationId) : undefined;
898
+ const scope = entry.type === "service"
899
+ ? validateTokenScope(entry.scope, { projects, environments }, organizationId)
900
+ : undefined;
551
901
  if (entry.type === "user" && entry.scope !== undefined)
552
902
  throw new TeamControlPlaneError("Token is invalid.");
553
903
  if (!actorReferenceExists(organizationId, entry.issuedBy, members, serviceAccounts))
554
904
  throw new TeamControlPlaneError("Token is invalid.");
555
905
  const issuedAt = timestamp(entry.issuedAt, "Token issuedAt");
556
906
  const expiresAt = timestamp(entry.expiresAt, "Token expiresAt");
557
- if (new Date(expiresAt).getTime() > new Date(issuedAt).getTime() + 90 * 24 * 60 * 60 * 1000)
907
+ if (new Date(expiresAt).getTime() >
908
+ new Date(issuedAt).getTime() + 90 * 24 * 60 * 60 * 1000)
558
909
  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)
910
+ const revokedAt = entry.revokedAt === undefined
911
+ ? undefined
912
+ : timestamp(entry.revokedAt, "Token revokedAt");
913
+ if (entry.type === "service" &&
914
+ serviceAccount?.disabledAt !== undefined &&
915
+ revokedAt === undefined)
561
916
  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 }) };
917
+ return {
918
+ id: identifier(entry.id, "Token id"),
919
+ organizationId,
920
+ type: entry.type,
921
+ subjectId,
922
+ digest: entry.digest,
923
+ issuedBy: actorReference(entry.issuedBy, "Token issuer id"),
924
+ issuedAt,
925
+ expiresAt,
926
+ ...(scope === undefined ? {} : { scope }),
927
+ ...(revokedAt === undefined ? {} : { revokedAt }),
928
+ };
563
929
  }
564
930
  function scenario(value, organizations, projects, environments, actors) {
565
931
  const entry = object(value, "Scenario is invalid.");
566
- exactKeys(entry, ["organizationId", "projectId", "environmentId", "scenarioId", "version", "title", "metadata", "createdBy", "createdAt"], "Scenario");
932
+ exactKeys(entry, [
933
+ "organizationId",
934
+ "projectId",
935
+ "environmentId",
936
+ "scenarioId",
937
+ "version",
938
+ "title",
939
+ "metadata",
940
+ "createdBy",
941
+ "createdAt",
942
+ ], "Scenario");
567
943
  const organizationId = identifier(entry.organizationId, "Scenario organization id");
568
944
  const projectId = identifier(entry.projectId, "Scenario project id");
569
945
  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"))))
946
+ if (!organizations.has(organizationId) ||
947
+ !projects.has(scopedKey(organizationId, projectId)) ||
948
+ environments.get(scopedKey(organizationId, projectId, environmentId)) ===
949
+ undefined ||
950
+ !actors.has(scopedKey(organizationId, actorReference(entry.createdBy, "Scenario creator id"))))
571
951
  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") };
952
+ return {
953
+ organizationId,
954
+ projectId,
955
+ environmentId,
956
+ scenarioId: identifier(entry.scenarioId, "Scenario id"),
957
+ version: positiveInteger(entry.version, "Scenario version"),
958
+ title: text(entry.title, "Scenario title", 160),
959
+ metadata: safeJson(entry.metadata, "Scenario metadata", 16 * 1024),
960
+ createdBy: actorReference(entry.createdBy, "Scenario creator id"),
961
+ createdAt: timestamp(entry.createdAt, "Scenario createdAt"),
962
+ };
573
963
  }
574
964
  function evidenceRecord(value, organizations, projects, environments, actors) {
575
965
  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");
966
+ exactKeys(entry, [
967
+ "id",
968
+ "organizationId",
969
+ "projectId",
970
+ "environmentId",
971
+ "runId",
972
+ "evidenceHash",
973
+ "status",
974
+ "passed",
975
+ "failCount",
976
+ "warningCount",
977
+ "providers",
978
+ "scenarios",
979
+ "enforcement",
980
+ "uploadedBy",
981
+ "uploadedAt",
982
+ "expiresAt",
983
+ ], "Evidence");
577
984
  const organizationId = identifier(entry.organizationId, "Evidence organization id");
578
985
  const projectId = identifier(entry.projectId, "Evidence project id");
579
986
  const environmentId = identifier(entry.environmentId, "Evidence environment id");
580
987
  const enforcement = object(entry.enforcement, "Evidence enforcement is invalid.");
581
988
  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")
989
+ if (!organizations.has(organizationId) ||
990
+ !projects.has(scopedKey(organizationId, projectId)) ||
991
+ environments.get(scopedKey(organizationId, projectId, environmentId)) ===
992
+ undefined ||
993
+ !actors.has(scopedKey(organizationId, actorReference(entry.uploadedBy, "Evidence uploader id"))) ||
994
+ typeof entry.evidenceHash !== "string" ||
995
+ !/^[a-f0-9]{64}$/.test(entry.evidenceHash) ||
996
+ !runStatus(entry.status) ||
997
+ typeof entry.passed !== "boolean" ||
998
+ !nonNegativeInteger(entry.failCount) ||
999
+ !nonNegativeInteger(entry.warningCount) ||
1000
+ !evidenceMode(enforcement.mode) ||
1001
+ typeof enforcement.isolated !== "boolean" ||
1002
+ typeof enforcement.degraded !== "boolean")
583
1003
  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") };
1004
+ return {
1005
+ id: identifier(entry.id, "Evidence id"),
1006
+ organizationId,
1007
+ projectId,
1008
+ environmentId,
1009
+ runId: runId(entry.runId),
1010
+ evidenceHash: entry.evidenceHash,
1011
+ status: entry.status,
1012
+ passed: entry.passed,
1013
+ failCount: entry.failCount,
1014
+ warningCount: entry.warningCount,
1015
+ providers: list(entry.providers, "Evidence providers"),
1016
+ scenarios: list(entry.scenarios, "Evidence scenarios", true),
1017
+ enforcement: {
1018
+ mode: enforcement.mode,
1019
+ isolated: enforcement.isolated,
1020
+ degraded: enforcement.degraded,
1021
+ },
1022
+ uploadedBy: actorReference(entry.uploadedBy, "Evidence uploader id"),
1023
+ uploadedAt: timestamp(entry.uploadedAt, "Evidence uploadedAt"),
1024
+ expiresAt: timestamp(entry.expiresAt, "Evidence expiresAt"),
1025
+ };
585
1026
  }
586
1027
  function policyVersion(value, organizations, actors) {
587
1028
  const entry = object(value, "Policy is invalid.");
588
- exactKeys(entry, ["organizationId", "version", "policy", "hash", "distributedBy", "distributedAt"], "Policy");
1029
+ exactKeys(entry, [
1030
+ "organizationId",
1031
+ "version",
1032
+ "policy",
1033
+ "hash",
1034
+ "distributedBy",
1035
+ "distributedAt",
1036
+ ], "Policy");
589
1037
  const organizationId = identifier(entry.organizationId, "Policy organization id");
590
1038
  const policy = safeJson(entry.policy, "Team policy", 32 * 1024);
591
1039
  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))
1040
+ if (!organizations.has(organizationId) ||
1041
+ !actors.has(scopedKey(organizationId, actorReference(entry.distributedBy, "Policy distributor id"))) ||
1042
+ typeof entry.hash !== "string" ||
1043
+ entry.hash !== stableHash(policy))
593
1044
  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") };
1045
+ return {
1046
+ organizationId,
1047
+ version: positiveInteger(entry.version, "Policy version"),
1048
+ policy,
1049
+ hash: entry.hash,
1050
+ distributedBy: actorReference(entry.distributedBy, "Policy distributor id"),
1051
+ distributedAt: timestamp(entry.distributedAt, "Policy distributedAt"),
1052
+ };
595
1053
  }
596
1054
  function auditAnchor(value, organizations) {
597
1055
  const entry = object(value, "Audit anchor is invalid.");
598
1056
  exactKeys(entry, ["organizationId", "sequence", "hash"], "Audit anchor");
599
1057
  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))
1058
+ if (!organizations.has(organizationId) ||
1059
+ !nonNegativeInteger(entry.sequence) ||
1060
+ typeof entry.hash !== "string" ||
1061
+ !/^[a-f0-9]{64}$/.test(entry.hash))
601
1062
  throw new TeamControlPlaneError("Audit anchor is invalid.");
602
1063
  return { organizationId, sequence: entry.sequence, hash: entry.hash };
603
1064
  }
604
1065
  function auditRecord(value, organizations, members, serviceAccounts) {
605
1066
  const entry = object(value, "Audit record is invalid.");
606
- exactKeys(entry, ["organizationId", "sequence", "actorId", "action", "resource", "timestamp", "previousHash", "recordHash"], "Audit record");
1067
+ exactKeys(entry, [
1068
+ "organizationId",
1069
+ "sequence",
1070
+ "actorId",
1071
+ "action",
1072
+ "resource",
1073
+ "timestamp",
1074
+ "previousHash",
1075
+ "recordHash",
1076
+ ], "Audit record");
607
1077
  const organizationId = identifier(entry.organizationId, "Audit organization id");
608
1078
  const actorIdValue = actorReference(entry.actorId, "Audit actor id");
609
1079
  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))
1080
+ if (!organizations.has(organizationId) ||
1081
+ ((members.length > 0 || serviceAccounts.length > 0) &&
1082
+ !actorReferenceExists(organizationId, actorIdValue, members, serviceAccounts)) ||
1083
+ typeof entry.previousHash !== "string" ||
1084
+ !/^[a-f0-9]{64}$/.test(entry.previousHash) ||
1085
+ typeof entry.recordHash !== "string" ||
1086
+ !/^[a-f0-9]{64}$/.test(entry.recordHash))
611
1087
  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 };
1088
+ return {
1089
+ organizationId,
1090
+ sequence,
1091
+ actorId: actorIdValue,
1092
+ action: text(entry.action, "Audit action", 80),
1093
+ resource: text(entry.resource, "Audit resource", 160),
1094
+ timestamp: timestamp(entry.timestamp, "Audit timestamp"),
1095
+ previousHash: entry.previousHash,
1096
+ recordHash: entry.recordHash,
1097
+ };
613
1098
  }
614
1099
  function legacyAuditRecord(value, organizations, members) {
615
1100
  const entry = object(value, "Audit record is invalid.");
616
1101
  exactKeys(entry, ["organizationId", "actorId", "action", "resource", "timestamp"], "Audit record");
617
1102
  const organizationId = identifier(entry.organizationId, "Audit organization id");
618
1103
  const actorIdValue = identifier(entry.actorId, "Audit actor id");
619
- if (!organizations.has(organizationId) || !members.some((member) => member.organizationId === organizationId && member.id === actorIdValue))
1104
+ if (!organizations.has(organizationId) ||
1105
+ !members.some((member) => member.organizationId === organizationId && member.id === actorIdValue))
620
1106
  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") };
1107
+ return {
1108
+ organizationId,
1109
+ actorId: actorIdValue,
1110
+ action: text(entry.action, "Audit action", 80),
1111
+ resource: text(entry.resource, "Audit resource", 160),
1112
+ timestamp: timestamp(entry.timestamp, "Audit timestamp"),
1113
+ };
622
1114
  }
623
1115
  function requirePermission(state, actor, permission) {
624
1116
  const role = actorRole(state, actor);
@@ -627,17 +1119,21 @@ function requirePermission(state, actor, permission) {
627
1119
  }
628
1120
  function requireScopedPermission(state, actor, permission, projectId, environmentId) {
629
1121
  requirePermission(state, actor, permission);
630
- if (isServiceActor(actor) && !serviceTokenScopes(state, actor).some((scope) => scope.projectId === projectId && scope.environmentId === environmentId && scope.permissions.includes(permission)))
1122
+ if (isServiceActor(actor) &&
1123
+ !serviceTokenScopes(state, actor).some((scope) => scope.projectId === projectId &&
1124
+ scope.environmentId === environmentId &&
1125
+ scope.permissions.includes(permission)))
631
1126
  throw new TeamControlPlaneError("Access denied.");
632
1127
  }
633
1128
  const MEMBER_TOKEN_MANAGEMENT_MATRIX = Object.freeze({
634
1129
  owner: ["owner", "admin", "developer", "viewer"],
635
1130
  admin: ["admin", "developer", "viewer"],
636
1131
  developer: [],
637
- viewer: []
1132
+ viewer: [],
638
1133
  });
639
1134
  function canManageMemberToken(actor, subject) {
640
- return actor !== "service_account" && MEMBER_TOKEN_MANAGEMENT_MATRIX[actor].includes(subject);
1135
+ return (actor !== "service_account" &&
1136
+ MEMBER_TOKEN_MANAGEMENT_MATRIX[actor].includes(subject));
641
1137
  }
642
1138
  function actorRole(state, actor) {
643
1139
  const organizationId = identifier(actor.organizationId, "Organization id");
@@ -645,7 +1141,9 @@ function actorRole(state, actor) {
645
1141
  if (!authenticatedServiceActors.has(actor))
646
1142
  throw new TeamControlPlaneError("Access denied.");
647
1143
  const serviceAccountId = identifier(actor.serviceAccountId, "Service account id");
648
- const account = state.serviceAccounts.find((candidate) => candidate.organizationId === organizationId && candidate.id === serviceAccountId && candidate.disabledAt === undefined);
1144
+ const account = state.serviceAccounts.find((candidate) => candidate.organizationId === organizationId &&
1145
+ candidate.id === serviceAccountId &&
1146
+ candidate.disabledAt === undefined);
649
1147
  if (account === undefined)
650
1148
  throw new TeamControlPlaneError("Access denied.");
651
1149
  serviceTokenScopes(state, actor);
@@ -666,17 +1164,22 @@ function requireMember(state, actor) {
666
1164
  return member;
667
1165
  }
668
1166
  function requireServiceAccount(state, organizationId, serviceAccountId) {
669
- const account = state.serviceAccounts.find((candidate) => candidate.organizationId === organizationId && candidate.id === serviceAccountId && candidate.disabledAt === undefined);
1167
+ const account = state.serviceAccounts.find((candidate) => candidate.organizationId === organizationId &&
1168
+ candidate.id === serviceAccountId &&
1169
+ candidate.disabledAt === undefined);
670
1170
  if (account === undefined)
671
1171
  throw new TeamControlPlaneError("Resource not found.");
672
1172
  return account;
673
1173
  }
674
1174
  function requireProject(state, organizationId, projectId) {
675
- if (!state.projects.some((candidate) => candidate.organizationId === organizationId && candidate.id === projectId))
1175
+ if (!state.projects.some((candidate) => candidate.organizationId === organizationId &&
1176
+ candidate.id === projectId))
676
1177
  throw new TeamControlPlaneError("Resource not found.");
677
1178
  }
678
1179
  function requireEnvironment(state, organizationId, projectId, environmentId) {
679
- if (!state.environments.some((candidate) => candidate.organizationId === organizationId && candidate.projectId === projectId && candidate.id === environmentId))
1180
+ if (!state.environments.some((candidate) => candidate.organizationId === organizationId &&
1181
+ candidate.projectId === projectId &&
1182
+ candidate.id === environmentId))
680
1183
  throw new TeamControlPlaneError("Resource not found.");
681
1184
  }
682
1185
  function issueTokenRecord(state, input, issuedAt, random) {
@@ -686,7 +1189,19 @@ function issueTokenRecord(state, input, issuedAt, random) {
686
1189
  const digest = hash(token);
687
1190
  if (state.tokens.some((candidate) => candidate.id === tokenId || equalHash(candidate.digest, digest)))
688
1191
  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) }) });
1192
+ state.tokens.push({
1193
+ id: tokenId,
1194
+ organizationId: input.organizationId,
1195
+ type: input.type,
1196
+ subjectId: input.subjectId,
1197
+ digest,
1198
+ issuedBy: input.issuedBy,
1199
+ issuedAt,
1200
+ expiresAt: input.expiresAt,
1201
+ ...(input.scope === undefined
1202
+ ? {}
1203
+ : { scope: structuredClone(input.scope) }),
1204
+ });
690
1205
  return { tokenId, token, expiresAt: input.expiresAt };
691
1206
  }
692
1207
  throw new TeamControlPlaneError("Unable to allocate a unique token.");
@@ -699,19 +1214,46 @@ function appendAudit(state, organizationId, actorIdValue, action, resource, time
699
1214
  const previous = records.at(-1);
700
1215
  const sequence = previous === undefined ? anchor.sequence + 1 : previous.sequence + 1;
701
1216
  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) });
1217
+ const recordWithoutHash = {
1218
+ organizationId,
1219
+ sequence,
1220
+ actorId: actorIdValue,
1221
+ action,
1222
+ resource,
1223
+ timestamp: timestampValue,
1224
+ previousHash,
1225
+ };
1226
+ state.audit.push({
1227
+ ...recordWithoutHash,
1228
+ recordHash: stableHash(recordWithoutHash),
1229
+ });
704
1230
  }
705
1231
  function verifyAuditChain(organizationId, anchor, records) {
706
1232
  try {
707
1233
  if (anchor.organizationId !== organizationId)
708
- return { valid: false, reason: "Audit anchor organization does not match." };
1234
+ return {
1235
+ valid: false,
1236
+ reason: "Audit anchor organization does not match.",
1237
+ };
709
1238
  let expectedSequence = anchor.sequence + 1;
710
1239
  let previousHash = anchor.hash;
711
1240
  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 });
1241
+ if (record.organizationId !== organizationId ||
1242
+ record.sequence !== expectedSequence ||
1243
+ record.previousHash !== previousHash)
1244
+ return {
1245
+ valid: false,
1246
+ reason: "Audit sequence or previous hash is invalid.",
1247
+ };
1248
+ const expectedHash = stableHash({
1249
+ organizationId: record.organizationId,
1250
+ sequence: record.sequence,
1251
+ actorId: record.actorId,
1252
+ action: record.action,
1253
+ resource: record.resource,
1254
+ timestamp: record.timestamp,
1255
+ previousHash: record.previousHash,
1256
+ });
715
1257
  if (!equalHash(record.recordHash, expectedHash))
716
1258
  return { valid: false, reason: "Audit record hash is invalid." };
717
1259
  expectedSequence += 1;
@@ -732,8 +1274,12 @@ function retain(state, now) {
732
1274
  }
733
1275
  function pruneAudit(state, organizationId, now) {
734
1276
  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);
1277
+ const all = state.audit
1278
+ .filter((entry) => entry.organizationId === organizationId)
1279
+ .sort((left, right) => left.sequence - right.sequence);
1280
+ const retained = all
1281
+ .filter((entry) => entry.timestamp >= cutoff)
1282
+ .slice(-MAX_AUDIT_PER_ORGANIZATION);
737
1283
  if (retained.length === all.length)
738
1284
  return;
739
1285
  const removed = all.slice(0, all.length - retained.length);
@@ -748,7 +1294,10 @@ function pruneAudit(state, organizationId, now) {
748
1294
  state.audit = state.audit.filter((entry) => entry.organizationId !== organizationId || retained.includes(entry));
749
1295
  }
750
1296
  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));
1297
+ const allowed = new Set(entries
1298
+ .filter((entry) => entry.organizationId === organizationId)
1299
+ .sort((left, right) => key(right).localeCompare(key(left)))
1300
+ .slice(0, max));
752
1301
  return entries.filter((entry) => entry.organizationId !== organizationId || allowed.has(entry));
753
1302
  }
754
1303
  function validateTokenScope(value, state, organizationId) {
@@ -757,10 +1306,15 @@ function validateTokenScope(value, state, organizationId) {
757
1306
  exactKeys(entry, ["projectId", "environmentId", "permissions"], "Service token scope");
758
1307
  const projectId = identifier(entry.projectId, "Service token scope project id");
759
1308
  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))
1309
+ if (!state.projects.some((project) => project.organizationId === organizationId && project.id === projectId) ||
1310
+ !state.environments.some((environment) => environment.organizationId === organizationId &&
1311
+ environment.projectId === projectId &&
1312
+ environment.id === environmentId))
761
1313
  throw new TeamControlPlaneError("Service token scope is invalid.");
762
1314
  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))
1315
+ if (typeof permission !== "string" ||
1316
+ !isScopedPermission(permission) ||
1317
+ !SERVICE_ACCOUNT_PERMISSIONS.includes(permission))
764
1318
  throw new TeamControlPlaneError("Service token permissions are invalid.");
765
1319
  return permission;
766
1320
  });
@@ -775,7 +1329,10 @@ function validateTokenScope(value, state, organizationId) {
775
1329
  return scopes.sort((left, right) => scopedKey(left.projectId, left.environmentId).localeCompare(scopedKey(right.projectId, right.environmentId)));
776
1330
  }
777
1331
  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"));
1332
+ return (!isServiceActor(actor) ||
1333
+ serviceTokenScopes(state, actor).some((scope) => scope.projectId === evidence.projectId &&
1334
+ scope.environmentId === evidence.environmentId &&
1335
+ scope.permissions.includes("evidence.read")));
779
1336
  }
780
1337
  function isServiceActor(actor) {
781
1338
  return actor.actorType === "service_account";
@@ -785,13 +1342,20 @@ function serviceTokenScopes(state, actor) {
785
1342
  if (clock === undefined)
786
1343
  throw new TeamControlPlaneError("Access denied.");
787
1344
  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())
1345
+ const token = state.tokens.find((candidate) => candidate.id === tokenId &&
1346
+ candidate.organizationId === actor.organizationId &&
1347
+ candidate.type === "service" &&
1348
+ candidate.subjectId === actor.serviceAccountId);
1349
+ if (token === undefined ||
1350
+ token.revokedAt !== undefined ||
1351
+ new Date(token.expiresAt).getTime() <= clock().getTime())
790
1352
  throw new TeamControlPlaneError("Access denied.");
791
1353
  return validateTokenScope(token.scope, state, actor.organizationId);
792
1354
  }
793
1355
  function actorId(actor) {
794
- return isServiceActor(actor) ? `service:${identifier(actor.serviceAccountId, "Service account id")}` : identifier(actor.memberId, "Member id");
1356
+ return isServiceActor(actor)
1357
+ ? `service:${identifier(actor.serviceAccountId, "Service account id")}`
1358
+ : identifier(actor.memberId, "Member id");
795
1359
  }
796
1360
  function userActorId(actor) {
797
1361
  if (isServiceActor(actor))
@@ -806,17 +1370,28 @@ function actorReference(value, label) {
806
1370
  return identifier(value, label);
807
1371
  }
808
1372
  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}`))]);
1373
+ return new Set([
1374
+ ...members.map((member) => scopedKey(member.organizationId, member.id)),
1375
+ ...serviceAccounts.map((account) => scopedKey(account.organizationId, `service:${account.id}`)),
1376
+ ]);
810
1377
  }
811
1378
  function actorReferenceExists(organizationId, actorIdValue, members, serviceAccounts) {
812
1379
  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)));
1380
+ return (members.some((member) => member.organizationId === organizationId && member.id === id) ||
1381
+ (id.startsWith("service:") &&
1382
+ serviceAccounts.some((account) => account.organizationId === organizationId &&
1383
+ account.id === id.slice("service:".length))));
814
1384
  }
815
1385
  function rejectRawEvidence(report) {
816
1386
  const encoded = JSON.stringify(report);
817
1387
  if (Buffer.byteLength(encoded, "utf8") > 512 * 1024)
818
1388
  throw new TeamControlPlaneError("Evidence artifact exceeds 524288 bytes.");
819
- const prohibited = new Set(["authorization", "cookie", "set-cookie", "rawbody"]);
1389
+ const prohibited = new Set([
1390
+ "authorization",
1391
+ "cookie",
1392
+ "set-cookie",
1393
+ "rawbody",
1394
+ ]);
820
1395
  const visit = (value, key) => {
821
1396
  if (key !== undefined && prohibited.has(key.toLowerCase()))
822
1397
  throw new TeamControlPlaneError("Evidence artifact contains an unsafe raw field.");
@@ -867,52 +1442,149 @@ function inspectJson(value, label, depth, key) {
867
1442
  }
868
1443
  function validatePolicy(policy) {
869
1444
  const candidate = policy;
870
- if (candidate.version !== 1 || !plainObject(candidate.network) || !plainObject(candidate.credentials) || !Array.isArray(candidate.requiredScenarios) || !plainObject(candidate.enforcement) || !plainObject(candidate.reports))
1445
+ if (candidate.version !== 1 ||
1446
+ !plainObject(candidate.network) ||
1447
+ !plainObject(candidate.credentials) ||
1448
+ !Array.isArray(candidate.requiredScenarios) ||
1449
+ !plainObject(candidate.enforcement) ||
1450
+ !plainObject(candidate.reports))
871
1451
  throw new TeamControlPlaneError("Team policy does not match GhostAPI policy schema v1.");
872
1452
  }
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")); }
1453
+ function hash(value) {
1454
+ return createHash("sha256").update(value).digest("hex");
1455
+ }
1456
+ function stableHash(value) {
1457
+ return createHash("sha256").update(stableStringify(value)).digest("hex");
1458
+ }
1459
+ function auditGenesisHash(organizationId) {
1460
+ return hash(`ghostapi-team-audit-v3:${organizationId}`);
1461
+ }
1462
+ function equalHash(left, right) {
1463
+ return (/^[a-f0-9]{64}$/.test(left) &&
1464
+ /^[a-f0-9]{64}$/.test(right) &&
1465
+ timingSafeEqual(Buffer.from(left, "hex"), Buffer.from(right, "hex")));
1466
+ }
877
1467
  function stableStringify(value) {
878
1468
  if (value === null || typeof value !== "object")
879
1469
  return JSON.stringify(value);
880
1470
  if (Array.isArray(value))
881
1471
  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; }
1472
+ return `{${Object.keys(value)
1473
+ .sort()
1474
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
1475
+ .join(",")}}`;
1476
+ }
1477
+ function scopedKey(...parts) {
1478
+ return parts.join("\u0000");
1479
+ }
1480
+ function identifier(value, label) {
1481
+ if (typeof value !== "string" || !IDENTIFIER.test(value))
1482
+ throw new TeamControlPlaneError(`${label} must be 1-64 lowercase letters, numbers, underscores, or hyphens.`);
1483
+ return value;
1484
+ }
1485
+ function runId(value) {
1486
+ if (typeof value !== "string" ||
1487
+ value.length === 0 ||
1488
+ value.length > 128 ||
1489
+ /[\u0000-\u001f]/.test(value) ||
1490
+ sanitizeSecretString(value) !== value)
1491
+ throw new TeamControlPlaneError("Evidence run id is invalid.");
1492
+ return value;
1493
+ }
1494
+ function text(value, label, max) {
1495
+ if (typeof value !== "string" ||
1496
+ value.trim() === "" ||
1497
+ value.length > max ||
1498
+ /[\u0000-\u001f]/.test(value) ||
1499
+ sanitizeSecretString(value) !== value)
1500
+ throw new TeamControlPlaneError(`${label} is invalid.`);
1501
+ return value.trim();
1502
+ }
1503
+ function timestamp(value, label) {
1504
+ if (typeof value !== "string" ||
1505
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) ||
1506
+ Number.isNaN(new Date(value).getTime()))
1507
+ throw new TeamControlPlaneError(`${label} is invalid.`);
1508
+ return value;
1509
+ }
1510
+ function futureTimestamp(value, now) {
1511
+ const result = timestamp(value, "Token expiry");
1512
+ const milliseconds = new Date(result).getTime();
1513
+ if (milliseconds <= now.getTime() ||
1514
+ milliseconds > now.getTime() + 90 * 24 * 60 * 60 * 1000)
1515
+ throw new TeamControlPlaneError("Token expiry must be within the next 90 days.");
1516
+ return result;
1517
+ }
1518
+ function positiveInteger(value, label) {
1519
+ if (!nonNegativeInteger(value) || value < 1 || value > 100_000)
1520
+ throw new TeamControlPlaneError(`${label} is invalid.`);
1521
+ return value;
1522
+ }
1523
+ function nonNegativeInteger(value) {
1524
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
1525
+ }
1526
+ function list(value, label, dotted = false) {
1527
+ if (!Array.isArray(value) || value.length > 100)
1528
+ throw new TeamControlPlaneError(`${label} are invalid.`);
1529
+ const result = value.map((entry) => {
1530
+ if (typeof entry !== "string" ||
1531
+ (dotted
1532
+ ? !/^[a-z0-9][a-z0-9._-]{0,127}$/.test(entry)
1533
+ : !IDENTIFIER.test(entry)))
1534
+ throw new TeamControlPlaneError(`${label} are invalid.`);
1535
+ return entry;
1536
+ });
1537
+ unique(result, label);
1538
+ return result.sort();
1539
+ }
1540
+ function object(value, message) {
1541
+ if (!plainObject(value))
1542
+ throw new TeamControlPlaneError(message);
1543
+ return value;
1544
+ }
1545
+ function plainObject(value) {
1546
+ return (value !== null &&
1547
+ typeof value === "object" &&
1548
+ !Array.isArray(value) &&
1549
+ Object.getPrototypeOf(value) === Object.prototype);
1550
+ }
1551
+ function array(value, label, max) {
1552
+ if (!Array.isArray(value) || value.length > max)
1553
+ throw new TeamControlPlaneError(`${label} are invalid.`);
1554
+ return value;
1555
+ }
1556
+ function exactKeys(value, allowed, label, optional = []) {
1557
+ for (const key of Object.keys(value))
1558
+ if (!allowed.includes(key))
1559
+ throw new TeamControlPlaneError(`${label} contains unsupported field: ${key}.`);
1560
+ for (const key of allowed)
1561
+ if (!optional.includes(key) && !(key in value))
1562
+ throw new TeamControlPlaneError(`${label} is missing field: ${key}.`);
1563
+ }
1564
+ function unique(values, label) {
1565
+ if (new Set(values).size !== values.length)
1566
+ throw new TeamControlPlaneError(`${label} must be unique.`);
1567
+ }
1568
+ function runStatus(value) {
1569
+ return (value === "unknown" ||
1570
+ value === "preparing" ||
1571
+ value === "running" ||
1572
+ value === "failed-to-start" ||
1573
+ value === "finished");
1574
+ }
1575
+ function evidenceMode(value) {
1576
+ return (value === "linux-network-namespace" ||
1577
+ value === "proxy-guidance" ||
1578
+ value === "unknown");
1579
+ }
1580
+ function isScopedPermission(value) {
1581
+ return (READ_PERMISSIONS.includes(value) ||
1582
+ PUBLISH_PERMISSIONS.includes(value));
1583
+ }
1584
+ function addDays(value, days) {
1585
+ return new Date(new Date(value).getTime() + days * 24 * 60 * 60 * 1000).toISOString();
1586
+ }
1587
+ function isErrorCode(error, code) {
1588
+ return error instanceof Error && "code" in error && error.code === code;
1589
+ }
918
1590
  //# sourceMappingURL=controlPlane.js.map