@yiaany/ghostapi 0.1.9 → 0.2.0
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.
- package/CHANGELOG.md +21 -0
- package/CONTRIBUTING.md +132 -119
- package/LICENSE +21 -21
- package/PROVENANCE.md +13 -0
- package/README.md +78 -84
- package/SECURITY.md +56 -53
- package/dist/actions/gateway.js +320 -72
- package/dist/actions/gateway.js.map +1 -1
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/index.js +1 -1
- package/dist/actions/index.js.map +1 -1
- package/dist/agents/agentPrompt.js +7 -5
- package/dist/agents/agentPrompt.js.map +1 -1
- package/dist/ai/aiClient.js +5 -5
- package/dist/ai/aiClient.js.map +1 -1
- package/dist/ai/aiGenerator.js +66 -26
- package/dist/ai/aiGenerator.js.map +1 -1
- package/dist/ai/genericInference.js +151 -36
- package/dist/ai/genericInference.js.map +1 -1
- package/dist/ai/prompts.js +55 -49
- package/dist/ai/prompts.js.map +1 -1
- package/dist/approvals/approvalInbox.js +649 -133
- package/dist/approvals/approvalInbox.js.map +1 -1
- package/dist/approvals/index.d.ts +2 -2
- package/dist/approvals/index.js +1 -1
- package/dist/approvals/index.js.map +1 -1
- package/dist/behavior/behaviorStore.js +34 -14
- package/dist/behavior/behaviorStore.js.map +1 -1
- package/dist/cache/index.js +14 -5
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/index.js +226 -93
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/openUrl.js +17 -3
- package/dist/cli/openUrl.js.map +1 -1
- package/dist/cli/parser.js +79 -18
- package/dist/cli/parser.js.map +1 -1
- package/dist/config/dataPaths.js +4 -2
- package/dist/config/dataPaths.js.map +1 -1
- package/dist/config/localConfig.js +5 -2
- package/dist/config/localConfig.js.map +1 -1
- package/dist/config/serverConfig.js +11 -5
- package/dist/config/serverConfig.js.map +1 -1
- package/dist/contracts/contract.js +311 -83
- package/dist/contracts/contract.js.map +1 -1
- package/dist/contracts/index.d.ts +2 -2
- package/dist/contracts/index.js +1 -1
- package/dist/contracts/index.js.map +1 -1
- package/dist/credentials/credentialBroker.js +391 -79
- package/dist/credentials/credentialBroker.js.map +1 -1
- package/dist/credentials/index.d.ts +2 -2
- package/dist/credentials/index.js +1 -1
- package/dist/credentials/index.js.map +1 -1
- package/dist/dashboard/app.js +823 -685
- package/dist/dashboard/dashboard.js.map +1 -1
- package/dist/dashboard/index.html +704 -267
- package/dist/dashboard/styles.css +1040 -349
- package/dist/egress/capabilities.js +55 -25
- package/dist/egress/capabilities.js.map +1 -1
- package/dist/egress/linuxBootstrap.js +47 -11
- package/dist/egress/linuxBootstrap.js.map +1 -1
- package/dist/egress/run.js +170 -31
- package/dist/egress/run.js.map +1 -1
- package/dist/errors/errorEngine.js +15 -3
- package/dist/errors/errorEngine.js.map +1 -1
- package/dist/errors/providerErrors.js.map +1 -1
- package/dist/evals/evals.js +304 -78
- package/dist/evals/evals.js.map +1 -1
- package/dist/evals/index.d.ts +2 -2
- package/dist/evals/index.js +1 -1
- package/dist/evals/index.js.map +1 -1
- package/dist/evidence/index.d.ts +2 -2
- package/dist/evidence/index.js +1 -1
- package/dist/evidence/index.js.map +1 -1
- package/dist/evidence/report.d.ts +1 -1
- package/dist/evidence/report.js +263 -79
- package/dist/evidence/report.js.map +1 -1
- package/dist/fault/faultLab.js +24 -9
- package/dist/fault/faultLab.js.map +1 -1
- package/dist/index.d.ts +42 -42
- package/dist/index.js +21 -21
- package/dist/index.js.map +1 -1
- package/dist/inventory/analysis.d.ts +1 -1
- package/dist/inventory/analysis.js +174 -50
- package/dist/inventory/analysis.js.map +1 -1
- package/dist/inventory/index.d.ts +4 -4
- package/dist/inventory/index.js +3 -3
- package/dist/inventory/index.js.map +1 -1
- package/dist/inventory/inventoryController.js +362 -115
- package/dist/inventory/inventoryController.js.map +1 -1
- package/dist/inventory/types.js +680 -140
- package/dist/inventory/types.js.map +1 -1
- package/dist/ledger/actionLedger.js +355 -78
- package/dist/ledger/actionLedger.js.map +1 -1
- package/dist/ledger/index.d.ts +2 -2
- package/dist/ledger/index.js +1 -1
- package/dist/ledger/index.js.map +1 -1
- package/dist/mcp/server.js +26 -11
- package/dist/mcp/server.js.map +1 -1
- package/dist/policy/index.d.ts +2 -2
- package/dist/policy/index.js +1 -1
- package/dist/policy/index.js.map +1 -1
- package/dist/policy/policy.js +112 -32
- package/dist/policy/policy.js.map +1 -1
- package/dist/productTelemetry/index.js +71 -20
- package/dist/productTelemetry/index.js.map +1 -1
- package/dist/providers/conformance.js +10 -5
- package/dist/providers/conformance.js.map +1 -1
- package/dist/providers/discord.js +2 -2
- package/dist/providers/generic.js +3 -3
- package/dist/providers/github.js +2 -2
- package/dist/providers/index.d.ts +3 -3
- package/dist/providers/index.js +2 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.js +3 -3
- package/dist/providers/packs/resendPack.js +51 -25
- package/dist/providers/packs/resendPack.js.map +1 -1
- package/dist/providers/packs/stripePack.js +590 -121
- package/dist/providers/packs/stripePack.js.map +1 -1
- package/dist/providers/registry.js +4 -4
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/runtime.js +7 -6
- package/dist/providers/runtime.js.map +1 -1
- package/dist/providers/stripeWebhook.js +39 -10
- package/dist/providers/stripeWebhook.js.map +1 -1
- package/dist/providers/twilio.js +2 -2
- package/dist/proxy/cacheKey.js +9 -2
- package/dist/proxy/cacheKey.js.map +1 -1
- package/dist/proxy/providerDetector.js +22 -9
- package/dist/proxy/providerDetector.js.map +1 -1
- package/dist/proxy/proxyHandler.js +57 -22
- package/dist/proxy/proxyHandler.js.map +1 -1
- package/dist/proxy/requestNormalizer.js +4 -2
- package/dist/proxy/requestNormalizer.js.map +1 -1
- package/dist/reliability/costGovernance.js +211 -49
- package/dist/reliability/costGovernance.js.map +1 -1
- package/dist/reliability/index.d.ts +8 -8
- package/dist/reliability/index.js +4 -4
- package/dist/reliability/index.js.map +1 -1
- package/dist/reliability/reconciliation.js +274 -65
- package/dist/reliability/reconciliation.js.map +1 -1
- package/dist/reliability/runtimeHealth.js +127 -35
- package/dist/reliability/runtimeHealth.js.map +1 -1
- package/dist/reliability/slo.js +257 -50
- package/dist/reliability/slo.js.map +1 -1
- package/dist/report/safetyReport.d.ts +6 -0
- package/dist/report/safetyReport.js +56 -12
- package/dist/report/safetyReport.js.map +1 -1
- package/dist/rules/aiRules.js +5 -5
- package/dist/rules/aiRules.js.map +1 -1
- package/dist/safety/index.d.ts +2 -2
- package/dist/safety/index.js +1 -1
- package/dist/safety/index.js.map +1 -1
- package/dist/safety/safetyController.js +675 -165
- package/dist/safety/safetyController.js.map +1 -1
- package/dist/scenarios/scenarioBundle.js +204 -71
- package/dist/scenarios/scenarioBundle.js.map +1 -1
- package/dist/scenarios/scenarioStore.js +88 -29
- package/dist/scenarios/scenarioStore.js.map +1 -1
- package/dist/security/headerSanitizer.js +52 -10
- package/dist/security/headerSanitizer.js.map +1 -1
- package/dist/security/secrets.js +10 -5
- package/dist/security/secrets.js.map +1 -1
- package/dist/server/accessControl.js +37 -10
- package/dist/server/accessControl.js.map +1 -1
- package/dist/server/createServer.js +1 -1
- package/dist/server/createServer.js.map +1 -1
- package/dist/server/eventsStore.js +15 -5
- package/dist/server/eventsStore.js.map +1 -1
- package/dist/server/routes.js +61 -19
- package/dist/server/routes.js.map +1 -1
- package/dist/server/sse.js +1 -1
- package/dist/server/sse.js.map +1 -1
- package/dist/setup/sdkDetector.js +7 -2
- package/dist/setup/sdkDetector.js.map +1 -1
- package/dist/setup/setupGenerator.js +136 -48
- package/dist/setup/setupGenerator.js.map +1 -1
- package/dist/state/stateExtractor.js +3 -1
- package/dist/state/stateExtractor.js.map +1 -1
- package/dist/state/stateResolver.js +10 -7
- package/dist/state/stateResolver.js.map +1 -1
- package/dist/state/stateStore.js +13 -4
- package/dist/state/stateStore.js.map +1 -1
- package/dist/storage/fileStore.js +12 -4
- package/dist/storage/fileStore.js.map +1 -1
- package/dist/teamControl/controlPlane.js +846 -174
- package/dist/teamControl/controlPlane.js.map +1 -1
- package/dist/teamControl/deployment.js +19 -5
- package/dist/teamControl/deployment.js.map +1 -1
- package/dist/teamControl/index.d.ts +4 -4
- package/dist/teamControl/index.js +2 -2
- package/dist/teamControl/index.js.map +1 -1
- package/dist/tests/testGenerator.js +15 -7
- package/dist/tests/testGenerator.js.map +1 -1
- package/dist/trust/index.d.ts +2 -2
- package/dist/trust/index.js +1 -1
- package/dist/trust/index.js.map +1 -1
- package/dist/trust/trustLadder.js +309 -63
- package/dist/trust/trustLadder.js.map +1 -1
- package/dist/worlds/index.d.ts +2 -2
- package/dist/worlds/index.js +1 -1
- package/dist/worlds/index.js.map +1 -1
- package/dist/worlds/worlds.js +364 -65
- package/dist/worlds/worlds.js.map +1 -1
- package/docs/assets/dashboard.png +0 -0
- package/docs/ci.md +49 -49
- package/docs/github-actions.md +84 -84
- package/docs/mcp.md +58 -52
- package/docs/policy.md +63 -61
- package/docs/providers/authoring-packs.md +180 -175
- package/docs/providers/stripe-core-pack.md +84 -82
- package/docs/releases/README.md +13 -0
- package/docs/security/action-gateway-threat-model.md +34 -34
- package/docs/security/action-ledger-incident-replay-threat-model.md +42 -42
- package/docs/security/approval-inbox-threat-model.md +35 -35
- package/docs/security/credential-broker-threat-model.md +52 -52
- package/docs/security/egress-threat-model.md +126 -126
- package/docs/security/inventory-threat-model.md +104 -104
- package/docs/security/kill-switch-budgets-threat-model.md +37 -37
- package/docs/security/reliability-threat-model.md +111 -111
- package/docs/security/trust-ladder-threat-model.md +33 -33
- package/docs/telemetry.md +14 -0
- package/docs/usage.md +680 -571
- package/examples/README.md +10 -10
- package/examples/agent-instructions/README.md +39 -39
- package/examples/ci-smoke/README.md +21 -21
- package/examples/ci-smoke/ghostapi.policy.yaml +22 -22
- package/examples/ci-smoke/package.json +9 -9
- package/examples/ci-smoke/production-egress.mjs +4 -2
- package/examples/ci-smoke/safe.mjs +21 -17
- package/examples/evals/README.md +24 -24
- package/examples/evals/retry-after.eval.json +71 -52
- package/examples/generic-rest/README.md +27 -27
- package/examples/github-fetch/README.md +38 -35
- package/examples/openai-streaming/README.md +16 -16
- package/examples/openai-streaming/streaming-tool-call.mjs +44 -31
- package/examples/policy/ghostapi.policy.yaml +27 -27
- package/examples/record-replay/README.md +19 -19
- package/examples/record-replay/replay-requests.json +22 -22
- package/examples/record-replay/stripe-checkout.har.json +30 -30
- package/examples/resend-node/README.md +22 -22
- package/examples/stripe-node/README.md +35 -35
- package/examples/stripe-node/checkout-flow.mjs +87 -61
- package/examples/twilio-node/README.md +21 -21
- package/examples/worlds/README.md +19 -19
- package/examples/worlds/subscription-recovery.mjs +21 -12
- package/package.json +28 -6
- package/dist/landing/assets/index-BXR41fAD.css +0 -1
- package/dist/landing/assets/index-CxWsopzP.css +0 -1
- package/dist/landing/assets/index-D8_ggMRE.js +0 -55
- package/dist/landing/assets/index-DZlpyhqd.js +0 -55
- package/dist/landing/assets/index-Dk-Xezs3.js +0 -55
- package/dist/landing/index.html +0 -15
- package/dist/landing/landing.d.ts +0 -3
- package/dist/landing/landing.js +0 -12
- package/dist/landing/landing.js.map +0 -1
- package/docs/commercial/README.md +0 -29
- package/docs/commercial/data-inventory.md +0 -36
- package/docs/commercial/manual-invoicing.md +0 -44
- package/docs/commercial/metrics.md +0 -51
- package/docs/commercial/pricing.md +0 -103
- package/docs/commercial/privacy-policy-draft.md +0 -41
- package/docs/commercial/terms-topics-for-counsel.md +0 -51
- package/docs/design-partners/README.md +0 -52
- package/docs/design-partners/demo-narratives.md +0 -33
- package/docs/design-partners/design-partner-offer.md +0 -39
- package/docs/design-partners/discovery-questions.md +0 -28
- package/docs/design-partners/feedback-capture-template.md +0 -48
- package/docs/design-partners/icp-one-pager.md +0 -43
- package/docs/design-partners/onboarding-checklist.md +0 -33
- package/docs/design-partners/pilot-success-criteria.md +0 -27
- package/docs/design-partners/pricing-interview-script.md +0 -18
- package/docs/design-partners/telemetry-plan.md +0 -48
- package/docs/development/baseline.md +0 -114
- package/docs/development/onboarding-smoke.md +0 -37
- package/docs/development/verification-0.1.8.md +0 -25
- package/docs/enterprise-product-roadmap-ru.md +0 -1879
- package/docs/fundraising/README.md +0 -87
- package/docs/fundraising/data-room-checklist.md +0 -72
- package/docs/fundraising/demo-script.md +0 -52
- package/docs/fundraising/design-partners-50.md +0 -74
- package/docs/fundraising/launch-posts.md +0 -37
- package/docs/fundraising/metrics-and-evidence.md +0 -30
- package/docs/fundraising/roadmap-12-month.md +0 -25
- package/docs/fundraising/technical-due-diligence-index.md +0 -37
- package/docs/fundraising/yc-application.md +0 -25
- package/docs/hosted-pilot.md +0 -111
- package/docs/operations/disaster-recovery-runbook.md +0 -81
- package/docs/operations/kill-switch-runbook.md +0 -32
- package/docs/release-checklist.md +0 -83
- package/docs/release-migration-and-rollback.md +0 -63
- package/docs/release-readiness.md +0 -39
- 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 = [
|
|
19
|
-
|
|
20
|
-
|
|
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 = [
|
|
44
|
+
const DEVELOPER_PERMISSIONS = [
|
|
45
|
+
...READ_PERMISSIONS,
|
|
46
|
+
...PUBLISH_PERMISSIONS,
|
|
47
|
+
];
|
|
23
48
|
const VIEWER_PERMISSIONS = [...READ_PERMISSIONS];
|
|
24
|
-
const SERVICE_ACCOUNT_PERMISSIONS = [
|
|
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({
|
|
60
|
-
|
|
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" &&
|
|
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 &&
|
|
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 = {
|
|
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 &&
|
|
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 = {
|
|
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 &&
|
|
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 &&
|
|
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 &&
|
|
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 = {
|
|
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 &&
|
|
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 = {
|
|
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
|
|
144
|
-
|
|
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, {
|
|
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, {
|
|
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 &&
|
|
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" &&
|
|
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
|
|
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, {
|
|
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 &&
|
|
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" &&
|
|
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 ||
|
|
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, {
|
|
208
|
-
|
|
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 &&
|
|
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({
|
|
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 &&
|
|
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 = {
|
|
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 &&
|
|
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,
|
|
250
|
-
|
|
251
|
-
|
|
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 &&
|
|
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 &&
|
|
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 = {
|
|
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
|
|
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 &&
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
330
|
-
|
|
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 {
|
|
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 &&
|
|
350
|
-
|
|
351
|
-
state.
|
|
352
|
-
|
|
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 ||
|
|
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 {
|
|
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, [
|
|
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, [
|
|
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" &&
|
|
650
|
+
if (entry.role !== "owner" &&
|
|
651
|
+
entry.role !== "admin" &&
|
|
652
|
+
entry.role !== "member")
|
|
427
653
|
throw new TeamControlPlaneError("Member is invalid.");
|
|
428
|
-
return {
|
|
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, [
|
|
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({
|
|
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 {
|
|
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, [
|
|
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 {
|
|
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 &&
|
|
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) => [
|
|
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 ||
|
|
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 ||
|
|
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 {
|
|
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) ||
|
|
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 {
|
|
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) ||
|
|
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 {
|
|
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 {
|
|
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) ||
|
|
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 {
|
|
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, [
|
|
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) ||
|
|
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" &&
|
|
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"
|
|
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"
|
|
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() >
|
|
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
|
|
560
|
-
|
|
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 {
|
|
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, [
|
|
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) ||
|
|
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 {
|
|
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, [
|
|
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) ||
|
|
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 {
|
|
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, [
|
|
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) ||
|
|
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 {
|
|
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) ||
|
|
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, [
|
|
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) ||
|
|
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 {
|
|
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) ||
|
|
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 {
|
|
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) &&
|
|
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" &&
|
|
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 &&
|
|
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 &&
|
|
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 &&
|
|
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 &&
|
|
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({
|
|
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 = {
|
|
703
|
-
|
|
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 {
|
|
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 ||
|
|
713
|
-
|
|
714
|
-
|
|
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
|
|
736
|
-
|
|
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
|
|
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) ||
|
|
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" ||
|
|
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) ||
|
|
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 &&
|
|
789
|
-
|
|
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)
|
|
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([
|
|
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) ||
|
|
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([
|
|
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 ||
|
|
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) {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
function
|
|
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)
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
function
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
function
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
function
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
throw new TeamControlPlaneError(`${label} is
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
function
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|