@yiaany/ghostapi 0.1.8 → 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 -80
- 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 +67 -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/assets/ghostapi-avatar.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
|
@@ -3,7 +3,7 @@ import { lstat, readFile } from "node:fs/promises";
|
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { getDataPaths } from "../config/dataPaths.js";
|
|
5
5
|
import { sanitizeSecretString } from "../security/secrets.js";
|
|
6
|
-
import { atomicWriteJson, ensurePrivateDirectory, withFileLock } from "../storage/fileStore.js";
|
|
6
|
+
import { atomicWriteJson, ensurePrivateDirectory, withFileLock, } from "../storage/fileStore.js";
|
|
7
7
|
const SCHEMA_VERSION = 1;
|
|
8
8
|
const MAX_STORE_BYTES = 1024 * 1024;
|
|
9
9
|
const MAX_TARGETS = 1_000;
|
|
@@ -12,7 +12,14 @@ const MAX_EVALS = 100;
|
|
|
12
12
|
const IDENTIFIER = /^[a-z0-9][a-z0-9._-]{0,127}$/;
|
|
13
13
|
const HASH = /^[a-f0-9]{64}$/;
|
|
14
14
|
const MAX_FRESHNESS_MS = 30 * 24 * 60 * 60 * 1000;
|
|
15
|
-
export const TRUST_LEVELS = [
|
|
15
|
+
export const TRUST_LEVELS = [
|
|
16
|
+
"simulate",
|
|
17
|
+
"shadow",
|
|
18
|
+
"dry-run",
|
|
19
|
+
"approve",
|
|
20
|
+
"bounded-auto",
|
|
21
|
+
"trusted",
|
|
22
|
+
];
|
|
16
23
|
export class TrustLadderError extends Error {
|
|
17
24
|
constructor(message) {
|
|
18
25
|
super(message);
|
|
@@ -27,12 +34,39 @@ export function createLocalSyntheticTrustCapabilities() {
|
|
|
27
34
|
environment: "synthetic",
|
|
28
35
|
levels: [
|
|
29
36
|
{ level: "simulate", status: "supported", externalSideEffects: false },
|
|
30
|
-
{
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
{
|
|
38
|
+
level: "shadow",
|
|
39
|
+
status: "supported",
|
|
40
|
+
externalSideEffects: false,
|
|
41
|
+
requiresActualContext: true,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
level: "dry-run",
|
|
45
|
+
status: "unsupported",
|
|
46
|
+
externalSideEffects: false,
|
|
47
|
+
requiresOfficialProviderSemantics: true,
|
|
48
|
+
reason: "ghostapi-synthetic has no provider-official dry-run semantic.",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
level: "approve",
|
|
52
|
+
status: "supported",
|
|
53
|
+
externalSideEffects: false,
|
|
54
|
+
requiresApproval: true,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
level: "bounded-auto",
|
|
58
|
+
status: "supported",
|
|
59
|
+
externalSideEffects: false,
|
|
60
|
+
requiresCanary: true,
|
|
61
|
+
requiresOutcomeComparison: true,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
level: "trusted",
|
|
65
|
+
status: "unsupported",
|
|
66
|
+
externalSideEffects: false,
|
|
67
|
+
reason: "The local synthetic runtime is not production authorization.",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
36
70
|
};
|
|
37
71
|
}
|
|
38
72
|
export function createLocalTrustLadder(options) {
|
|
@@ -106,7 +140,9 @@ export class LocalTrustLadder {
|
|
|
106
140
|
const current = findOrCreateTarget(state, prediction.target, now);
|
|
107
141
|
assertActiveLevel(current, "shadow");
|
|
108
142
|
const evidence = comparisonEvidence(prediction.target, "shadow", observationHash(prediction), observationHash(actual), now);
|
|
109
|
-
appendAudit(state, "shadow.compared", current, undefined, undefined, evidence.matched
|
|
143
|
+
appendAudit(state, "shadow.compared", current, undefined, undefined, evidence.matched
|
|
144
|
+
? "predicted action matches actual input context"
|
|
145
|
+
: "predicted action differs from actual input context", now);
|
|
110
146
|
return evidence;
|
|
111
147
|
});
|
|
112
148
|
}
|
|
@@ -119,7 +155,9 @@ export class LocalTrustLadder {
|
|
|
119
155
|
const current = findOrCreateTarget(state, prediction.target, now);
|
|
120
156
|
assertActiveLevel(current, "bounded-auto");
|
|
121
157
|
const evidence = comparisonEvidence(prediction.target, "bounded-auto", outcomeHash(prediction), outcomeHash(actual), now);
|
|
122
|
-
appendAudit(state, "bounded.outcome_compared", current, undefined, undefined, evidence.matched
|
|
158
|
+
appendAudit(state, "bounded.outcome_compared", current, undefined, undefined, evidence.matched
|
|
159
|
+
? "predicted and actual bounded outcomes match"
|
|
160
|
+
: "predicted and actual bounded outcomes differ", now);
|
|
123
161
|
return evidence;
|
|
124
162
|
});
|
|
125
163
|
}
|
|
@@ -144,14 +182,23 @@ export class LocalTrustLadder {
|
|
|
144
182
|
current.canary.errors += 1;
|
|
145
183
|
current.updatedAt = now;
|
|
146
184
|
appendAudit(state, "canary.outcome", current, undefined, undefined, reason, now);
|
|
147
|
-
if (outcome === "violation" &&
|
|
185
|
+
if (outcome === "violation" &&
|
|
186
|
+
policy.violationResponse === "demote_to_approve") {
|
|
148
187
|
const previous = current.level;
|
|
149
188
|
current.level = "approve";
|
|
150
189
|
appendAudit(state, "trust.auto_demoted", current, previous, "approve", "policy violation automatically returned trust to approval mode", now);
|
|
151
190
|
}
|
|
152
|
-
if ((outcome === "violation" &&
|
|
191
|
+
if ((outcome === "violation" &&
|
|
192
|
+
policy.violationResponse === "open_circuit_breaker") ||
|
|
193
|
+
(current.canary.violations > 0 &&
|
|
194
|
+
current.canary.violations >= policy.stopConditions.maxViolations) ||
|
|
195
|
+
(current.canary.errors > 0 &&
|
|
196
|
+
current.canary.errors >= policy.stopConditions.maxErrors)) {
|
|
153
197
|
current.circuitBreaker = "open";
|
|
154
|
-
appendAudit(state, "canary.circuit_opened", current, undefined, undefined, outcome === "violation" &&
|
|
198
|
+
appendAudit(state, "canary.circuit_opened", current, undefined, undefined, outcome === "violation" &&
|
|
199
|
+
policy.violationResponse === "open_circuit_breaker"
|
|
200
|
+
? "policy violation opened the configured circuit breaker"
|
|
201
|
+
: "configured canary stop condition was breached", now);
|
|
155
202
|
}
|
|
156
203
|
return clone(current);
|
|
157
204
|
});
|
|
@@ -214,33 +261,62 @@ export function createTestTrustOwnerVerifier() {
|
|
|
214
261
|
return {
|
|
215
262
|
verifier: {
|
|
216
263
|
async authenticate(identity) {
|
|
217
|
-
if (identity === null ||
|
|
264
|
+
if (identity === null ||
|
|
265
|
+
typeof identity !== "object" ||
|
|
266
|
+
!issued.has(identity))
|
|
218
267
|
throw new TrustLadderError("Trust owner identity is not authenticated.");
|
|
219
268
|
return validateOwner(identity);
|
|
220
|
-
}
|
|
269
|
+
},
|
|
221
270
|
},
|
|
222
271
|
issue(input) {
|
|
223
272
|
const owner = Object.freeze(validateOwner({ ...input, principalId: input.principalId ?? input.id }));
|
|
224
273
|
issued.add(owner);
|
|
225
274
|
return owner;
|
|
226
|
-
}
|
|
275
|
+
},
|
|
227
276
|
};
|
|
228
277
|
}
|
|
229
278
|
function validateCapabilities(value) {
|
|
230
279
|
const capabilities = object(value, "Trust capabilities are invalid.");
|
|
231
280
|
exactKeys(capabilities, ["schemaVersion", "kind", "provider", "environment", "levels"], "Trust capabilities");
|
|
232
|
-
if (capabilities.schemaVersion !== SCHEMA_VERSION ||
|
|
281
|
+
if (capabilities.schemaVersion !== SCHEMA_VERSION ||
|
|
282
|
+
capabilities.kind !== "ghostapi.trust-capabilities" ||
|
|
283
|
+
capabilities.provider !== "ghostapi-synthetic" ||
|
|
284
|
+
capabilities.environment !== "synthetic")
|
|
233
285
|
throw new TrustLadderError("Only local synthetic trust capabilities are available.");
|
|
234
286
|
const levels = array(capabilities.levels, "Trust level capabilities", TRUST_LEVELS.length).map(validateCapability);
|
|
235
|
-
if (levels.length !== TRUST_LEVELS.length ||
|
|
287
|
+
if (levels.length !== TRUST_LEVELS.length ||
|
|
288
|
+
levels.some((capability, index) => capability.level !== TRUST_LEVELS[index]))
|
|
236
289
|
throw new TrustLadderError("Trust capabilities must define every level in stable order.");
|
|
237
290
|
if (levels.some((capability) => capability.externalSideEffects))
|
|
238
291
|
throw new TrustLadderError("Local trust capabilities cannot enable external side effects.");
|
|
239
|
-
return {
|
|
292
|
+
return {
|
|
293
|
+
schemaVersion: 1,
|
|
294
|
+
kind: "ghostapi.trust-capabilities",
|
|
295
|
+
provider: "ghostapi-synthetic",
|
|
296
|
+
environment: "synthetic",
|
|
297
|
+
levels,
|
|
298
|
+
};
|
|
240
299
|
}
|
|
241
300
|
function validateCapability(value) {
|
|
242
301
|
const capability = object(value, "Trust level capability is invalid.");
|
|
243
|
-
exactKeys(capability, [
|
|
302
|
+
exactKeys(capability, [
|
|
303
|
+
"level",
|
|
304
|
+
"status",
|
|
305
|
+
"externalSideEffects",
|
|
306
|
+
"requiresActualContext",
|
|
307
|
+
"requiresOfficialProviderSemantics",
|
|
308
|
+
"requiresApproval",
|
|
309
|
+
"requiresCanary",
|
|
310
|
+
"requiresOutcomeComparison",
|
|
311
|
+
"reason",
|
|
312
|
+
], "Trust level capability", [
|
|
313
|
+
"requiresActualContext",
|
|
314
|
+
"requiresOfficialProviderSemantics",
|
|
315
|
+
"requiresApproval",
|
|
316
|
+
"requiresCanary",
|
|
317
|
+
"requiresOutcomeComparison",
|
|
318
|
+
"reason",
|
|
319
|
+
]);
|
|
244
320
|
const level = trustLevel(capability.level, "Trust capability level");
|
|
245
321
|
if (capability.status !== "supported" && capability.status !== "unsupported")
|
|
246
322
|
throw new TrustLadderError("Trust capability status is invalid.");
|
|
@@ -251,21 +327,54 @@ function validateCapability(value) {
|
|
|
251
327
|
const requiresApproval = booleanFlag(capability.requiresApproval, "Trust capability flag");
|
|
252
328
|
const requiresCanary = booleanFlag(capability.requiresCanary, "Trust capability flag");
|
|
253
329
|
const requiresOutcomeComparison = booleanFlag(capability.requiresOutcomeComparison, "Trust capability flag");
|
|
254
|
-
const reason = capability.reason === undefined
|
|
330
|
+
const reason = capability.reason === undefined
|
|
331
|
+
? undefined
|
|
332
|
+
: text(capability.reason, "Trust capability reason", 200);
|
|
255
333
|
if (capability.status === "unsupported" && reason === undefined)
|
|
256
334
|
throw new TrustLadderError("Unsupported trust capability requires a reason.");
|
|
257
|
-
return {
|
|
335
|
+
return {
|
|
336
|
+
level,
|
|
337
|
+
status: capability.status,
|
|
338
|
+
externalSideEffects: false,
|
|
339
|
+
...(requiresActualContext === undefined ? {} : { requiresActualContext }),
|
|
340
|
+
...(requiresOfficialProviderSemantics === undefined
|
|
341
|
+
? {}
|
|
342
|
+
: { requiresOfficialProviderSemantics }),
|
|
343
|
+
...(requiresApproval === undefined ? {} : { requiresApproval }),
|
|
344
|
+
...(requiresCanary === undefined ? {} : { requiresCanary }),
|
|
345
|
+
...(requiresOutcomeComparison === undefined
|
|
346
|
+
? {}
|
|
347
|
+
: { requiresOutcomeComparison }),
|
|
348
|
+
...(reason === undefined ? {} : { reason }),
|
|
349
|
+
};
|
|
258
350
|
}
|
|
259
351
|
function validatePolicy(value) {
|
|
260
352
|
const policy = object(value, "Trust promotion policy is invalid.");
|
|
261
|
-
exactKeys(policy, [
|
|
262
|
-
|
|
353
|
+
exactKeys(policy, [
|
|
354
|
+
"schemaVersion",
|
|
355
|
+
"kind",
|
|
356
|
+
"id",
|
|
357
|
+
"ownerPrincipalId",
|
|
358
|
+
"automaticPromotion",
|
|
359
|
+
"minimumRuns",
|
|
360
|
+
"requiredEvalIds",
|
|
361
|
+
"maxViolationRateBps",
|
|
362
|
+
"maxErrorRateBps",
|
|
363
|
+
"evidenceFreshnessMs",
|
|
364
|
+
"canary",
|
|
365
|
+
"stopConditions",
|
|
366
|
+
"violationResponse",
|
|
367
|
+
], "Trust promotion policy");
|
|
368
|
+
if (policy.schemaVersion !== SCHEMA_VERSION ||
|
|
369
|
+
policy.kind !== "ghostapi.trust-promotion-policy" ||
|
|
370
|
+
policy.automaticPromotion !== false)
|
|
263
371
|
throw new TrustLadderError("Automatic trust promotion is disabled.");
|
|
264
372
|
const canary = object(policy.canary, "Trust canary scope is invalid.");
|
|
265
373
|
exactKeys(canary, ["tenantIds", "resourceIds", "percentageBps"], "Trust canary scope");
|
|
266
374
|
const stopConditions = object(policy.stopConditions, "Trust canary stop conditions are invalid.");
|
|
267
375
|
exactKeys(stopConditions, ["maxViolations", "maxErrors"], "Trust canary stop conditions");
|
|
268
|
-
if (policy.violationResponse !== "demote_to_approve" &&
|
|
376
|
+
if (policy.violationResponse !== "demote_to_approve" &&
|
|
377
|
+
policy.violationResponse !== "open_circuit_breaker")
|
|
269
378
|
throw new TrustLadderError("Trust violation response is invalid.");
|
|
270
379
|
return {
|
|
271
380
|
schemaVersion: 1,
|
|
@@ -278,9 +387,16 @@ function validatePolicy(value) {
|
|
|
278
387
|
maxViolationRateBps: bps(policy.maxViolationRateBps, "Trust maximum violation rate"),
|
|
279
388
|
maxErrorRateBps: bps(policy.maxErrorRateBps, "Trust maximum error rate"),
|
|
280
389
|
evidenceFreshnessMs: positive(policy.evidenceFreshnessMs, "Trust evidence freshness", MAX_FRESHNESS_MS),
|
|
281
|
-
canary: {
|
|
282
|
-
|
|
283
|
-
|
|
390
|
+
canary: {
|
|
391
|
+
tenantIds: identifiers(canary.tenantIds, "Trust canary tenant ids", MAX_TARGETS, true),
|
|
392
|
+
resourceIds: identifiers(canary.resourceIds, "Trust canary resource ids", MAX_TARGETS, true),
|
|
393
|
+
percentageBps: bps(canary.percentageBps, "Trust canary percentage"),
|
|
394
|
+
},
|
|
395
|
+
stopConditions: {
|
|
396
|
+
maxViolations: nonNegative(stopConditions.maxViolations, "Trust canary maximum violations", 1_000_000),
|
|
397
|
+
maxErrors: nonNegative(stopConditions.maxErrors, "Trust canary maximum errors", 1_000_000),
|
|
398
|
+
},
|
|
399
|
+
violationResponse: policy.violationResponse,
|
|
284
400
|
};
|
|
285
401
|
}
|
|
286
402
|
function validateEvidence(value) {
|
|
@@ -294,32 +410,61 @@ function validateEvidence(value) {
|
|
|
294
410
|
exactKeys(evaluation, ["id", "status", "completedAt"], "Trust evidence eval");
|
|
295
411
|
if (evaluation.status !== "passed" && evaluation.status !== "failed")
|
|
296
412
|
throw new TrustLadderError("Trust evidence eval status is invalid.");
|
|
297
|
-
return {
|
|
413
|
+
return {
|
|
414
|
+
id: identifier(evaluation.id, "Trust evidence eval id"),
|
|
415
|
+
status: evaluation.status,
|
|
416
|
+
completedAt: timestamp(evaluation.completedAt, "Trust evidence eval completion"),
|
|
417
|
+
};
|
|
298
418
|
});
|
|
299
419
|
unique(evals.map((evaluation) => evaluation.id), "Trust evidence eval ids");
|
|
300
|
-
return {
|
|
420
|
+
return {
|
|
421
|
+
runCount,
|
|
422
|
+
violations,
|
|
423
|
+
errors,
|
|
424
|
+
evals,
|
|
425
|
+
observedAt: timestamp(evidence.observedAt, "Trust evidence observation time"),
|
|
426
|
+
};
|
|
301
427
|
}
|
|
302
428
|
function validateTarget(value) {
|
|
303
429
|
const target = object(value, "Trust target is invalid.");
|
|
304
430
|
exactKeys(target, ["provider", "environment", "tenantId", "resourceId"], "Trust target");
|
|
305
|
-
if (target.provider !== "ghostapi-synthetic" ||
|
|
431
|
+
if (target.provider !== "ghostapi-synthetic" ||
|
|
432
|
+
target.environment !== "synthetic")
|
|
306
433
|
throw new TrustLadderError("Local trust preparation accepts synthetic identities only; production identities are unsupported.");
|
|
307
|
-
return {
|
|
434
|
+
return {
|
|
435
|
+
provider: "ghostapi-synthetic",
|
|
436
|
+
environment: "synthetic",
|
|
437
|
+
tenantId: identifier(target.tenantId, "Trust target tenant id"),
|
|
438
|
+
resourceId: identifier(target.resourceId, "Trust target resource id"),
|
|
439
|
+
};
|
|
308
440
|
}
|
|
309
441
|
function validateObservation(value) {
|
|
310
442
|
const observation = object(value, "Shadow observation is invalid.");
|
|
311
443
|
exactKeys(observation, ["target", "actionHash", "operation", "contextHash"], "Shadow observation");
|
|
312
|
-
return {
|
|
444
|
+
return {
|
|
445
|
+
target: validateTarget(observation.target),
|
|
446
|
+
actionHash: hash(observation.actionHash, "Shadow action hash"),
|
|
447
|
+
operation: identifier(observation.operation, "Shadow operation"),
|
|
448
|
+
contextHash: hash(observation.contextHash, "Shadow context hash"),
|
|
449
|
+
};
|
|
313
450
|
}
|
|
314
451
|
function validateOutcomeObservation(value) {
|
|
315
452
|
const observation = object(value, "Bounded outcome observation is invalid.");
|
|
316
453
|
exactKeys(observation, ["target", "actionHash", "outcomeHash", "executionReceiptHash"], "Bounded outcome observation");
|
|
317
|
-
return {
|
|
454
|
+
return {
|
|
455
|
+
target: validateTarget(observation.target),
|
|
456
|
+
actionHash: hash(observation.actionHash, "Bounded action hash"),
|
|
457
|
+
outcomeHash: hash(observation.outcomeHash, "Bounded outcome hash"),
|
|
458
|
+
executionReceiptHash: hash(observation.executionReceiptHash, "Bounded execution receipt hash"),
|
|
459
|
+
};
|
|
318
460
|
}
|
|
319
461
|
function validateOwner(value) {
|
|
320
462
|
const owner = object(value, "Trust owner identity is invalid.");
|
|
321
463
|
exactKeys(owner, ["id", "principalId"], "Trust owner identity");
|
|
322
|
-
return {
|
|
464
|
+
return {
|
|
465
|
+
id: identifier(owner.id, "Trust owner id"),
|
|
466
|
+
principalId: identifier(owner.principalId, "Trust owner principal id"),
|
|
467
|
+
};
|
|
323
468
|
}
|
|
324
469
|
function validateState(value) {
|
|
325
470
|
const state = object(value, "Trust ladder store must be an object.");
|
|
@@ -343,22 +488,43 @@ function validateTargetState(value) {
|
|
|
343
488
|
const errors = nonNegative(canary.errors, "Trust canary error count", observed);
|
|
344
489
|
if (state.circuitBreaker !== "closed" && state.circuitBreaker !== "open")
|
|
345
490
|
throw new TrustLadderError("Trust circuit breaker state is invalid.");
|
|
346
|
-
return {
|
|
491
|
+
return {
|
|
492
|
+
target: validateTarget(state.target),
|
|
493
|
+
level: trustLevel(state.level, "Trust target level"),
|
|
494
|
+
circuitBreaker: state.circuitBreaker,
|
|
495
|
+
canary: { observed, violations, errors },
|
|
496
|
+
updatedAt: timestamp(state.updatedAt, "Trust target update time"),
|
|
497
|
+
};
|
|
347
498
|
}
|
|
348
499
|
function validateAudit(value) {
|
|
349
500
|
const record = object(value, "Trust audit record is invalid.");
|
|
350
|
-
exactKeys(record, [
|
|
501
|
+
exactKeys(record, [
|
|
502
|
+
"sequence",
|
|
503
|
+
"event",
|
|
504
|
+
"targetKey",
|
|
505
|
+
"fromLevel",
|
|
506
|
+
"toLevel",
|
|
507
|
+
"reason",
|
|
508
|
+
"timestamp",
|
|
509
|
+
"previousHash",
|
|
510
|
+
"recordHash",
|
|
511
|
+
], "Trust audit record", ["fromLevel", "toLevel"]);
|
|
351
512
|
const base = {
|
|
352
513
|
sequence: positive(record.sequence, "Trust audit sequence", MAX_AUDIT),
|
|
353
514
|
event: text(record.event, "Trust audit event", 80),
|
|
354
515
|
targetKey: identifier(record.targetKey, "Trust audit target key"),
|
|
355
|
-
...(record.fromLevel === undefined
|
|
356
|
-
|
|
516
|
+
...(record.fromLevel === undefined
|
|
517
|
+
? {}
|
|
518
|
+
: { fromLevel: trustLevel(record.fromLevel, "Trust audit from level") }),
|
|
519
|
+
...(record.toLevel === undefined
|
|
520
|
+
? {}
|
|
521
|
+
: { toLevel: trustLevel(record.toLevel, "Trust audit to level") }),
|
|
357
522
|
reason: text(record.reason, "Trust audit reason", 200),
|
|
358
523
|
timestamp: timestamp(record.timestamp, "Trust audit timestamp"),
|
|
359
|
-
previousHash: hash(record.previousHash, "Trust audit previous hash")
|
|
524
|
+
previousHash: hash(record.previousHash, "Trust audit previous hash"),
|
|
360
525
|
};
|
|
361
|
-
if (hash(record.recordHash, "Trust audit record hash") !==
|
|
526
|
+
if (hash(record.recordHash, "Trust audit record hash") !==
|
|
527
|
+
sha256(canonical(base)))
|
|
362
528
|
throw new TrustLadderError("Trust audit record hash is invalid.");
|
|
363
529
|
return { ...base, recordHash: record.recordHash };
|
|
364
530
|
}
|
|
@@ -373,7 +539,12 @@ function validateAuditChain(anchor, records) {
|
|
|
373
539
|
}
|
|
374
540
|
}
|
|
375
541
|
function emptyState() {
|
|
376
|
-
return {
|
|
542
|
+
return {
|
|
543
|
+
schemaVersion: 1,
|
|
544
|
+
targets: [],
|
|
545
|
+
auditAnchor: sha256("ghostapi.trust.audit.v1"),
|
|
546
|
+
audit: [],
|
|
547
|
+
};
|
|
377
548
|
}
|
|
378
549
|
function findOrCreateTarget(state, target, now) {
|
|
379
550
|
const existing = state.targets.find((candidate) => sameTarget(candidate.target, target));
|
|
@@ -381,7 +552,13 @@ function findOrCreateTarget(state, target, now) {
|
|
|
381
552
|
return existing;
|
|
382
553
|
if (state.targets.length >= MAX_TARGETS)
|
|
383
554
|
throw new TrustLadderError("Trust target limit was reached.");
|
|
384
|
-
const created = {
|
|
555
|
+
const created = {
|
|
556
|
+
target: clone(target),
|
|
557
|
+
level: "simulate",
|
|
558
|
+
circuitBreaker: "closed",
|
|
559
|
+
canary: { observed: 0, violations: 0, errors: 0 },
|
|
560
|
+
updatedAt: now,
|
|
561
|
+
};
|
|
385
562
|
state.targets.push(created);
|
|
386
563
|
return created;
|
|
387
564
|
}
|
|
@@ -390,7 +567,8 @@ function assertPromotionEvidence(evidence, policy, now) {
|
|
|
390
567
|
throw new TrustLadderError("Trust promotion evidence has too few runs.");
|
|
391
568
|
if (Date.parse(evidence.observedAt) > Date.parse(now))
|
|
392
569
|
throw new TrustLadderError("Trust promotion evidence is from the future.");
|
|
393
|
-
if (Date.parse(now) - Date.parse(evidence.observedAt) >
|
|
570
|
+
if (Date.parse(now) - Date.parse(evidence.observedAt) >
|
|
571
|
+
policy.evidenceFreshnessMs)
|
|
394
572
|
throw new TrustLadderError("Trust promotion evidence is stale.");
|
|
395
573
|
for (const required of policy.requiredEvalIds) {
|
|
396
574
|
const evaluation = evidence.evals.find((candidate) => candidate.id === required);
|
|
@@ -398,7 +576,8 @@ function assertPromotionEvidence(evidence, policy, now) {
|
|
|
398
576
|
throw new TrustLadderError(`Trust promotion requires passing eval: ${required}`);
|
|
399
577
|
if (Date.parse(evaluation.completedAt) > Date.parse(now))
|
|
400
578
|
throw new TrustLadderError(`Trust promotion eval is from the future: ${required}`);
|
|
401
|
-
if (Date.parse(now) - Date.parse(evaluation.completedAt) >
|
|
579
|
+
if (Date.parse(now) - Date.parse(evaluation.completedAt) >
|
|
580
|
+
policy.evidenceFreshnessMs)
|
|
402
581
|
throw new TrustLadderError(`Trust promotion eval is stale: ${required}`);
|
|
403
582
|
}
|
|
404
583
|
if (rateBps(evidence.violations, evidence.runCount) > policy.maxViolationRateBps)
|
|
@@ -420,20 +599,54 @@ function capabilityFor(capabilities, level) {
|
|
|
420
599
|
return capability;
|
|
421
600
|
}
|
|
422
601
|
function canaryDecision(target, policy) {
|
|
423
|
-
if (policy.canary.tenantIds.length > 0 &&
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
602
|
+
if (policy.canary.tenantIds.length > 0 &&
|
|
603
|
+
!policy.canary.tenantIds.includes(target.tenantId))
|
|
604
|
+
return {
|
|
605
|
+
target: clone(target),
|
|
606
|
+
assigned: false,
|
|
607
|
+
bucket: deterministicBucket(policy.id, target),
|
|
608
|
+
reason: "tenant is outside the configured canary scope",
|
|
609
|
+
};
|
|
610
|
+
if (policy.canary.resourceIds.length > 0 &&
|
|
611
|
+
!policy.canary.resourceIds.includes(target.resourceId))
|
|
612
|
+
return {
|
|
613
|
+
target: clone(target),
|
|
614
|
+
assigned: false,
|
|
615
|
+
bucket: deterministicBucket(policy.id, target),
|
|
616
|
+
reason: "resource is outside the configured canary scope",
|
|
617
|
+
};
|
|
427
618
|
const bucket = deterministicBucket(policy.id, target);
|
|
428
619
|
return bucket < policy.canary.percentageBps
|
|
429
|
-
? {
|
|
430
|
-
|
|
620
|
+
? {
|
|
621
|
+
target: clone(target),
|
|
622
|
+
assigned: true,
|
|
623
|
+
bucket,
|
|
624
|
+
reason: "target is inside deterministic canary scope",
|
|
625
|
+
}
|
|
626
|
+
: {
|
|
627
|
+
target: clone(target),
|
|
628
|
+
assigned: false,
|
|
629
|
+
bucket,
|
|
630
|
+
reason: "target is outside the configured canary percentage",
|
|
631
|
+
};
|
|
431
632
|
}
|
|
432
633
|
function deterministicBucket(policyId, target) {
|
|
433
|
-
return createHash("sha256")
|
|
634
|
+
return (createHash("sha256")
|
|
635
|
+
.update(`ghostapi.trust.canary.v1:${policyId}:${targetKey(target)}`, "utf8")
|
|
636
|
+
.digest()
|
|
637
|
+
.readUInt32BE(0) % 10_000);
|
|
434
638
|
}
|
|
435
639
|
function comparisonEvidence(target, level, predictedHash, actualHash, observedAt) {
|
|
436
|
-
return {
|
|
640
|
+
return {
|
|
641
|
+
schemaVersion: 1,
|
|
642
|
+
kind: "ghostapi.trust-comparison",
|
|
643
|
+
target: clone(target),
|
|
644
|
+
level,
|
|
645
|
+
matched: predictedHash === actualHash,
|
|
646
|
+
predictedHash,
|
|
647
|
+
actualHash,
|
|
648
|
+
observedAt,
|
|
649
|
+
};
|
|
437
650
|
}
|
|
438
651
|
function observationHash(value) {
|
|
439
652
|
return sha256(canonical(value));
|
|
@@ -445,7 +658,16 @@ function appendAudit(state, event, target, fromLevel, toLevel, reason, timestamp
|
|
|
445
658
|
if (state.audit.length >= MAX_AUDIT)
|
|
446
659
|
throw new TrustLadderError("Trust audit limit was reached.");
|
|
447
660
|
const previousHash = state.audit.at(-1)?.recordHash ?? state.auditAnchor;
|
|
448
|
-
const base = {
|
|
661
|
+
const base = {
|
|
662
|
+
sequence: state.audit.length + 1,
|
|
663
|
+
event: text(event, "Trust audit event", 80),
|
|
664
|
+
targetKey: targetKey(target.target),
|
|
665
|
+
...(fromLevel === undefined ? {} : { fromLevel }),
|
|
666
|
+
...(toLevel === undefined ? {} : { toLevel }),
|
|
667
|
+
reason: text(reason, "Trust audit reason", 200),
|
|
668
|
+
timestamp: timestampValue,
|
|
669
|
+
previousHash,
|
|
670
|
+
};
|
|
449
671
|
state.audit.push({ ...base, recordHash: sha256(canonical(base)) });
|
|
450
672
|
}
|
|
451
673
|
function assertActiveLevel(state, level) {
|
|
@@ -455,7 +677,10 @@ function assertActiveLevel(state, level) {
|
|
|
455
677
|
throw new TrustLadderError(`Trust target is not active at ${level}.`);
|
|
456
678
|
}
|
|
457
679
|
function sameTarget(left, right) {
|
|
458
|
-
return left.provider === right.provider &&
|
|
680
|
+
return (left.provider === right.provider &&
|
|
681
|
+
left.environment === right.environment &&
|
|
682
|
+
left.tenantId === right.tenantId &&
|
|
683
|
+
left.resourceId === right.resourceId);
|
|
459
684
|
}
|
|
460
685
|
function targetKey(target) {
|
|
461
686
|
return `target-${sha256(canonical(target)).slice(0, 32)}`;
|
|
@@ -471,7 +696,9 @@ function canaryOutcome(value) {
|
|
|
471
696
|
return value;
|
|
472
697
|
}
|
|
473
698
|
function identifier(value, label) {
|
|
474
|
-
if (typeof value !== "string" ||
|
|
699
|
+
if (typeof value !== "string" ||
|
|
700
|
+
!IDENTIFIER.test(value) ||
|
|
701
|
+
sanitizeSecretString(value) !== value)
|
|
475
702
|
throw new TrustLadderError(`${label} must be a safe identifier.`);
|
|
476
703
|
return value;
|
|
477
704
|
}
|
|
@@ -488,17 +715,25 @@ function hash(value, label) {
|
|
|
488
715
|
return value;
|
|
489
716
|
}
|
|
490
717
|
function timestamp(value, label) {
|
|
491
|
-
if (typeof value !== "string" ||
|
|
718
|
+
if (typeof value !== "string" ||
|
|
719
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) ||
|
|
720
|
+
!Number.isFinite(Date.parse(value)))
|
|
492
721
|
throw new TrustLadderError(`${label} must be an ISO UTC timestamp.`);
|
|
493
722
|
return value;
|
|
494
723
|
}
|
|
495
724
|
function positive(value, label, max) {
|
|
496
|
-
if (typeof value !== "number" ||
|
|
725
|
+
if (typeof value !== "number" ||
|
|
726
|
+
!Number.isInteger(value) ||
|
|
727
|
+
value < 1 ||
|
|
728
|
+
value > max)
|
|
497
729
|
throw new TrustLadderError(`${label} is invalid.`);
|
|
498
730
|
return value;
|
|
499
731
|
}
|
|
500
732
|
function nonNegative(value, label, max) {
|
|
501
|
-
if (typeof value !== "number" ||
|
|
733
|
+
if (typeof value !== "number" ||
|
|
734
|
+
!Number.isInteger(value) ||
|
|
735
|
+
value < 0 ||
|
|
736
|
+
value > max)
|
|
502
737
|
throw new TrustLadderError(`${label} is invalid.`);
|
|
503
738
|
return value;
|
|
504
739
|
}
|
|
@@ -516,7 +751,11 @@ function rateBps(count, total) {
|
|
|
516
751
|
return total === 0 ? 10_000 : Math.floor((count * 10_000) / total);
|
|
517
752
|
}
|
|
518
753
|
function text(value, label, maxLength) {
|
|
519
|
-
if (typeof value !== "string" ||
|
|
754
|
+
if (typeof value !== "string" ||
|
|
755
|
+
value.trim() === "" ||
|
|
756
|
+
value.length > maxLength ||
|
|
757
|
+
/[\u0000-\u001f]/.test(value) ||
|
|
758
|
+
sanitizeSecretString(value) !== value)
|
|
520
759
|
throw new TrustLadderError(`${label} is invalid.`);
|
|
521
760
|
return value.trim();
|
|
522
761
|
}
|
|
@@ -532,7 +771,8 @@ function array(value, label, max) {
|
|
|
532
771
|
}
|
|
533
772
|
function exactKeys(value, keys, label, optional = []) {
|
|
534
773
|
for (const key of Object.keys(value))
|
|
535
|
-
if (!keys.includes(key) ||
|
|
774
|
+
if (!keys.includes(key) ||
|
|
775
|
+
(value[key] === undefined && !optional.includes(key)))
|
|
536
776
|
throw new TrustLadderError(`${label} contains unsupported field: ${key}`);
|
|
537
777
|
}
|
|
538
778
|
function unique(values, label) {
|
|
@@ -540,12 +780,18 @@ function unique(values, label) {
|
|
|
540
780
|
throw new TrustLadderError(`${label} must be unique.`);
|
|
541
781
|
}
|
|
542
782
|
function canonical(value) {
|
|
543
|
-
if (value === null ||
|
|
783
|
+
if (value === null ||
|
|
784
|
+
typeof value === "boolean" ||
|
|
785
|
+
typeof value === "string" ||
|
|
786
|
+
typeof value === "number")
|
|
544
787
|
return JSON.stringify(value);
|
|
545
788
|
if (Array.isArray(value))
|
|
546
789
|
return `[${value.map(canonical).join(",")}]`;
|
|
547
790
|
if (typeof value === "object")
|
|
548
|
-
return `{${Object.keys(value)
|
|
791
|
+
return `{${Object.keys(value)
|
|
792
|
+
.sort()
|
|
793
|
+
.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
|
|
794
|
+
.join(",")}}`;
|
|
549
795
|
throw new TrustLadderError("Trust data must be JSON.");
|
|
550
796
|
}
|
|
551
797
|
function sha256(value) {
|