@yiaany/ghostapi 0.1.7 → 0.1.8
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 +42 -2
- package/CONTRIBUTING.md +119 -117
- package/LICENSE +21 -21
- package/README.md +208 -185
- package/SECURITY.md +53 -37
- package/dist/actions/gateway.d.ts +179 -0
- package/dist/actions/gateway.js +514 -0
- package/dist/actions/gateway.js.map +1 -0
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.js +2 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/ai/aiClient.js +4 -4
- package/dist/ai/aiClient.js.map +1 -1
- package/dist/ai/aiGenerator.d.ts +2 -2
- package/dist/ai/aiGenerator.js +21 -72
- package/dist/ai/aiGenerator.js.map +1 -1
- package/dist/ai/prompts.js +51 -51
- package/dist/ai/prompts.js.map +1 -1
- package/dist/approvals/approvalInbox.d.ts +150 -0
- package/dist/approvals/approvalInbox.js +412 -0
- package/dist/approvals/approvalInbox.js.map +1 -0
- package/dist/approvals/index.d.ts +2 -0
- package/dist/approvals/index.js +2 -0
- package/dist/approvals/index.js.map +1 -0
- package/dist/behavior/behaviorStore.d.ts +2 -0
- package/dist/behavior/behaviorStore.js +50 -46
- package/dist/behavior/behaviorStore.js.map +1 -1
- package/dist/cache/index.d.ts +2 -0
- package/dist/cache/index.js +58 -14
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/index.js +438 -58
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/openUrl.d.ts +10 -0
- package/dist/cli/openUrl.js +31 -0
- package/dist/cli/openUrl.js.map +1 -0
- package/dist/cli/parser.d.ts +155 -0
- package/dist/cli/parser.js +504 -2
- package/dist/cli/parser.js.map +1 -1
- package/dist/config/dataPaths.d.ts +31 -0
- package/dist/config/dataPaths.js +45 -0
- package/dist/config/dataPaths.js.map +1 -0
- package/dist/config/localConfig.d.ts +1 -2
- package/dist/config/localConfig.js +27 -21
- package/dist/config/localConfig.js.map +1 -1
- package/dist/config/serverConfig.d.ts +2 -0
- package/dist/config/serverConfig.js +8 -1
- package/dist/config/serverConfig.js.map +1 -1
- package/dist/contracts/contract.d.ts +76 -0
- package/dist/contracts/contract.js +603 -0
- package/dist/contracts/contract.js.map +1 -0
- package/dist/contracts/index.d.ts +2 -0
- package/dist/contracts/index.js +2 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/credentials/credentialBroker.d.ts +207 -0
- package/dist/credentials/credentialBroker.js +588 -0
- package/dist/credentials/credentialBroker.js.map +1 -0
- package/dist/credentials/index.d.ts +2 -0
- package/dist/credentials/index.js +2 -0
- package/dist/credentials/index.js.map +1 -0
- package/dist/dashboard/app.js +685 -615
- package/dist/dashboard/index.html +13 -7
- package/dist/dashboard/styles.css +349 -349
- package/dist/egress/capabilities.d.ts +32 -0
- package/dist/egress/capabilities.js +165 -0
- package/dist/egress/capabilities.js.map +1 -0
- package/dist/egress/linuxBootstrap.d.ts +1 -0
- package/dist/egress/linuxBootstrap.js +121 -0
- package/dist/egress/linuxBootstrap.js.map +1 -0
- package/dist/egress/run.d.ts +18 -0
- package/dist/egress/run.js +305 -0
- package/dist/egress/run.js.map +1 -0
- package/dist/errors/errorEngine.d.ts +2 -2
- package/dist/errors/errorEngine.js +3 -40
- package/dist/errors/errorEngine.js.map +1 -1
- package/dist/evals/evals.d.ts +126 -0
- package/dist/evals/evals.js +466 -0
- package/dist/evals/evals.js.map +1 -0
- package/dist/evals/index.d.ts +2 -0
- package/dist/evals/index.js +2 -0
- package/dist/evals/index.js.map +1 -0
- package/dist/evidence/index.d.ts +2 -0
- package/dist/evidence/index.js +2 -0
- package/dist/evidence/index.js.map +1 -0
- package/dist/evidence/report.d.ts +143 -0
- package/dist/evidence/report.js +556 -0
- package/dist/evidence/report.js.map +1 -0
- package/dist/fault/faultLab.d.ts +5 -4
- package/dist/fault/faultLab.js +66 -24
- package/dist/fault/faultLab.js.map +1 -1
- package/dist/index.d.ts +46 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -1
- package/dist/inventory/analysis.d.ts +12 -0
- package/dist/inventory/analysis.js +457 -0
- package/dist/inventory/analysis.js.map +1 -0
- package/dist/inventory/index.d.ts +5 -0
- package/dist/inventory/index.js +4 -0
- package/dist/inventory/index.js.map +1 -0
- package/dist/inventory/inventoryController.d.ts +53 -0
- package/dist/inventory/inventoryController.js +1044 -0
- package/dist/inventory/inventoryController.js.map +1 -0
- package/dist/inventory/types.d.ts +579 -0
- package/dist/inventory/types.js +787 -0
- package/dist/inventory/types.js.map +1 -0
- package/dist/landing/assets/index-CxWsopzP.css +1 -0
- package/dist/landing/assets/index-DZlpyhqd.js +55 -0
- package/dist/landing/assets/index-Dk-Xezs3.js +55 -0
- package/dist/landing/index.html +15 -15
- package/dist/ledger/actionLedger.d.ts +125 -0
- package/dist/ledger/actionLedger.js +594 -0
- package/dist/ledger/actionLedger.js.map +1 -0
- package/dist/ledger/index.d.ts +2 -0
- package/dist/ledger/index.js +2 -0
- package/dist/ledger/index.js.map +1 -0
- package/dist/mcp/server.js +4 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/policy/index.d.ts +3 -0
- package/dist/policy/index.js +2 -0
- package/dist/policy/index.js.map +1 -0
- package/dist/policy/policy.d.ts +14 -0
- package/dist/policy/policy.js +282 -0
- package/dist/policy/policy.js.map +1 -0
- package/dist/policy/types.d.ts +52 -0
- package/dist/policy/types.js +2 -0
- package/dist/policy/types.js.map +1 -0
- package/dist/productTelemetry/index.d.ts +16 -0
- package/dist/productTelemetry/index.js +142 -0
- package/dist/productTelemetry/index.js.map +1 -0
- package/dist/providers/conformance.d.ts +7 -0
- package/dist/providers/conformance.js +48 -0
- package/dist/providers/conformance.js.map +1 -0
- package/dist/providers/index.d.ts +4 -2
- package/dist/providers/index.js +3 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/packs/resendPack.d.ts +2 -0
- package/dist/providers/packs/resendPack.js +147 -0
- package/dist/providers/packs/resendPack.js.map +1 -0
- package/dist/providers/packs/stripePack.d.ts +2 -0
- package/dist/providers/packs/stripePack.js +852 -0
- package/dist/providers/packs/stripePack.js.map +1 -0
- package/dist/providers/registry.d.ts +7 -3
- package/dist/providers/registry.js +55 -4
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/resend.js +2 -11
- package/dist/providers/resend.js.map +1 -1
- package/dist/providers/runtime.d.ts +8 -0
- package/dist/providers/runtime.js +48 -0
- package/dist/providers/runtime.js.map +1 -0
- package/dist/providers/stripeWebhook.d.ts +13 -0
- package/dist/providers/stripeWebhook.js +50 -0
- package/dist/providers/stripeWebhook.js.map +1 -0
- package/dist/providers/types.d.ts +153 -0
- package/dist/proxy/cacheKey.js +3 -3
- package/dist/proxy/cacheKey.js.map +1 -1
- package/dist/proxy/providerDetector.d.ts +2 -7
- package/dist/proxy/providerDetector.js +4 -4
- package/dist/proxy/providerDetector.js.map +1 -1
- package/dist/proxy/proxyHandler.js +116 -12
- package/dist/proxy/proxyHandler.js.map +1 -1
- package/dist/proxy/requestNormalizer.d.ts +0 -1
- package/dist/proxy/requestNormalizer.js +2 -5
- package/dist/proxy/requestNormalizer.js.map +1 -1
- package/dist/reliability/costGovernance.d.ts +175 -0
- package/dist/reliability/costGovernance.js +515 -0
- package/dist/reliability/costGovernance.js.map +1 -0
- package/dist/reliability/index.d.ts +8 -0
- package/dist/reliability/index.js +5 -0
- package/dist/reliability/index.js.map +1 -0
- package/dist/reliability/reconciliation.d.ts +157 -0
- package/dist/reliability/reconciliation.js +522 -0
- package/dist/reliability/reconciliation.js.map +1 -0
- package/dist/reliability/runtimeHealth.d.ts +60 -0
- package/dist/reliability/runtimeHealth.js +356 -0
- package/dist/reliability/runtimeHealth.js.map +1 -0
- package/dist/reliability/slo.d.ts +131 -0
- package/dist/reliability/slo.js +375 -0
- package/dist/reliability/slo.js.map +1 -0
- package/dist/report/safetyReport.js +7 -3
- package/dist/report/safetyReport.js.map +1 -1
- package/dist/safety/index.d.ts +2 -0
- package/dist/safety/index.js +2 -0
- package/dist/safety/index.js.map +1 -0
- package/dist/safety/safetyController.d.ts +187 -0
- package/dist/safety/safetyController.js +477 -0
- package/dist/safety/safetyController.js.map +1 -0
- package/dist/scenarios/scenarioBundle.d.ts +75 -0
- package/dist/scenarios/scenarioBundle.js +750 -0
- package/dist/scenarios/scenarioBundle.js.map +1 -0
- package/dist/scenarios/scenarioStore.d.ts +1 -0
- package/dist/scenarios/scenarioStore.js +47 -32
- package/dist/scenarios/scenarioStore.js.map +1 -1
- package/dist/security/headerSanitizer.d.ts +2 -0
- package/dist/security/headerSanitizer.js +36 -1
- package/dist/security/headerSanitizer.js.map +1 -1
- package/dist/security/secrets.js +24 -4
- package/dist/security/secrets.js.map +1 -1
- package/dist/server/accessControl.d.ts +5 -0
- package/dist/server/accessControl.js +100 -0
- package/dist/server/accessControl.js.map +1 -0
- package/dist/server/createServer.d.ts +0 -5
- package/dist/server/createServer.js +9 -29
- package/dist/server/createServer.js.map +1 -1
- package/dist/server/eventsStore.d.ts +6 -1
- package/dist/server/eventsStore.js +83 -15
- package/dist/server/eventsStore.js.map +1 -1
- package/dist/server/routes.js +36 -10
- package/dist/server/routes.js.map +1 -1
- package/dist/server/sse.d.ts +2 -1
- package/dist/server/sse.js +15 -3
- package/dist/server/sse.js.map +1 -1
- package/dist/setup/setupGenerator.js +53 -6
- package/dist/setup/setupGenerator.js.map +1 -1
- package/dist/state/stateStore.d.ts +5 -0
- package/dist/state/stateStore.js +48 -62
- package/dist/state/stateStore.js.map +1 -1
- package/dist/storage/fileStore.d.ts +10 -0
- package/dist/storage/fileStore.js +131 -0
- package/dist/storage/fileStore.js.map +1 -0
- package/dist/teamControl/controlPlane.d.ts +255 -0
- package/dist/teamControl/controlPlane.js +918 -0
- package/dist/teamControl/controlPlane.js.map +1 -0
- package/dist/teamControl/deployment.d.ts +33 -0
- package/dist/teamControl/deployment.js +67 -0
- package/dist/teamControl/deployment.js.map +1 -0
- package/dist/teamControl/index.d.ts +4 -0
- package/dist/teamControl/index.js +3 -0
- package/dist/teamControl/index.js.map +1 -0
- package/dist/tests/testGenerator.js +4 -1
- package/dist/tests/testGenerator.js.map +1 -1
- package/dist/trust/index.d.ts +2 -0
- package/dist/trust/index.js +2 -0
- package/dist/trust/index.js.map +1 -0
- package/dist/trust/trustLadder.d.ts +182 -0
- package/dist/trust/trustLadder.js +560 -0
- package/dist/trust/trustLadder.js.map +1 -0
- package/dist/worlds/index.d.ts +2 -0
- package/dist/worlds/index.js +2 -0
- package/dist/worlds/index.js.map +1 -0
- package/dist/worlds/worlds.d.ts +160 -0
- package/dist/worlds/worlds.js +491 -0
- package/dist/worlds/worlds.js.map +1 -0
- package/docs/assets/dashboard.png +0 -0
- package/docs/ci.md +49 -0
- package/docs/commercial/README.md +29 -0
- package/docs/commercial/data-inventory.md +36 -0
- package/docs/commercial/manual-invoicing.md +44 -0
- package/docs/commercial/metrics.md +51 -0
- package/docs/commercial/pricing.md +103 -0
- package/docs/commercial/privacy-policy-draft.md +41 -0
- package/docs/commercial/terms-topics-for-counsel.md +51 -0
- package/docs/design-partners/README.md +52 -0
- package/docs/design-partners/demo-narratives.md +33 -0
- package/docs/design-partners/design-partner-offer.md +39 -0
- package/docs/design-partners/discovery-questions.md +28 -0
- package/docs/design-partners/feedback-capture-template.md +48 -0
- package/docs/design-partners/icp-one-pager.md +43 -0
- package/docs/design-partners/onboarding-checklist.md +33 -0
- package/docs/design-partners/pilot-success-criteria.md +27 -0
- package/docs/design-partners/pricing-interview-script.md +18 -0
- package/docs/design-partners/telemetry-plan.md +48 -0
- package/docs/development/baseline.md +114 -0
- package/docs/development/onboarding-smoke.md +37 -0
- package/docs/development/verification-0.1.8.md +25 -0
- package/docs/enterprise-product-roadmap-ru.md +1879 -0
- package/docs/fundraising/README.md +87 -0
- package/docs/fundraising/data-room-checklist.md +72 -0
- package/docs/fundraising/demo-script.md +52 -0
- package/docs/fundraising/design-partners-50.md +74 -0
- package/docs/fundraising/launch-posts.md +37 -0
- package/docs/fundraising/metrics-and-evidence.md +30 -0
- package/docs/fundraising/roadmap-12-month.md +25 -0
- package/docs/fundraising/technical-due-diligence-index.md +37 -0
- package/docs/fundraising/yc-application.md +25 -0
- package/docs/github-actions.md +84 -0
- package/docs/hosted-pilot.md +111 -0
- package/docs/mcp.md +52 -50
- package/docs/operations/disaster-recovery-runbook.md +81 -0
- package/docs/operations/kill-switch-runbook.md +32 -0
- package/docs/policy.md +61 -0
- package/docs/providers/authoring-packs.md +175 -0
- package/docs/providers/stripe-core-pack.md +82 -0
- package/docs/release-checklist.md +83 -71
- package/docs/release-migration-and-rollback.md +63 -0
- package/docs/release-readiness.md +39 -0
- package/docs/security/action-gateway-threat-model.md +34 -0
- package/docs/security/action-ledger-incident-replay-threat-model.md +42 -0
- package/docs/security/approval-inbox-threat-model.md +35 -0
- package/docs/security/credential-broker-threat-model.md +52 -0
- package/docs/security/egress-threat-model.md +126 -0
- package/docs/security/inventory-threat-model.md +104 -0
- package/docs/security/kill-switch-budgets-threat-model.md +37 -0
- package/docs/security/reliability-threat-model.md +111 -0
- package/docs/security/trust-ladder-threat-model.md +33 -0
- package/docs/team-control-plane.md +72 -0
- package/docs/usage.md +571 -57
- package/examples/README.md +10 -0
- package/examples/agent-instructions/README.md +39 -39
- package/examples/ci-smoke/README.md +21 -0
- package/examples/ci-smoke/ghostapi.policy.yaml +22 -0
- package/examples/ci-smoke/package.json +9 -0
- package/examples/ci-smoke/production-egress.mjs +2 -0
- package/examples/ci-smoke/safe.mjs +17 -0
- package/examples/evals/README.md +24 -0
- package/examples/evals/retry-after.eval.json +52 -0
- package/examples/generic-rest/README.md +27 -27
- package/examples/github-fetch/README.md +35 -35
- package/examples/openai-streaming/README.md +16 -0
- package/examples/openai-streaming/streaming-tool-call.mjs +31 -0
- package/examples/policy/ghostapi.policy.yaml +27 -0
- package/examples/record-replay/README.md +19 -0
- package/examples/record-replay/replay-requests.json +22 -0
- package/examples/record-replay/stripe-checkout.har.json +30 -0
- package/examples/resend-node/README.md +22 -22
- package/examples/stripe-node/README.md +35 -29
- package/examples/stripe-node/checkout-flow.mjs +61 -0
- package/examples/twilio-node/README.md +21 -21
- package/examples/worlds/README.md +19 -0
- package/examples/worlds/subscription-recovery.mjs +12 -0
- package/package.json +27 -12
- package/dist/providers/stripe.d.ts +0 -2
- package/dist/providers/stripe.js +0 -15
- package/dist/providers/stripe.js.map +0 -1
- package/docs/assets/ghostapi-avatar.png +0 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { getDataPaths } from "../config/dataPaths.js";
|
|
5
|
+
import { sanitizeSecretString } from "../security/secrets.js";
|
|
6
|
+
import { atomicWriteJson, ensurePrivateDirectory, withFileLock } from "../storage/fileStore.js";
|
|
7
|
+
const SCHEMA_VERSION = 1;
|
|
8
|
+
const MAX_STORE_BYTES = 1024 * 1024;
|
|
9
|
+
const MAX_CREDENTIALS = 1_000;
|
|
10
|
+
const MAX_GRANTS = 10_000;
|
|
11
|
+
const MAX_RECEIPTS = 10_000;
|
|
12
|
+
const MAX_SCOPES = 16;
|
|
13
|
+
const MAX_GRANT_TTL_MS = 15 * 60 * 1000;
|
|
14
|
+
const MAX_BREAK_GLASS_TTL_MS = 5 * 60 * 1000;
|
|
15
|
+
const IDENTIFIER = /^[a-z0-9][a-z0-9._-]{0,127}$/;
|
|
16
|
+
const REFERENCE = /^[a-z0-9][a-z0-9._:/-]{0,255}$/;
|
|
17
|
+
const HASH = /^[a-f0-9]{64}$/;
|
|
18
|
+
export class CredentialBrokerError extends Error {
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "CredentialBrokerError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function createCredentialBroker(options) {
|
|
25
|
+
return new CredentialBroker(options);
|
|
26
|
+
}
|
|
27
|
+
export class CredentialBroker {
|
|
28
|
+
path;
|
|
29
|
+
now;
|
|
30
|
+
vault;
|
|
31
|
+
executor;
|
|
32
|
+
workloadVerifier;
|
|
33
|
+
actionReceiptVerifier;
|
|
34
|
+
breakGlassAuthorizer;
|
|
35
|
+
constructor(options) {
|
|
36
|
+
this.path = options.path ?? getDataPaths().credentialBroker;
|
|
37
|
+
this.now = options.now ?? (() => new Date());
|
|
38
|
+
this.vault = options.vault;
|
|
39
|
+
this.executor = options.executor;
|
|
40
|
+
this.workloadVerifier = options.workloadVerifier;
|
|
41
|
+
this.actionReceiptVerifier = options.actionReceiptVerifier;
|
|
42
|
+
this.breakGlassAuthorizer = options.breakGlassAuthorizer ?? createDisabledBreakGlassAuthorizer();
|
|
43
|
+
}
|
|
44
|
+
async registerCredential(input) {
|
|
45
|
+
const candidate = validateCredentialMetadata({ ...input, version: 1, createdAt: this.timestamp(), expiresAt: input.expiresAt });
|
|
46
|
+
return this.mutate((state) => {
|
|
47
|
+
if (state.credentials.some((credential) => credential.tenantId === candidate.tenantId && credential.id === candidate.id))
|
|
48
|
+
throw new CredentialBrokerError("Credential already exists.");
|
|
49
|
+
state.credentials.push(candidate);
|
|
50
|
+
return clone(candidate);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async rotateCredential(input) {
|
|
54
|
+
const tenantId = identifier(input.tenantId, "Credential tenant id");
|
|
55
|
+
const credentialId = identifier(input.credentialId, "Credential id");
|
|
56
|
+
const vaultRef = reference(input.vaultRef, "Credential vault reference");
|
|
57
|
+
const expiresAt = futureTimestamp(input.expiresAt, this.timestamp(), "Credential expiry");
|
|
58
|
+
return this.mutate((state, now) => {
|
|
59
|
+
const credential = findCredential(state, tenantId, credentialId);
|
|
60
|
+
if (credential.revokedAt !== undefined)
|
|
61
|
+
throw new CredentialBrokerError("Credential is revoked.");
|
|
62
|
+
credential.vaultRef = vaultRef;
|
|
63
|
+
credential.expiresAt = expiresAt;
|
|
64
|
+
credential.version += 1;
|
|
65
|
+
credential.rotatedAt = now;
|
|
66
|
+
for (const grant of state.grants)
|
|
67
|
+
if (grant.tenantId === tenantId && grant.credentialId === credentialId && grant.revokedAt === undefined)
|
|
68
|
+
grant.revokedAt = now;
|
|
69
|
+
return clone(credential);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async revokeCredential(tenantIdValue, credentialIdValue) {
|
|
73
|
+
const tenantId = identifier(tenantIdValue, "Credential tenant id");
|
|
74
|
+
const credentialId = identifier(credentialIdValue, "Credential id");
|
|
75
|
+
await this.mutate((state, now) => {
|
|
76
|
+
const credential = findCredential(state, tenantId, credentialId);
|
|
77
|
+
if (credential.revokedAt === undefined)
|
|
78
|
+
credential.revokedAt = now;
|
|
79
|
+
for (const grant of state.grants)
|
|
80
|
+
if (grant.tenantId === tenantId && grant.credentialId === credentialId && grant.revokedAt === undefined)
|
|
81
|
+
grant.revokedAt = now;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async issueGrant(input) {
|
|
85
|
+
const workload = await this.authenticateWorkload(input.identity);
|
|
86
|
+
const request = validateAccessRequest(input.request);
|
|
87
|
+
const expiresAt = futureTimestamp(input.expiresAt, this.timestamp(), "Credential grant expiry");
|
|
88
|
+
return this.mutate(async (state, now) => {
|
|
89
|
+
const credential = findCredential(state, workload.tenantId, request.credentialId);
|
|
90
|
+
assertCredentialUsable(credential, now);
|
|
91
|
+
assertRequestMatchesCredential(request, credential, workload);
|
|
92
|
+
const normalOwnerMatch = credential.ownerWorkloadId === workload.workloadId && credential.ownerWorkloadKind === workload.workloadKind;
|
|
93
|
+
const breakGlass = normalOwnerMatch ? undefined : await this.breakGlassAuthorizer.authorize(input.breakGlassApproval, workload, request);
|
|
94
|
+
if (breakGlass !== undefined && Date.parse(expiresAt) - Date.parse(now) > MAX_BREAK_GLASS_TTL_MS)
|
|
95
|
+
throw new CredentialBrokerError("Break-glass grants must expire within five minutes.");
|
|
96
|
+
if (Date.parse(expiresAt) - Date.parse(now) > MAX_GRANT_TTL_MS)
|
|
97
|
+
throw new CredentialBrokerError("Credential grants must expire within fifteen minutes.");
|
|
98
|
+
if (Date.parse(expiresAt) > Date.parse(credential.expiresAt) || Date.parse(expiresAt) > Date.parse(workload.expiresAt))
|
|
99
|
+
throw new CredentialBrokerError("Credential grant expiry exceeds its credential or workload lifetime.");
|
|
100
|
+
const grant = {
|
|
101
|
+
id: identifier(`grant-${randomUUID().replace(/-/g, "")}`, "Credential grant id"),
|
|
102
|
+
credentialId: credential.id,
|
|
103
|
+
credentialVersion: credential.version,
|
|
104
|
+
tenantId: credential.tenantId,
|
|
105
|
+
projectId: credential.projectId,
|
|
106
|
+
environment: credential.environment,
|
|
107
|
+
workloadId: workload.workloadId,
|
|
108
|
+
subjectId: workload.subjectId,
|
|
109
|
+
workloadKind: workload.workloadKind,
|
|
110
|
+
runId: workload.runId,
|
|
111
|
+
provider: credential.provider,
|
|
112
|
+
scopes: [...request.scopes],
|
|
113
|
+
audience: "ghostapi-server",
|
|
114
|
+
action: clone(request.action),
|
|
115
|
+
issuedAt: now,
|
|
116
|
+
expiresAt,
|
|
117
|
+
...(breakGlass === undefined ? {} : { breakGlass })
|
|
118
|
+
};
|
|
119
|
+
state.grants.push(grant);
|
|
120
|
+
return clone(grant);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
async executeServerSide(input) {
|
|
124
|
+
const workload = await this.authenticateWorkload(input.identity);
|
|
125
|
+
const grantId = identifier(input.grantId, "Credential grant id");
|
|
126
|
+
const request = validateAccessRequest(input.request);
|
|
127
|
+
const started = await this.mutate(async (state, now) => {
|
|
128
|
+
const grant = state.grants.find((candidate) => candidate.id === grantId);
|
|
129
|
+
if (grant === undefined)
|
|
130
|
+
throw new CredentialBrokerError("Credential grant was not found.");
|
|
131
|
+
assertGrantUsable(grant, workload, request, now);
|
|
132
|
+
const credential = findCredential(state, workload.tenantId, grant.credentialId);
|
|
133
|
+
assertCredentialUsable(credential, now);
|
|
134
|
+
if (credential.version !== grant.credentialVersion)
|
|
135
|
+
throw new CredentialBrokerError("Credential grant was invalidated by rotation.");
|
|
136
|
+
if (credential.provider !== this.executor.provider || !grant.scopes.every((scope) => this.executor.supportsScope(scope)))
|
|
137
|
+
throw new CredentialBrokerError("Credential executor denies the requested scope.");
|
|
138
|
+
const prior = state.receipts.find((receipt) => receipt.grantId === grant.id);
|
|
139
|
+
if (prior !== undefined) {
|
|
140
|
+
if (prior.status === "executed")
|
|
141
|
+
return clone(prior);
|
|
142
|
+
throw new CredentialBrokerError("Prior credential execution has no retry-safe outcome and will not retry automatically.");
|
|
143
|
+
}
|
|
144
|
+
await this.actionReceiptVerifier.verify(request.action, workload);
|
|
145
|
+
const receipt = { id: identifier(`credential-use-${randomUUID().replace(/-/g, "")}`, "Credential use receipt id"), grantId: grant.id, credentialId: credential.id, credentialVersion: credential.version, tenantId: credential.tenantId, workloadId: workload.workloadId, action: clone(grant.action), status: "executing", executedAt: now };
|
|
146
|
+
state.receipts.push(receipt);
|
|
147
|
+
return { grant: clone(grant), credential: clone(credential), receipt: clone(receipt) };
|
|
148
|
+
});
|
|
149
|
+
if ("status" in started)
|
|
150
|
+
return started;
|
|
151
|
+
let secret;
|
|
152
|
+
try {
|
|
153
|
+
secret = await this.vault.readSecret(started.credential.vaultRef);
|
|
154
|
+
if (!(secret instanceof Uint8Array) || secret.byteLength === 0 || secret.byteLength > 64 * 1024)
|
|
155
|
+
throw new CredentialBrokerError("Credential vault returned invalid secret material.");
|
|
156
|
+
await this.assertExecutionActive(started.grant, input.identity);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
await this.completeExecution(started.receipt.id, "failed");
|
|
160
|
+
throw new CredentialBrokerError("Credential execution did not start.");
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const result = await this.executor.execute({ secret, grant: clone(started.grant), workload: clone(workload), assertActive: async () => this.assertExecutionActive(started.grant, input.identity) });
|
|
164
|
+
return await this.completeExecution(started.receipt.id, "executed", result.providerRequestId);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
await this.completeExecution(started.receipt.id, "unknown");
|
|
168
|
+
throw new CredentialBrokerError("Credential execution outcome is unknown and will not retry automatically.");
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
secret?.fill(0);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async assertExecutionActive(grant, identity) {
|
|
175
|
+
const verifiedWorkload = validateWorkloadIdentity(await this.workloadVerifier.authenticate(identity), this.timestamp());
|
|
176
|
+
await this.actionReceiptVerifier.verify(grant.action, verifiedWorkload);
|
|
177
|
+
await withFileLock(this.path, async () => {
|
|
178
|
+
const state = await this.read();
|
|
179
|
+
const current = state.grants.find((candidate) => candidate.id === grant.id);
|
|
180
|
+
if (current === undefined)
|
|
181
|
+
throw new CredentialBrokerError("Credential grant was not found.");
|
|
182
|
+
assertGrantUsable(current, verifiedWorkload, { credentialId: grant.credentialId, provider: grant.provider, scopes: grant.scopes, audience: grant.audience, action: grant.action }, this.timestamp());
|
|
183
|
+
const credential = findCredential(state, verifiedWorkload.tenantId, current.credentialId);
|
|
184
|
+
assertCredentialUsable(credential, this.timestamp());
|
|
185
|
+
if (credential.version !== current.credentialVersion || credential.provider !== this.executor.provider || !current.scopes.every((scope) => this.executor.supportsScope(scope)))
|
|
186
|
+
throw new CredentialBrokerError("Credential grant is no longer executable.");
|
|
187
|
+
if (state.receipts.find((receipt) => receipt.grantId === current.id)?.status !== "executing")
|
|
188
|
+
throw new CredentialBrokerError("Credential execution is no longer active.");
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
async completeExecution(receiptId, status, providerRequestId) {
|
|
192
|
+
return this.mutate((state, now) => {
|
|
193
|
+
const receipt = state.receipts.find((candidate) => candidate.id === receiptId);
|
|
194
|
+
if (receipt === undefined || receipt.status !== "executing")
|
|
195
|
+
throw new CredentialBrokerError("Credential execution receipt is not active.");
|
|
196
|
+
receipt.status = status;
|
|
197
|
+
receipt.executedAt = now;
|
|
198
|
+
if (status === "executed") {
|
|
199
|
+
receipt.providerRequestId = identifier(providerRequestId, "Provider request id");
|
|
200
|
+
findCredential(state, receipt.tenantId, receipt.credentialId).lastUsedAt = now;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
receipt.failureCode = status === "failed" ? "execution_not_started" : "unknown_outcome";
|
|
204
|
+
}
|
|
205
|
+
return clone(receipt);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
async listOrphanedCredentials() {
|
|
209
|
+
const state = await this.read();
|
|
210
|
+
const candidates = state.credentials.filter((credential) => credential.revokedAt === undefined);
|
|
211
|
+
const results = await Promise.all(candidates.map(async (credential) => {
|
|
212
|
+
const active = this.workloadVerifier.isActive === undefined ? false : await this.workloadVerifier.isActive({ tenantId: credential.tenantId, projectId: credential.projectId, environment: credential.environment, workloadId: credential.ownerWorkloadId, workloadKind: credential.ownerWorkloadKind });
|
|
213
|
+
return active ? undefined : clone(credential);
|
|
214
|
+
}));
|
|
215
|
+
return results.filter((credential) => credential !== undefined);
|
|
216
|
+
}
|
|
217
|
+
async inspectCredential(tenantIdValue, credentialIdValue) {
|
|
218
|
+
const credential = findCredential(await this.read(), identifier(tenantIdValue, "Credential tenant id"), identifier(credentialIdValue, "Credential id"));
|
|
219
|
+
return clone(credential);
|
|
220
|
+
}
|
|
221
|
+
async readStateForTesting() {
|
|
222
|
+
return this.read();
|
|
223
|
+
}
|
|
224
|
+
async authenticateWorkload(identity) {
|
|
225
|
+
const workload = await this.workloadVerifier.authenticate(identity);
|
|
226
|
+
return validateWorkloadIdentity(workload, this.timestamp());
|
|
227
|
+
}
|
|
228
|
+
async read() {
|
|
229
|
+
await ensurePrivateDirectory(dirname(this.path));
|
|
230
|
+
const info = await lstat(this.path).catch((error) => isErrorCode(error, "ENOENT") ? null : Promise.reject(error));
|
|
231
|
+
if (info === null)
|
|
232
|
+
return emptyState();
|
|
233
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
234
|
+
throw new CredentialBrokerError("Credential broker store must be a regular non-symlink file.");
|
|
235
|
+
const source = await readFile(this.path, "utf8");
|
|
236
|
+
if (Buffer.byteLength(source, "utf8") > MAX_STORE_BYTES)
|
|
237
|
+
throw new CredentialBrokerError(`Credential broker store exceeds ${MAX_STORE_BYTES} bytes.`);
|
|
238
|
+
try {
|
|
239
|
+
return validateState(JSON.parse(source));
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
if (error instanceof CredentialBrokerError)
|
|
243
|
+
throw error;
|
|
244
|
+
throw new CredentialBrokerError("Credential broker store is not valid JSON.");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async mutate(operation) {
|
|
248
|
+
return withFileLock(this.path, async () => {
|
|
249
|
+
const state = await this.read();
|
|
250
|
+
const result = await operation(state, this.timestamp());
|
|
251
|
+
await atomicWriteJson(this.path, validateState(state));
|
|
252
|
+
return result;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
timestamp() {
|
|
256
|
+
const value = this.now();
|
|
257
|
+
if (!(value instanceof Date) || !Number.isFinite(value.getTime()))
|
|
258
|
+
throw new CredentialBrokerError("Credential broker clock is invalid.");
|
|
259
|
+
return value.toISOString();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
export function createDisabledBreakGlassAuthorizer() {
|
|
263
|
+
return { async authorize() { throw new CredentialBrokerError("Break-glass authorization is not configured."); } };
|
|
264
|
+
}
|
|
265
|
+
export function createTestActionReceiptVerifier(reference) {
|
|
266
|
+
let calls = 0;
|
|
267
|
+
return {
|
|
268
|
+
verifier: {
|
|
269
|
+
async verify(candidate) {
|
|
270
|
+
calls += 1;
|
|
271
|
+
if (!sameAction(reference, candidate))
|
|
272
|
+
throw new CredentialBrokerError("Action receipt does not authorize this credential use.");
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
get calls() { return calls; }
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
export function createTestCredentialVault() {
|
|
279
|
+
const values = new Map();
|
|
280
|
+
return {
|
|
281
|
+
vault: {
|
|
282
|
+
kind: "test-memory-vault",
|
|
283
|
+
async readSecret(vaultRef) {
|
|
284
|
+
const value = values.get(reference(vaultRef, "Credential vault reference"));
|
|
285
|
+
if (value === undefined)
|
|
286
|
+
throw new CredentialBrokerError("Credential vault reference was not found.");
|
|
287
|
+
return new Uint8Array(value);
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
put(vaultRef, material) {
|
|
291
|
+
if (!(material instanceof Uint8Array) || material.byteLength === 0 || material.byteLength > 64 * 1024)
|
|
292
|
+
throw new CredentialBrokerError("Test credential material is invalid.");
|
|
293
|
+
values.set(reference(vaultRef, "Credential vault reference"), new Uint8Array(material));
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
export function createTestCredentialExecutor() {
|
|
298
|
+
const executions = [];
|
|
299
|
+
return {
|
|
300
|
+
executor: {
|
|
301
|
+
provider: "test-provider",
|
|
302
|
+
supportsScope: (scope) => scope === "test.execute",
|
|
303
|
+
async execute(input) {
|
|
304
|
+
if (input.grant.audience !== "ghostapi-server" || !input.grant.scopes.every((scope) => scope === "test.execute") || input.secret.byteLength === 0)
|
|
305
|
+
throw new CredentialBrokerError("Test provider rejected credential execution.");
|
|
306
|
+
await input.assertActive();
|
|
307
|
+
executions.push({ grantId: input.grant.id, secretLength: input.secret.byteLength });
|
|
308
|
+
return { providerRequestId: `test-provider-${input.grant.id}` };
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
executions
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
export function createTestWorkloadIdentityProvider(options = {}) {
|
|
315
|
+
const now = options.now ?? (() => new Date());
|
|
316
|
+
const issued = new WeakSet();
|
|
317
|
+
const active = new Set();
|
|
318
|
+
return {
|
|
319
|
+
verifier: {
|
|
320
|
+
async authenticate(identity) {
|
|
321
|
+
if (identity === null || typeof identity !== "object" || !issued.has(identity))
|
|
322
|
+
throw new CredentialBrokerError("Workload identity is not authenticated.");
|
|
323
|
+
const workload = validateWorkloadIdentity(identity, timestampFrom(now));
|
|
324
|
+
if (!active.has(workloadKey(workload)))
|
|
325
|
+
throw new CredentialBrokerError("Workload identity is inactive.");
|
|
326
|
+
return clone(workload);
|
|
327
|
+
},
|
|
328
|
+
async isActive(binding) {
|
|
329
|
+
return active.has(workloadKey(binding));
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
issue(input) {
|
|
333
|
+
const workload = validateWorkloadIdentity({ schemaVersion: 1, kind: "ghostapi.workload-identity", ...input }, timestampFrom(now));
|
|
334
|
+
const frozen = Object.freeze(clone(workload));
|
|
335
|
+
issued.add(frozen);
|
|
336
|
+
active.add(workloadKey(workload));
|
|
337
|
+
return frozen;
|
|
338
|
+
},
|
|
339
|
+
deactivate(workloadId) {
|
|
340
|
+
const id = identifier(workloadId, "Workload id");
|
|
341
|
+
for (const key of [...active])
|
|
342
|
+
if (key.endsWith(`:${id}`))
|
|
343
|
+
active.delete(key);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
export function createTestBreakGlassAuthorizer(options = {}) {
|
|
348
|
+
const now = options.now ?? (() => new Date());
|
|
349
|
+
const issued = new WeakSet();
|
|
350
|
+
return {
|
|
351
|
+
authorizer: {
|
|
352
|
+
async authorize(approval, workload, request) {
|
|
353
|
+
if (approval === null || typeof approval !== "object" || !issued.has(approval))
|
|
354
|
+
throw new CredentialBrokerError("Break-glass approval is not trusted.");
|
|
355
|
+
const parsed = validateBreakGlassApproval(approval, timestampFrom(now));
|
|
356
|
+
if (parsed.approvedBy === workload.subjectId || parsed.approvedBy === workload.workloadId || !sameAction(parsed.action, request.action))
|
|
357
|
+
throw new CredentialBrokerError("Break-glass approval does not authorize this workload action.");
|
|
358
|
+
return { approvalId: parsed.approvalId, approvedBy: parsed.approvedBy, reason: parsed.reason };
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
issue(input) {
|
|
362
|
+
const approval = validateBreakGlassApproval({ schemaVersion: 1, kind: "ghostapi.break-glass-approval", ...input }, timestampFrom(now));
|
|
363
|
+
const frozen = Object.freeze(clone(approval));
|
|
364
|
+
issued.add(frozen);
|
|
365
|
+
return frozen;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function emptyState() {
|
|
370
|
+
return { schemaVersion: 1, credentials: [], grants: [], receipts: [] };
|
|
371
|
+
}
|
|
372
|
+
function validateState(value) {
|
|
373
|
+
const state = object(value, "Credential broker store must be an object.");
|
|
374
|
+
exactKeys(state, ["schemaVersion", "credentials", "grants", "receipts"], "Credential broker store");
|
|
375
|
+
if (state.schemaVersion !== SCHEMA_VERSION)
|
|
376
|
+
throw new CredentialBrokerError("Unsupported credential broker schema version.");
|
|
377
|
+
const credentials = array(state.credentials, "Credentials", MAX_CREDENTIALS).map(validateCredentialMetadata);
|
|
378
|
+
unique(credentials.map((credential) => `${credential.tenantId}:${credential.id}`), "Credential ids");
|
|
379
|
+
const grants = array(state.grants, "Credential grants", MAX_GRANTS).map(validateGrant);
|
|
380
|
+
unique(grants.map((grant) => grant.id), "Credential grant ids");
|
|
381
|
+
for (const grant of grants) {
|
|
382
|
+
const credential = credentials.find((candidate) => candidate.tenantId === grant.tenantId && candidate.id === grant.credentialId);
|
|
383
|
+
if (credential === undefined || grant.credentialVersion > credential.version || grant.provider !== credential.provider || !grant.scopes.every((scope) => credential.allowedScopes.includes(scope)))
|
|
384
|
+
throw new CredentialBrokerError("Credential grant references an invalid credential.");
|
|
385
|
+
}
|
|
386
|
+
const receipts = array(state.receipts, "Credential use receipts", MAX_RECEIPTS).map(validateReceipt);
|
|
387
|
+
unique(receipts.map((receipt) => receipt.id), "Credential use receipt ids");
|
|
388
|
+
unique(receipts.map((receipt) => receipt.grantId), "Credential use receipt grants");
|
|
389
|
+
for (const receipt of receipts) {
|
|
390
|
+
const grant = grants.find((candidate) => candidate.id === receipt.grantId);
|
|
391
|
+
if (grant === undefined || receipt.credentialId !== grant.credentialId || receipt.credentialVersion !== grant.credentialVersion || receipt.tenantId !== grant.tenantId || receipt.workloadId !== grant.workloadId || !sameAction(receipt.action, grant.action))
|
|
392
|
+
throw new CredentialBrokerError("Credential use receipt references an invalid grant.");
|
|
393
|
+
}
|
|
394
|
+
return { schemaVersion: 1, credentials, grants, receipts };
|
|
395
|
+
}
|
|
396
|
+
function validateCredentialMetadata(value) {
|
|
397
|
+
const metadata = object(value, "Credential metadata is invalid.");
|
|
398
|
+
exactKeys(metadata, ["id", "tenantId", "projectId", "environment", "provider", "ownerWorkloadId", "ownerWorkloadKind", "vaultRef", "allowedScopes", "version", "createdAt", "expiresAt", "rotatedAt", "revokedAt", "lastUsedAt"], "Credential metadata", ["rotatedAt", "revokedAt", "lastUsedAt"]);
|
|
399
|
+
const createdAt = timestamp(metadata.createdAt, "Credential creation time");
|
|
400
|
+
const expiresAt = timestamp(metadata.expiresAt, "Credential expiry");
|
|
401
|
+
if (Date.parse(expiresAt) <= Date.parse(createdAt))
|
|
402
|
+
throw new CredentialBrokerError("Credential expiry must follow creation.");
|
|
403
|
+
return { id: identifier(metadata.id, "Credential id"), tenantId: identifier(metadata.tenantId, "Credential tenant id"), projectId: identifier(metadata.projectId, "Credential project id"), environment: identifier(metadata.environment, "Credential environment"), provider: identifier(metadata.provider, "Credential provider"), ownerWorkloadId: identifier(metadata.ownerWorkloadId, "Credential owner workload id"), ownerWorkloadKind: workloadKind(metadata.ownerWorkloadKind), vaultRef: reference(metadata.vaultRef, "Credential vault reference"), allowedScopes: scopes(metadata.allowedScopes, "Credential allowed scopes"), version: positiveInteger(metadata.version, "Credential version"), createdAt, expiresAt, ...(metadata.rotatedAt === undefined ? {} : { rotatedAt: timestamp(metadata.rotatedAt, "Credential rotation time") }), ...(metadata.revokedAt === undefined ? {} : { revokedAt: timestamp(metadata.revokedAt, "Credential revocation time") }), ...(metadata.lastUsedAt === undefined ? {} : { lastUsedAt: timestamp(metadata.lastUsedAt, "Credential last use time") }) };
|
|
404
|
+
}
|
|
405
|
+
function validateGrant(value) {
|
|
406
|
+
const grant = object(value, "Credential grant is invalid.");
|
|
407
|
+
exactKeys(grant, ["id", "credentialId", "credentialVersion", "tenantId", "projectId", "environment", "workloadId", "subjectId", "workloadKind", "runId", "provider", "scopes", "audience", "action", "issuedAt", "expiresAt", "breakGlass", "revokedAt"], "Credential grant", ["breakGlass", "revokedAt"]);
|
|
408
|
+
const issuedAt = timestamp(grant.issuedAt, "Credential grant issue time");
|
|
409
|
+
const expiresAt = timestamp(grant.expiresAt, "Credential grant expiry");
|
|
410
|
+
if (Date.parse(expiresAt) <= Date.parse(issuedAt) || Date.parse(expiresAt) - Date.parse(issuedAt) > MAX_GRANT_TTL_MS)
|
|
411
|
+
throw new CredentialBrokerError("Credential grant lifetime is invalid.");
|
|
412
|
+
const breakGlass = grant.breakGlass === undefined ? undefined : breakGlassRecord(grant.breakGlass);
|
|
413
|
+
if (breakGlass !== undefined && Date.parse(expiresAt) - Date.parse(issuedAt) > MAX_BREAK_GLASS_TTL_MS)
|
|
414
|
+
throw new CredentialBrokerError("Break-glass grant lifetime is invalid.");
|
|
415
|
+
if (grant.audience !== "ghostapi-server")
|
|
416
|
+
throw new CredentialBrokerError("Credential grant audience is invalid.");
|
|
417
|
+
return { id: identifier(grant.id, "Credential grant id"), credentialId: identifier(grant.credentialId, "Credential id"), credentialVersion: positiveInteger(grant.credentialVersion, "Credential version"), tenantId: identifier(grant.tenantId, "Credential tenant id"), projectId: identifier(grant.projectId, "Credential project id"), environment: identifier(grant.environment, "Credential environment"), workloadId: identifier(grant.workloadId, "Credential workload id"), subjectId: identifier(grant.subjectId, "Credential subject id"), workloadKind: workloadKind(grant.workloadKind), runId: identifier(grant.runId, "Credential run id"), provider: identifier(grant.provider, "Credential provider"), scopes: scopes(grant.scopes, "Credential grant scopes"), audience: "ghostapi-server", action: actionReference(grant.action), issuedAt, expiresAt, ...(breakGlass === undefined ? {} : { breakGlass }), ...(grant.revokedAt === undefined ? {} : { revokedAt: timestamp(grant.revokedAt, "Credential grant revocation time") }) };
|
|
418
|
+
}
|
|
419
|
+
function validateReceipt(value) {
|
|
420
|
+
const receipt = object(value, "Credential use receipt is invalid.");
|
|
421
|
+
exactKeys(receipt, ["id", "grantId", "credentialId", "credentialVersion", "tenantId", "workloadId", "action", "status", "providerRequestId", "failureCode", "executedAt"], "Credential use receipt", ["providerRequestId", "failureCode"]);
|
|
422
|
+
if (receipt.status !== "executing" && receipt.status !== "executed" && receipt.status !== "failed" && receipt.status !== "unknown")
|
|
423
|
+
throw new CredentialBrokerError("Credential use receipt status is invalid.");
|
|
424
|
+
if (receipt.status === "executing" && (receipt.failureCode !== undefined || receipt.providerRequestId !== undefined))
|
|
425
|
+
throw new CredentialBrokerError("Executing credential receipt is invalid.");
|
|
426
|
+
if (receipt.status === "executed" && (receipt.failureCode !== undefined || receipt.providerRequestId === undefined))
|
|
427
|
+
throw new CredentialBrokerError("Executed credential receipt is invalid.");
|
|
428
|
+
if (receipt.status === "failed" && (receipt.failureCode !== "execution_not_started" || receipt.providerRequestId !== undefined))
|
|
429
|
+
throw new CredentialBrokerError("Failed credential receipt is invalid.");
|
|
430
|
+
if (receipt.status === "unknown" && (receipt.failureCode !== "unknown_outcome" || receipt.providerRequestId !== undefined))
|
|
431
|
+
throw new CredentialBrokerError("Unknown credential receipt is invalid.");
|
|
432
|
+
return { id: identifier(receipt.id, "Credential use receipt id"), grantId: identifier(receipt.grantId, "Credential grant id"), credentialId: identifier(receipt.credentialId, "Credential id"), credentialVersion: positiveInteger(receipt.credentialVersion, "Credential version"), tenantId: identifier(receipt.tenantId, "Credential tenant id"), workloadId: identifier(receipt.workloadId, "Credential workload id"), action: actionReference(receipt.action), status: receipt.status, ...(receipt.providerRequestId === undefined ? {} : { providerRequestId: identifier(receipt.providerRequestId, "Provider request id") }), ...(receipt.failureCode === undefined ? {} : { failureCode: receipt.failureCode }), executedAt: timestamp(receipt.executedAt, "Credential execution time") };
|
|
433
|
+
}
|
|
434
|
+
function validateAccessRequest(value) {
|
|
435
|
+
const request = object(value, "Credential access request is invalid.");
|
|
436
|
+
exactKeys(request, ["credentialId", "provider", "scopes", "audience", "action"], "Credential access request");
|
|
437
|
+
if (request.audience !== "ghostapi-server")
|
|
438
|
+
throw new CredentialBrokerError("Credential access request audience is invalid.");
|
|
439
|
+
return { credentialId: identifier(request.credentialId, "Credential id"), provider: identifier(request.provider, "Credential provider"), scopes: scopes(request.scopes, "Credential request scopes"), audience: "ghostapi-server", action: actionReference(request.action) };
|
|
440
|
+
}
|
|
441
|
+
function validateWorkloadIdentity(value, now) {
|
|
442
|
+
const identity = object(value, "Workload identity is invalid.");
|
|
443
|
+
exactKeys(identity, ["schemaVersion", "kind", "tenantId", "projectId", "environment", "workloadId", "subjectId", "workloadKind", "runId", "issuedAt", "expiresAt"], "Workload identity");
|
|
444
|
+
if (identity.schemaVersion !== SCHEMA_VERSION || identity.kind !== "ghostapi.workload-identity")
|
|
445
|
+
throw new CredentialBrokerError("Unsupported workload identity schema.");
|
|
446
|
+
const issuedAt = timestamp(identity.issuedAt, "Workload identity issue time");
|
|
447
|
+
const expiresAt = timestamp(identity.expiresAt, "Workload identity expiry");
|
|
448
|
+
if (Date.parse(expiresAt) <= Date.parse(issuedAt) || Date.parse(expiresAt) <= Date.parse(now))
|
|
449
|
+
throw new CredentialBrokerError("Workload identity is expired.");
|
|
450
|
+
return { schemaVersion: 1, kind: "ghostapi.workload-identity", tenantId: identifier(identity.tenantId, "Workload tenant id"), projectId: identifier(identity.projectId, "Workload project id"), environment: identifier(identity.environment, "Workload environment"), workloadId: identifier(identity.workloadId, "Workload id"), subjectId: identifier(identity.subjectId, "Workload subject id"), workloadKind: workloadKind(identity.workloadKind), runId: identifier(identity.runId, "Workload run id"), issuedAt, expiresAt };
|
|
451
|
+
}
|
|
452
|
+
function validateBreakGlassApproval(value, now) {
|
|
453
|
+
const approval = object(value, "Break-glass approval is invalid.");
|
|
454
|
+
exactKeys(approval, ["schemaVersion", "kind", "approvalId", "action", "approvedBy", "reason", "issuedAt", "expiresAt"], "Break-glass approval");
|
|
455
|
+
if (approval.schemaVersion !== SCHEMA_VERSION || approval.kind !== "ghostapi.break-glass-approval")
|
|
456
|
+
throw new CredentialBrokerError("Unsupported break-glass approval schema.");
|
|
457
|
+
const issuedAt = timestamp(approval.issuedAt, "Break-glass issue time");
|
|
458
|
+
const expiresAt = timestamp(approval.expiresAt, "Break-glass expiry");
|
|
459
|
+
if (Date.parse(issuedAt) > Date.parse(now) || Date.parse(expiresAt) <= Date.parse(now) || Date.parse(expiresAt) - Date.parse(issuedAt) > MAX_BREAK_GLASS_TTL_MS)
|
|
460
|
+
throw new CredentialBrokerError("Break-glass approval is expired or exceeds five minutes.");
|
|
461
|
+
return { schemaVersion: 1, kind: "ghostapi.break-glass-approval", approvalId: identifier(approval.approvalId, "Break-glass approval id"), action: actionReference(approval.action), approvedBy: identifier(approval.approvedBy, "Break-glass approver id"), reason: text(approval.reason, "Break-glass reason", 200), issuedAt, expiresAt };
|
|
462
|
+
}
|
|
463
|
+
function breakGlassRecord(value) {
|
|
464
|
+
const record = object(value, "Break-glass record is invalid.");
|
|
465
|
+
exactKeys(record, ["approvalId", "approvedBy", "reason"], "Break-glass record");
|
|
466
|
+
return { approvalId: identifier(record.approvalId, "Break-glass approval id"), approvedBy: identifier(record.approvedBy, "Break-glass approver id"), reason: text(record.reason, "Break-glass reason", 200) };
|
|
467
|
+
}
|
|
468
|
+
function actionReference(value) {
|
|
469
|
+
const action = object(value, "Credential action reference is invalid.");
|
|
470
|
+
exactKeys(action, ["actionId", "actionHash", "actionReceiptHash"], "Credential action reference");
|
|
471
|
+
return { actionId: identifier(action.actionId, "Credential action id"), actionHash: hash(action.actionHash, "Credential action hash"), actionReceiptHash: hash(action.actionReceiptHash, "Credential action receipt hash") };
|
|
472
|
+
}
|
|
473
|
+
function assertCredentialUsable(credential, now) {
|
|
474
|
+
if (credential.revokedAt !== undefined)
|
|
475
|
+
throw new CredentialBrokerError("Credential is revoked.");
|
|
476
|
+
if (Date.parse(credential.expiresAt) <= Date.parse(now))
|
|
477
|
+
throw new CredentialBrokerError("Credential is expired.");
|
|
478
|
+
}
|
|
479
|
+
function assertRequestMatchesCredential(request, credential, workload) {
|
|
480
|
+
if (request.provider !== credential.provider || workload.tenantId !== credential.tenantId || workload.projectId !== credential.projectId || workload.environment !== credential.environment)
|
|
481
|
+
throw new CredentialBrokerError("Credential request crosses a tenant, project, environment, or provider boundary.");
|
|
482
|
+
if (!request.scopes.every((scope) => credential.allowedScopes.includes(scope)))
|
|
483
|
+
throw new CredentialBrokerError("Credential request exceeds the credential scope.");
|
|
484
|
+
}
|
|
485
|
+
function assertGrantUsable(grant, workload, request, now) {
|
|
486
|
+
if (grant.revokedAt !== undefined || Date.parse(grant.expiresAt) <= Date.parse(now))
|
|
487
|
+
throw new CredentialBrokerError("Credential grant is expired or revoked.");
|
|
488
|
+
if (grant.tenantId !== workload.tenantId || grant.projectId !== workload.projectId || grant.environment !== workload.environment || grant.workloadId !== workload.workloadId || grant.subjectId !== workload.subjectId || grant.workloadKind !== workload.workloadKind || grant.runId !== workload.runId)
|
|
489
|
+
throw new CredentialBrokerError("Credential grant does not belong to this workload subject and run.");
|
|
490
|
+
if (grant.credentialId !== request.credentialId || grant.provider !== request.provider || grant.audience !== request.audience || !sameScopes(grant.scopes, request.scopes) || !sameAction(grant.action, request.action))
|
|
491
|
+
throw new CredentialBrokerError("Credential request differs from the granted action.");
|
|
492
|
+
}
|
|
493
|
+
function findCredential(state, tenantId, credentialId) {
|
|
494
|
+
const credential = state.credentials.find((candidate) => candidate.tenantId === tenantId && candidate.id === credentialId);
|
|
495
|
+
if (credential === undefined)
|
|
496
|
+
throw new CredentialBrokerError("Credential was not found.");
|
|
497
|
+
return credential;
|
|
498
|
+
}
|
|
499
|
+
function scopes(value, label) {
|
|
500
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > MAX_SCOPES)
|
|
501
|
+
throw new CredentialBrokerError(`${label} must be a bounded non-empty list.`);
|
|
502
|
+
const parsed = value.map((scope) => identifier(scope, label));
|
|
503
|
+
if (new Set(parsed).size !== parsed.length)
|
|
504
|
+
throw new CredentialBrokerError(`${label} must not contain duplicates.`);
|
|
505
|
+
return parsed.sort();
|
|
506
|
+
}
|
|
507
|
+
function workloadKind(value) {
|
|
508
|
+
if (value !== "agent_run" && value !== "ci_job" && value !== "production_service")
|
|
509
|
+
throw new CredentialBrokerError("Workload kind is invalid.");
|
|
510
|
+
return value;
|
|
511
|
+
}
|
|
512
|
+
function sameScopes(left, right) {
|
|
513
|
+
return left.length === right.length && left.every((scope, index) => scope === right[index]);
|
|
514
|
+
}
|
|
515
|
+
function sameAction(left, right) {
|
|
516
|
+
return left.actionId === right.actionId && left.actionHash === right.actionHash && left.actionReceiptHash === right.actionReceiptHash;
|
|
517
|
+
}
|
|
518
|
+
function workloadKey(binding) {
|
|
519
|
+
return `${binding.tenantId}:${binding.projectId}:${binding.environment}:${binding.workloadKind}:${binding.workloadId}`;
|
|
520
|
+
}
|
|
521
|
+
function identifier(value, label) {
|
|
522
|
+
if (typeof value !== "string" || !IDENTIFIER.test(value) || sanitizeSecretString(value) !== value)
|
|
523
|
+
throw new CredentialBrokerError(`${label} must be a safe stable identifier.`);
|
|
524
|
+
return value;
|
|
525
|
+
}
|
|
526
|
+
function reference(value, label) {
|
|
527
|
+
if (typeof value !== "string" || !REFERENCE.test(value) || sanitizeSecretString(value) !== value)
|
|
528
|
+
throw new CredentialBrokerError(`${label} is invalid.`);
|
|
529
|
+
return value;
|
|
530
|
+
}
|
|
531
|
+
function hash(value, label) {
|
|
532
|
+
if (typeof value !== "string" || !HASH.test(value))
|
|
533
|
+
throw new CredentialBrokerError(`${label} must be a SHA-256 hex digest.`);
|
|
534
|
+
return value;
|
|
535
|
+
}
|
|
536
|
+
function positiveInteger(value, label) {
|
|
537
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1)
|
|
538
|
+
throw new CredentialBrokerError(`${label} must be a positive integer.`);
|
|
539
|
+
return value;
|
|
540
|
+
}
|
|
541
|
+
function timestamp(value, label) {
|
|
542
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) || !Number.isFinite(Date.parse(value)))
|
|
543
|
+
throw new CredentialBrokerError(`${label} must be an ISO UTC timestamp.`);
|
|
544
|
+
return value;
|
|
545
|
+
}
|
|
546
|
+
function futureTimestamp(value, now, label) {
|
|
547
|
+
const parsed = timestamp(value, label);
|
|
548
|
+
if (Date.parse(parsed) <= Date.parse(now))
|
|
549
|
+
throw new CredentialBrokerError(`${label} must be in the future.`);
|
|
550
|
+
return parsed;
|
|
551
|
+
}
|
|
552
|
+
function text(value, label, maxLength) {
|
|
553
|
+
if (typeof value !== "string" || value.length < 1 || value.length > maxLength || /[\u0000-\u001f]/.test(value) || sanitizeSecretString(value) !== value)
|
|
554
|
+
throw new CredentialBrokerError(`${label} is invalid.`);
|
|
555
|
+
return value;
|
|
556
|
+
}
|
|
557
|
+
function array(value, label, max) {
|
|
558
|
+
if (!Array.isArray(value) || value.length > max)
|
|
559
|
+
throw new CredentialBrokerError(`${label} is invalid.`);
|
|
560
|
+
return value;
|
|
561
|
+
}
|
|
562
|
+
function object(value, message) {
|
|
563
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
564
|
+
throw new CredentialBrokerError(message);
|
|
565
|
+
return value;
|
|
566
|
+
}
|
|
567
|
+
function exactKeys(value, keys, label, optional = []) {
|
|
568
|
+
for (const key of Object.keys(value))
|
|
569
|
+
if (!keys.includes(key) || (value[key] === undefined && !optional.includes(key)))
|
|
570
|
+
throw new CredentialBrokerError(`${label} contains unsupported field: ${key}`);
|
|
571
|
+
}
|
|
572
|
+
function unique(values, label) {
|
|
573
|
+
if (new Set(values).size !== values.length)
|
|
574
|
+
throw new CredentialBrokerError(`${label} must be unique.`);
|
|
575
|
+
}
|
|
576
|
+
function timestampFrom(now) {
|
|
577
|
+
const value = now();
|
|
578
|
+
if (!(value instanceof Date) || !Number.isFinite(value.getTime()))
|
|
579
|
+
throw new CredentialBrokerError("Credential broker clock is invalid.");
|
|
580
|
+
return value.toISOString();
|
|
581
|
+
}
|
|
582
|
+
function clone(value) {
|
|
583
|
+
return structuredClone(value);
|
|
584
|
+
}
|
|
585
|
+
function isErrorCode(error, code) {
|
|
586
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
587
|
+
}
|
|
588
|
+
//# sourceMappingURL=credentialBroker.js.map
|