@yiaany/ghostapi 0.1.9 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/CONTRIBUTING.md +132 -119
- package/LICENSE +21 -21
- package/PROVENANCE.md +13 -0
- package/README.md +78 -84
- package/SECURITY.md +56 -53
- package/dist/actions/gateway.js +320 -72
- package/dist/actions/gateway.js.map +1 -1
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/index.js +1 -1
- package/dist/actions/index.js.map +1 -1
- package/dist/agents/agentPrompt.js +7 -5
- package/dist/agents/agentPrompt.js.map +1 -1
- package/dist/ai/aiClient.js +5 -5
- package/dist/ai/aiClient.js.map +1 -1
- package/dist/ai/aiGenerator.js +66 -26
- package/dist/ai/aiGenerator.js.map +1 -1
- package/dist/ai/genericInference.js +151 -36
- package/dist/ai/genericInference.js.map +1 -1
- package/dist/ai/prompts.js +55 -49
- package/dist/ai/prompts.js.map +1 -1
- package/dist/approvals/approvalInbox.js +649 -133
- package/dist/approvals/approvalInbox.js.map +1 -1
- package/dist/approvals/index.d.ts +2 -2
- package/dist/approvals/index.js +1 -1
- package/dist/approvals/index.js.map +1 -1
- package/dist/behavior/behaviorStore.js +34 -14
- package/dist/behavior/behaviorStore.js.map +1 -1
- package/dist/cache/index.js +14 -5
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/index.js +226 -93
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/openUrl.js +17 -3
- package/dist/cli/openUrl.js.map +1 -1
- package/dist/cli/parser.js +79 -18
- package/dist/cli/parser.js.map +1 -1
- package/dist/config/dataPaths.js +4 -2
- package/dist/config/dataPaths.js.map +1 -1
- package/dist/config/localConfig.js +5 -2
- package/dist/config/localConfig.js.map +1 -1
- package/dist/config/serverConfig.js +11 -5
- package/dist/config/serverConfig.js.map +1 -1
- package/dist/contracts/contract.js +311 -83
- package/dist/contracts/contract.js.map +1 -1
- package/dist/contracts/index.d.ts +2 -2
- package/dist/contracts/index.js +1 -1
- package/dist/contracts/index.js.map +1 -1
- package/dist/credentials/credentialBroker.js +391 -79
- package/dist/credentials/credentialBroker.js.map +1 -1
- package/dist/credentials/index.d.ts +2 -2
- package/dist/credentials/index.js +1 -1
- package/dist/credentials/index.js.map +1 -1
- package/dist/dashboard/app.js +823 -685
- package/dist/dashboard/dashboard.js.map +1 -1
- package/dist/dashboard/index.html +704 -267
- package/dist/dashboard/styles.css +1040 -349
- package/dist/egress/capabilities.js +55 -25
- package/dist/egress/capabilities.js.map +1 -1
- package/dist/egress/linuxBootstrap.js +47 -11
- package/dist/egress/linuxBootstrap.js.map +1 -1
- package/dist/egress/run.js +170 -31
- package/dist/egress/run.js.map +1 -1
- package/dist/errors/errorEngine.js +15 -3
- package/dist/errors/errorEngine.js.map +1 -1
- package/dist/errors/providerErrors.js.map +1 -1
- package/dist/evals/evals.js +304 -78
- package/dist/evals/evals.js.map +1 -1
- package/dist/evals/index.d.ts +2 -2
- package/dist/evals/index.js +1 -1
- package/dist/evals/index.js.map +1 -1
- package/dist/evidence/index.d.ts +2 -2
- package/dist/evidence/index.js +1 -1
- package/dist/evidence/index.js.map +1 -1
- package/dist/evidence/report.d.ts +1 -1
- package/dist/evidence/report.js +263 -79
- package/dist/evidence/report.js.map +1 -1
- package/dist/fault/faultLab.js +24 -9
- package/dist/fault/faultLab.js.map +1 -1
- package/dist/index.d.ts +42 -42
- package/dist/index.js +21 -21
- package/dist/index.js.map +1 -1
- package/dist/inventory/analysis.d.ts +1 -1
- package/dist/inventory/analysis.js +174 -50
- package/dist/inventory/analysis.js.map +1 -1
- package/dist/inventory/index.d.ts +4 -4
- package/dist/inventory/index.js +3 -3
- package/dist/inventory/index.js.map +1 -1
- package/dist/inventory/inventoryController.js +362 -115
- package/dist/inventory/inventoryController.js.map +1 -1
- package/dist/inventory/types.js +680 -140
- package/dist/inventory/types.js.map +1 -1
- package/dist/ledger/actionLedger.js +355 -78
- package/dist/ledger/actionLedger.js.map +1 -1
- package/dist/ledger/index.d.ts +2 -2
- package/dist/ledger/index.js +1 -1
- package/dist/ledger/index.js.map +1 -1
- package/dist/mcp/server.js +26 -11
- package/dist/mcp/server.js.map +1 -1
- package/dist/policy/index.d.ts +2 -2
- package/dist/policy/index.js +1 -1
- package/dist/policy/index.js.map +1 -1
- package/dist/policy/policy.js +112 -32
- package/dist/policy/policy.js.map +1 -1
- package/dist/productTelemetry/index.js +71 -20
- package/dist/productTelemetry/index.js.map +1 -1
- package/dist/providers/conformance.js +10 -5
- package/dist/providers/conformance.js.map +1 -1
- package/dist/providers/discord.js +2 -2
- package/dist/providers/generic.js +3 -3
- package/dist/providers/github.js +2 -2
- package/dist/providers/index.d.ts +3 -3
- package/dist/providers/index.js +2 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.js +3 -3
- package/dist/providers/packs/resendPack.js +51 -25
- package/dist/providers/packs/resendPack.js.map +1 -1
- package/dist/providers/packs/stripePack.js +590 -121
- package/dist/providers/packs/stripePack.js.map +1 -1
- package/dist/providers/registry.js +4 -4
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/runtime.js +7 -6
- package/dist/providers/runtime.js.map +1 -1
- package/dist/providers/stripeWebhook.js +39 -10
- package/dist/providers/stripeWebhook.js.map +1 -1
- package/dist/providers/twilio.js +2 -2
- package/dist/proxy/cacheKey.js +9 -2
- package/dist/proxy/cacheKey.js.map +1 -1
- package/dist/proxy/providerDetector.js +22 -9
- package/dist/proxy/providerDetector.js.map +1 -1
- package/dist/proxy/proxyHandler.js +57 -22
- package/dist/proxy/proxyHandler.js.map +1 -1
- package/dist/proxy/requestNormalizer.js +4 -2
- package/dist/proxy/requestNormalizer.js.map +1 -1
- package/dist/reliability/costGovernance.js +211 -49
- package/dist/reliability/costGovernance.js.map +1 -1
- package/dist/reliability/index.d.ts +8 -8
- package/dist/reliability/index.js +4 -4
- package/dist/reliability/index.js.map +1 -1
- package/dist/reliability/reconciliation.js +274 -65
- package/dist/reliability/reconciliation.js.map +1 -1
- package/dist/reliability/runtimeHealth.js +127 -35
- package/dist/reliability/runtimeHealth.js.map +1 -1
- package/dist/reliability/slo.js +257 -50
- package/dist/reliability/slo.js.map +1 -1
- package/dist/report/safetyReport.d.ts +6 -0
- package/dist/report/safetyReport.js +56 -12
- package/dist/report/safetyReport.js.map +1 -1
- package/dist/rules/aiRules.js +5 -5
- package/dist/rules/aiRules.js.map +1 -1
- package/dist/safety/index.d.ts +2 -2
- package/dist/safety/index.js +1 -1
- package/dist/safety/index.js.map +1 -1
- package/dist/safety/safetyController.js +675 -165
- package/dist/safety/safetyController.js.map +1 -1
- package/dist/scenarios/scenarioBundle.js +204 -71
- package/dist/scenarios/scenarioBundle.js.map +1 -1
- package/dist/scenarios/scenarioStore.js +88 -29
- package/dist/scenarios/scenarioStore.js.map +1 -1
- package/dist/security/headerSanitizer.js +52 -10
- package/dist/security/headerSanitizer.js.map +1 -1
- package/dist/security/secrets.js +10 -5
- package/dist/security/secrets.js.map +1 -1
- package/dist/server/accessControl.js +37 -10
- package/dist/server/accessControl.js.map +1 -1
- package/dist/server/createServer.js +1 -1
- package/dist/server/createServer.js.map +1 -1
- package/dist/server/eventsStore.js +15 -5
- package/dist/server/eventsStore.js.map +1 -1
- package/dist/server/routes.js +61 -19
- package/dist/server/routes.js.map +1 -1
- package/dist/server/sse.js +1 -1
- package/dist/server/sse.js.map +1 -1
- package/dist/setup/sdkDetector.js +7 -2
- package/dist/setup/sdkDetector.js.map +1 -1
- package/dist/setup/setupGenerator.js +136 -48
- package/dist/setup/setupGenerator.js.map +1 -1
- package/dist/state/stateExtractor.js +3 -1
- package/dist/state/stateExtractor.js.map +1 -1
- package/dist/state/stateResolver.js +10 -7
- package/dist/state/stateResolver.js.map +1 -1
- package/dist/state/stateStore.js +13 -4
- package/dist/state/stateStore.js.map +1 -1
- package/dist/storage/fileStore.js +12 -4
- package/dist/storage/fileStore.js.map +1 -1
- package/dist/teamControl/controlPlane.js +846 -174
- package/dist/teamControl/controlPlane.js.map +1 -1
- package/dist/teamControl/deployment.js +19 -5
- package/dist/teamControl/deployment.js.map +1 -1
- package/dist/teamControl/index.d.ts +4 -4
- package/dist/teamControl/index.js +2 -2
- package/dist/teamControl/index.js.map +1 -1
- package/dist/tests/testGenerator.js +15 -7
- package/dist/tests/testGenerator.js.map +1 -1
- package/dist/trust/index.d.ts +2 -2
- package/dist/trust/index.js +1 -1
- package/dist/trust/index.js.map +1 -1
- package/dist/trust/trustLadder.js +309 -63
- package/dist/trust/trustLadder.js.map +1 -1
- package/dist/worlds/index.d.ts +2 -2
- package/dist/worlds/index.js +1 -1
- package/dist/worlds/index.js.map +1 -1
- package/dist/worlds/worlds.js +364 -65
- package/dist/worlds/worlds.js.map +1 -1
- package/docs/assets/dashboard.png +0 -0
- package/docs/ci.md +49 -49
- package/docs/github-actions.md +84 -84
- package/docs/mcp.md +58 -52
- package/docs/policy.md +63 -61
- package/docs/providers/authoring-packs.md +180 -175
- package/docs/providers/stripe-core-pack.md +84 -82
- package/docs/releases/README.md +13 -0
- package/docs/security/action-gateway-threat-model.md +34 -34
- package/docs/security/action-ledger-incident-replay-threat-model.md +42 -42
- package/docs/security/approval-inbox-threat-model.md +35 -35
- package/docs/security/credential-broker-threat-model.md +52 -52
- package/docs/security/egress-threat-model.md +126 -126
- package/docs/security/inventory-threat-model.md +104 -104
- package/docs/security/kill-switch-budgets-threat-model.md +37 -37
- package/docs/security/reliability-threat-model.md +111 -111
- package/docs/security/trust-ladder-threat-model.md +33 -33
- package/docs/telemetry.md +14 -0
- package/docs/usage.md +680 -571
- package/examples/README.md +10 -10
- package/examples/agent-instructions/README.md +39 -39
- package/examples/ci-smoke/README.md +21 -21
- package/examples/ci-smoke/ghostapi.policy.yaml +22 -22
- package/examples/ci-smoke/package.json +9 -9
- package/examples/ci-smoke/production-egress.mjs +4 -2
- package/examples/ci-smoke/safe.mjs +21 -17
- package/examples/evals/README.md +24 -24
- package/examples/evals/retry-after.eval.json +71 -52
- package/examples/generic-rest/README.md +27 -27
- package/examples/github-fetch/README.md +38 -35
- package/examples/openai-streaming/README.md +16 -16
- package/examples/openai-streaming/streaming-tool-call.mjs +44 -31
- package/examples/policy/ghostapi.policy.yaml +27 -27
- package/examples/record-replay/README.md +19 -19
- package/examples/record-replay/replay-requests.json +22 -22
- package/examples/record-replay/stripe-checkout.har.json +30 -30
- package/examples/resend-node/README.md +22 -22
- package/examples/stripe-node/README.md +35 -35
- package/examples/stripe-node/checkout-flow.mjs +87 -61
- package/examples/twilio-node/README.md +21 -21
- package/examples/worlds/README.md +19 -19
- package/examples/worlds/subscription-recovery.mjs +21 -12
- package/package.json +28 -6
- package/dist/landing/assets/index-BXR41fAD.css +0 -1
- package/dist/landing/assets/index-CxWsopzP.css +0 -1
- package/dist/landing/assets/index-D8_ggMRE.js +0 -55
- package/dist/landing/assets/index-DZlpyhqd.js +0 -55
- package/dist/landing/assets/index-Dk-Xezs3.js +0 -55
- package/dist/landing/index.html +0 -15
- package/dist/landing/landing.d.ts +0 -3
- package/dist/landing/landing.js +0 -12
- package/dist/landing/landing.js.map +0 -1
- package/docs/commercial/README.md +0 -29
- package/docs/commercial/data-inventory.md +0 -36
- package/docs/commercial/manual-invoicing.md +0 -44
- package/docs/commercial/metrics.md +0 -51
- package/docs/commercial/pricing.md +0 -103
- package/docs/commercial/privacy-policy-draft.md +0 -41
- package/docs/commercial/terms-topics-for-counsel.md +0 -51
- package/docs/design-partners/README.md +0 -52
- package/docs/design-partners/demo-narratives.md +0 -33
- package/docs/design-partners/design-partner-offer.md +0 -39
- package/docs/design-partners/discovery-questions.md +0 -28
- package/docs/design-partners/feedback-capture-template.md +0 -48
- package/docs/design-partners/icp-one-pager.md +0 -43
- package/docs/design-partners/onboarding-checklist.md +0 -33
- package/docs/design-partners/pilot-success-criteria.md +0 -27
- package/docs/design-partners/pricing-interview-script.md +0 -18
- package/docs/design-partners/telemetry-plan.md +0 -48
- package/docs/development/baseline.md +0 -114
- package/docs/development/onboarding-smoke.md +0 -37
- package/docs/development/verification-0.1.8.md +0 -25
- package/docs/enterprise-product-roadmap-ru.md +0 -1879
- package/docs/fundraising/README.md +0 -87
- package/docs/fundraising/data-room-checklist.md +0 -72
- package/docs/fundraising/demo-script.md +0 -52
- package/docs/fundraising/design-partners-50.md +0 -74
- package/docs/fundraising/launch-posts.md +0 -37
- package/docs/fundraising/metrics-and-evidence.md +0 -30
- package/docs/fundraising/roadmap-12-month.md +0 -25
- package/docs/fundraising/technical-due-diligence-index.md +0 -37
- package/docs/fundraising/yc-application.md +0 -25
- package/docs/hosted-pilot.md +0 -111
- package/docs/operations/disaster-recovery-runbook.md +0 -81
- package/docs/operations/kill-switch-runbook.md +0 -32
- package/docs/release-checklist.md +0 -83
- package/docs/release-migration-and-rollback.md +0 -63
- package/docs/release-readiness.md +0 -39
- package/docs/team-control-plane.md +0 -72
package/dist/worlds/worlds.js
CHANGED
|
@@ -2,8 +2,8 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { lstat, readFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { getDataPaths } from "../config/dataPaths.js";
|
|
5
|
-
import { isSecretFieldName, sanitizeSecretString } from "../security/secrets.js";
|
|
6
|
-
import { atomicWriteJson, ensurePrivateDirectory, withFileLock } from "../storage/fileStore.js";
|
|
5
|
+
import { isSecretFieldName, sanitizeSecretString, } from "../security/secrets.js";
|
|
6
|
+
import { atomicWriteJson, ensurePrivateDirectory, withFileLock, } from "../storage/fileStore.js";
|
|
7
7
|
const WORLD_SCHEMA_VERSION = 1;
|
|
8
8
|
const WORLD_KIND = "ghostapi.world";
|
|
9
9
|
const WORLD_VERSION = "1.0.0";
|
|
@@ -35,22 +35,62 @@ export function createSyntheticWorld(options) {
|
|
|
35
35
|
version: WORLD_VERSION,
|
|
36
36
|
seed,
|
|
37
37
|
clock: { initialAt, timezone: "UTC" },
|
|
38
|
-
personas: [
|
|
39
|
-
|
|
38
|
+
personas: [
|
|
39
|
+
{
|
|
40
|
+
id: personaId,
|
|
41
|
+
displayName: `Operator ${suffix.toUpperCase()}`,
|
|
42
|
+
handle: `operator-${suffix}`,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
organizations: [
|
|
46
|
+
{ id: organizationId, name: `Sandbox ${suffix.toUpperCase()}`, slug },
|
|
47
|
+
],
|
|
40
48
|
accounts: {
|
|
41
|
-
stripe: {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
stripe: {
|
|
50
|
+
accountId: `acct_world_${suffix}`,
|
|
51
|
+
customerPrefix: `cus_world_${suffix}`,
|
|
52
|
+
},
|
|
53
|
+
github: {
|
|
54
|
+
userLogin: `world-${suffix}`,
|
|
55
|
+
owner: `sandbox-${suffix}`,
|
|
56
|
+
repository: `integration-${suffix}`,
|
|
57
|
+
},
|
|
58
|
+
email: {
|
|
59
|
+
domain: "ghostapi.invalid",
|
|
60
|
+
inboxAddress: `operator-${suffix}@ghostapi.invalid`,
|
|
61
|
+
},
|
|
62
|
+
genericRest: {
|
|
63
|
+
tenantId: `tenant_world_${suffix}`,
|
|
64
|
+
basePath: `/tenants/tenant_world_${suffix}`,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
resources: {
|
|
68
|
+
defaultPriceId: `price_world_${suffix}`,
|
|
69
|
+
recoveryIssueLabel: "payment-recovery",
|
|
45
70
|
},
|
|
46
|
-
resources: { defaultPriceId: `price_world_${suffix}`, recoveryIssueLabel: "payment-recovery" },
|
|
47
71
|
relationships: [{ personaId, organizationId, role: "owner" }],
|
|
48
72
|
providerProjections: {
|
|
49
|
-
stripe: {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
73
|
+
stripe: {
|
|
74
|
+
personaId,
|
|
75
|
+
organizationId,
|
|
76
|
+
customerPrefix: `cus_world_${suffix}`,
|
|
77
|
+
},
|
|
78
|
+
github: {
|
|
79
|
+
personaId,
|
|
80
|
+
organizationId,
|
|
81
|
+
repository: `sandbox-${suffix}/integration-${suffix}`,
|
|
82
|
+
},
|
|
83
|
+
email: {
|
|
84
|
+
personaId,
|
|
85
|
+
organizationId,
|
|
86
|
+
inboxAddress: `operator-${suffix}@ghostapi.invalid`,
|
|
87
|
+
},
|
|
88
|
+
genericRest: {
|
|
89
|
+
personaId,
|
|
90
|
+
organizationId,
|
|
91
|
+
tenantId: `tenant_world_${suffix}`,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
54
94
|
};
|
|
55
95
|
const initialState = {
|
|
56
96
|
clockAt: initialAt,
|
|
@@ -58,9 +98,16 @@ export function createSyntheticWorld(options) {
|
|
|
58
98
|
github: { issues: [] },
|
|
59
99
|
email: { messages: [] },
|
|
60
100
|
genericRest: { failures: [] },
|
|
61
|
-
receipts: []
|
|
101
|
+
receipts: [],
|
|
62
102
|
};
|
|
63
|
-
return validateSyntheticWorld({
|
|
103
|
+
return validateSyntheticWorld({
|
|
104
|
+
schemaVersion: WORLD_SCHEMA_VERSION,
|
|
105
|
+
kind: WORLD_KIND,
|
|
106
|
+
manifest,
|
|
107
|
+
revision: 0,
|
|
108
|
+
baseline: initialState,
|
|
109
|
+
state: initialState,
|
|
110
|
+
});
|
|
64
111
|
}
|
|
65
112
|
export async function createWorld(options) {
|
|
66
113
|
const world = createSyntheticWorld(options);
|
|
@@ -81,7 +128,7 @@ export async function resetWorld(id) {
|
|
|
81
128
|
return mutateWorld(normalizeIdentifier(id, "World id"), (world) => ({
|
|
82
129
|
...world,
|
|
83
130
|
revision: world.revision + 1,
|
|
84
|
-
state: structuredClone(world.baseline)
|
|
131
|
+
state: structuredClone(world.baseline),
|
|
85
132
|
}));
|
|
86
133
|
}
|
|
87
134
|
export async function forkWorld(sourceId, options) {
|
|
@@ -92,7 +139,10 @@ export async function forkWorld(sourceId, options) {
|
|
|
92
139
|
const fork = structuredClone(source);
|
|
93
140
|
fork.manifest.id = id;
|
|
94
141
|
fork.manifest.title = title;
|
|
95
|
-
fork.manifest.lineage = {
|
|
142
|
+
fork.manifest.lineage = {
|
|
143
|
+
parentId: source.manifest.id,
|
|
144
|
+
parentRevision: source.revision,
|
|
145
|
+
};
|
|
96
146
|
fork.revision = 0;
|
|
97
147
|
fork.baseline = structuredClone(source.state);
|
|
98
148
|
fork.state = structuredClone(source.state);
|
|
@@ -131,18 +181,65 @@ export async function runSubscriptionFailureWorkflow(id, actionId, commit) {
|
|
|
131
181
|
emailId,
|
|
132
182
|
issueNumber: world.state.github.issues.length + 1,
|
|
133
183
|
genericFailureId,
|
|
134
|
-
completedAt: nextTimestamp(world.state.clockAt)
|
|
184
|
+
completedAt: nextTimestamp(world.state.clockAt),
|
|
135
185
|
};
|
|
136
186
|
const nextState = {
|
|
137
187
|
clockAt: receipt.completedAt,
|
|
138
188
|
stripe: {
|
|
139
|
-
customers: [
|
|
140
|
-
|
|
189
|
+
customers: [
|
|
190
|
+
...world.state.stripe.customers,
|
|
191
|
+
{
|
|
192
|
+
id: customerId,
|
|
193
|
+
personaId: persona.id,
|
|
194
|
+
organizationId: organization.id,
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
subscriptions: [
|
|
198
|
+
...world.state.stripe.subscriptions,
|
|
199
|
+
{
|
|
200
|
+
id: subscriptionId,
|
|
201
|
+
customerId,
|
|
202
|
+
priceId: world.manifest.resources.defaultPriceId,
|
|
203
|
+
status: "past_due",
|
|
204
|
+
failureCode: "card_declined",
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
github: {
|
|
209
|
+
issues: [
|
|
210
|
+
...world.state.github.issues,
|
|
211
|
+
{
|
|
212
|
+
number: receipt.issueNumber,
|
|
213
|
+
repository: world.manifest.providerProjections.github.repository,
|
|
214
|
+
title: `Recover ${subscriptionId}`,
|
|
215
|
+
status: "open",
|
|
216
|
+
relatedSubscriptionId: subscriptionId,
|
|
217
|
+
},
|
|
218
|
+
],
|
|
141
219
|
},
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
220
|
+
email: {
|
|
221
|
+
messages: [
|
|
222
|
+
...world.state.email.messages,
|
|
223
|
+
{
|
|
224
|
+
id: emailId,
|
|
225
|
+
to: world.manifest.accounts.email.inboxAddress,
|
|
226
|
+
template: "subscription-payment-failed",
|
|
227
|
+
relatedSubscriptionId: subscriptionId,
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
genericRest: {
|
|
232
|
+
failures: [
|
|
233
|
+
...world.state.genericRest.failures,
|
|
234
|
+
{
|
|
235
|
+
id: genericFailureId,
|
|
236
|
+
tenantId: world.manifest.accounts.genericRest.tenantId,
|
|
237
|
+
code: "payment_failed",
|
|
238
|
+
relatedSubscriptionId: subscriptionId,
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
receipts: [...world.state.receipts, receipt],
|
|
146
243
|
};
|
|
147
244
|
return { ...world, revision: world.revision + 1, state: nextState };
|
|
148
245
|
}, commit);
|
|
@@ -163,7 +260,7 @@ export function formatWorld(world) {
|
|
|
163
260
|
`Persona: ${world.manifest.personas[0].id}`,
|
|
164
261
|
`Organization: ${world.manifest.organizations[0].id}`,
|
|
165
262
|
`Providers: stripe, github, email, generic-rest`,
|
|
166
|
-
`Workflow receipts: ${world.state.receipts.length}
|
|
263
|
+
`Workflow receipts: ${world.state.receipts.length}`,
|
|
167
264
|
].join("\n");
|
|
168
265
|
}
|
|
169
266
|
export function validateSyntheticWorld(value) {
|
|
@@ -171,12 +268,21 @@ export function validateSyntheticWorld(value) {
|
|
|
171
268
|
assertExactKeys(root, ["schemaVersion", "kind", "manifest", "revision", "baseline", "state"], "Synthetic world");
|
|
172
269
|
if (root.schemaVersion !== WORLD_SCHEMA_VERSION || root.kind !== WORLD_KIND)
|
|
173
270
|
throw new SyntheticWorldError("Unsupported synthetic world schema.");
|
|
174
|
-
if (!Number.isInteger(root.revision) ||
|
|
271
|
+
if (!Number.isInteger(root.revision) ||
|
|
272
|
+
root.revision < 0 ||
|
|
273
|
+
root.revision > 1_000_000)
|
|
175
274
|
throw new SyntheticWorldError("Synthetic world revision is invalid.");
|
|
176
275
|
const manifest = normalizeManifest(root.manifest);
|
|
177
276
|
const baseline = normalizeState(root.baseline, manifest);
|
|
178
277
|
const state = normalizeState(root.state, manifest);
|
|
179
|
-
const world = {
|
|
278
|
+
const world = {
|
|
279
|
+
schemaVersion: 1,
|
|
280
|
+
kind: "ghostapi.world",
|
|
281
|
+
manifest,
|
|
282
|
+
revision: root.revision,
|
|
283
|
+
baseline,
|
|
284
|
+
state,
|
|
285
|
+
};
|
|
180
286
|
assertWorldSize(world);
|
|
181
287
|
return world;
|
|
182
288
|
}
|
|
@@ -224,7 +330,20 @@ async function ensureWorldFileIsSafe(path, required) {
|
|
|
224
330
|
}
|
|
225
331
|
function normalizeManifest(value) {
|
|
226
332
|
const manifest = readObject(value, "Synthetic world manifest is invalid.");
|
|
227
|
-
assertExactKeys(manifest, [
|
|
333
|
+
assertExactKeys(manifest, [
|
|
334
|
+
"id",
|
|
335
|
+
"title",
|
|
336
|
+
"version",
|
|
337
|
+
"seed",
|
|
338
|
+
"clock",
|
|
339
|
+
"personas",
|
|
340
|
+
"organizations",
|
|
341
|
+
"accounts",
|
|
342
|
+
"resources",
|
|
343
|
+
"relationships",
|
|
344
|
+
"providerProjections",
|
|
345
|
+
"lineage",
|
|
346
|
+
], "Synthetic world manifest");
|
|
228
347
|
const id = normalizeIdentifier(manifest.id, "World id");
|
|
229
348
|
const title = normalizeTitle(manifest.title);
|
|
230
349
|
if (manifest.version !== WORLD_VERSION)
|
|
@@ -240,22 +359,49 @@ function normalizeManifest(value) {
|
|
|
240
359
|
const resources = normalizeResources(manifest.resources);
|
|
241
360
|
const relationships = normalizeRelationships(manifest.relationships, personas[0].id, organizations[0].id);
|
|
242
361
|
const providerProjections = normalizeProjections(manifest.providerProjections, personas[0].id, organizations[0].id, accounts);
|
|
243
|
-
const lineage = manifest.lineage === undefined
|
|
244
|
-
|
|
362
|
+
const lineage = manifest.lineage === undefined
|
|
363
|
+
? undefined
|
|
364
|
+
: normalizeLineage(manifest.lineage);
|
|
365
|
+
return {
|
|
366
|
+
id,
|
|
367
|
+
title,
|
|
368
|
+
version: WORLD_VERSION,
|
|
369
|
+
seed,
|
|
370
|
+
clock: { initialAt: clock.initialAt, timezone: "UTC" },
|
|
371
|
+
personas,
|
|
372
|
+
organizations,
|
|
373
|
+
accounts,
|
|
374
|
+
resources,
|
|
375
|
+
relationships,
|
|
376
|
+
providerProjections,
|
|
377
|
+
...(lineage === undefined ? {} : { lineage }),
|
|
378
|
+
};
|
|
245
379
|
}
|
|
246
380
|
function normalizePersonas(value) {
|
|
247
381
|
if (!Array.isArray(value) || value.length !== 1)
|
|
248
382
|
throw new SyntheticWorldError("Synthetic world requires exactly one synthetic persona.");
|
|
249
383
|
const persona = readObject(value[0], "Synthetic persona is invalid.");
|
|
250
384
|
assertExactKeys(persona, ["id", "displayName", "handle"], "Synthetic persona");
|
|
251
|
-
return [
|
|
385
|
+
return [
|
|
386
|
+
{
|
|
387
|
+
id: normalizeIdentifier(persona.id, "Synthetic persona id"),
|
|
388
|
+
displayName: normalizeSafeText(persona.displayName, "Synthetic persona displayName", 80),
|
|
389
|
+
handle: normalizeIdentifier(persona.handle, "Synthetic persona handle"),
|
|
390
|
+
},
|
|
391
|
+
];
|
|
252
392
|
}
|
|
253
393
|
function normalizeOrganizations(value) {
|
|
254
394
|
if (!Array.isArray(value) || value.length !== 1)
|
|
255
395
|
throw new SyntheticWorldError("Synthetic world requires exactly one synthetic organization.");
|
|
256
396
|
const organization = readObject(value[0], "Synthetic organization is invalid.");
|
|
257
397
|
assertExactKeys(organization, ["id", "name", "slug"], "Synthetic organization");
|
|
258
|
-
return [
|
|
398
|
+
return [
|
|
399
|
+
{
|
|
400
|
+
id: normalizeIdentifier(organization.id, "Synthetic organization id"),
|
|
401
|
+
name: normalizeSafeText(organization.name, "Synthetic organization name", 80),
|
|
402
|
+
slug: normalizeIdentifier(organization.slug, "Synthetic organization slug"),
|
|
403
|
+
},
|
|
404
|
+
];
|
|
259
405
|
}
|
|
260
406
|
function normalizeAccounts(value) {
|
|
261
407
|
const accounts = readObject(value, "Synthetic world accounts are invalid.");
|
|
@@ -273,23 +419,38 @@ function normalizeAccounts(value) {
|
|
|
273
419
|
if (domain !== "ghostapi.invalid")
|
|
274
420
|
throw new SyntheticWorldError("Synthetic world email addresses must use ghostapi.invalid.");
|
|
275
421
|
return {
|
|
276
|
-
stripe: {
|
|
277
|
-
|
|
422
|
+
stripe: {
|
|
423
|
+
accountId: normalizeIdentifier(stripe.accountId, "Synthetic Stripe account id"),
|
|
424
|
+
customerPrefix: normalizeIdentifier(stripe.customerPrefix, "Synthetic Stripe customer prefix"),
|
|
425
|
+
},
|
|
426
|
+
github: {
|
|
427
|
+
userLogin: normalizeIdentifier(github.userLogin, "Synthetic GitHub login"),
|
|
428
|
+
owner: normalizeIdentifier(github.owner, "Synthetic GitHub owner"),
|
|
429
|
+
repository: normalizeIdentifier(github.repository, "Synthetic GitHub repository"),
|
|
430
|
+
},
|
|
278
431
|
email: { domain, inboxAddress },
|
|
279
|
-
genericRest: {
|
|
432
|
+
genericRest: {
|
|
433
|
+
tenantId: normalizeIdentifier(genericRest.tenantId, "Synthetic tenant id"),
|
|
434
|
+
basePath: normalizeBasePath(genericRest.basePath),
|
|
435
|
+
},
|
|
280
436
|
};
|
|
281
437
|
}
|
|
282
438
|
function normalizeResources(value) {
|
|
283
439
|
const resources = readObject(value, "Synthetic world resources are invalid.");
|
|
284
440
|
assertExactKeys(resources, ["defaultPriceId", "recoveryIssueLabel"], "Synthetic world resources");
|
|
285
|
-
return {
|
|
441
|
+
return {
|
|
442
|
+
defaultPriceId: normalizeIdentifier(resources.defaultPriceId, "Synthetic price id"),
|
|
443
|
+
recoveryIssueLabel: normalizeIdentifier(resources.recoveryIssueLabel, "Synthetic issue label"),
|
|
444
|
+
};
|
|
286
445
|
}
|
|
287
446
|
function normalizeRelationships(value, personaId, organizationId) {
|
|
288
447
|
if (!Array.isArray(value) || value.length !== 1)
|
|
289
448
|
throw new SyntheticWorldError("Synthetic world requires one persona-to-organization relationship.");
|
|
290
449
|
const relationship = readObject(value[0], "Synthetic world relationship is invalid.");
|
|
291
450
|
assertExactKeys(relationship, ["personaId", "organizationId", "role"], "Synthetic world relationship");
|
|
292
|
-
if (relationship.personaId !== personaId ||
|
|
451
|
+
if (relationship.personaId !== personaId ||
|
|
452
|
+
relationship.organizationId !== organizationId ||
|
|
453
|
+
relationship.role !== "owner")
|
|
293
454
|
throw new SyntheticWorldError("Synthetic world relationship does not match canonical identity.");
|
|
294
455
|
return [{ personaId, organizationId, role: "owner" }];
|
|
295
456
|
}
|
|
@@ -304,17 +465,54 @@ function normalizeProjections(value, personaId, organizationId, accounts) {
|
|
|
304
465
|
assertExactKeys(github, ["personaId", "organizationId", "repository"], "Synthetic GitHub projection");
|
|
305
466
|
assertExactKeys(email, ["personaId", "organizationId", "inboxAddress"], "Synthetic email projection");
|
|
306
467
|
assertExactKeys(genericRest, ["personaId", "organizationId", "tenantId"], "Synthetic generic REST projection");
|
|
307
|
-
if (stripe.personaId !== personaId ||
|
|
468
|
+
if (stripe.personaId !== personaId ||
|
|
469
|
+
stripe.organizationId !== organizationId ||
|
|
470
|
+
stripe.customerPrefix !== accounts.stripe.customerPrefix ||
|
|
471
|
+
github.personaId !== personaId ||
|
|
472
|
+
github.organizationId !== organizationId ||
|
|
473
|
+
github.repository !==
|
|
474
|
+
`${accounts.github.owner}/${accounts.github.repository}` ||
|
|
475
|
+
email.personaId !== personaId ||
|
|
476
|
+
email.organizationId !== organizationId ||
|
|
477
|
+
email.inboxAddress !== accounts.email.inboxAddress ||
|
|
478
|
+
genericRest.personaId !== personaId ||
|
|
479
|
+
genericRest.organizationId !== organizationId ||
|
|
480
|
+
genericRest.tenantId !== accounts.genericRest.tenantId) {
|
|
308
481
|
throw new SyntheticWorldError("Provider projections do not match canonical synthetic identity.");
|
|
309
482
|
}
|
|
310
|
-
return {
|
|
483
|
+
return {
|
|
484
|
+
stripe: {
|
|
485
|
+
personaId,
|
|
486
|
+
organizationId,
|
|
487
|
+
customerPrefix: accounts.stripe.customerPrefix,
|
|
488
|
+
},
|
|
489
|
+
github: {
|
|
490
|
+
personaId,
|
|
491
|
+
organizationId,
|
|
492
|
+
repository: `${accounts.github.owner}/${accounts.github.repository}`,
|
|
493
|
+
},
|
|
494
|
+
email: {
|
|
495
|
+
personaId,
|
|
496
|
+
organizationId,
|
|
497
|
+
inboxAddress: accounts.email.inboxAddress,
|
|
498
|
+
},
|
|
499
|
+
genericRest: {
|
|
500
|
+
personaId,
|
|
501
|
+
organizationId,
|
|
502
|
+
tenantId: accounts.genericRest.tenantId,
|
|
503
|
+
},
|
|
504
|
+
};
|
|
311
505
|
}
|
|
312
506
|
function normalizeLineage(value) {
|
|
313
507
|
const lineage = readObject(value, "Synthetic world lineage is invalid.");
|
|
314
508
|
assertExactKeys(lineage, ["parentId", "parentRevision"], "Synthetic world lineage");
|
|
315
|
-
if (!Number.isInteger(lineage.parentRevision) ||
|
|
509
|
+
if (!Number.isInteger(lineage.parentRevision) ||
|
|
510
|
+
lineage.parentRevision < 0)
|
|
316
511
|
throw new SyntheticWorldError("Synthetic world lineage revision is invalid.");
|
|
317
|
-
return {
|
|
512
|
+
return {
|
|
513
|
+
parentId: normalizeIdentifier(lineage.parentId, "Synthetic world parent id"),
|
|
514
|
+
parentRevision: lineage.parentRevision,
|
|
515
|
+
};
|
|
318
516
|
}
|
|
319
517
|
function normalizeState(value, manifest) {
|
|
320
518
|
const state = readObject(value, "Synthetic world state is invalid.");
|
|
@@ -335,9 +533,19 @@ function normalizeState(value, manifest) {
|
|
|
335
533
|
const messages = normalizeMessages(email.messages, subscriptions, manifest);
|
|
336
534
|
const failures = normalizeFailures(genericRest.failures, subscriptions, manifest);
|
|
337
535
|
const receipts = normalizeReceipts(state.receipts, customers, subscriptions, messages, issues, failures, manifest);
|
|
338
|
-
if (customers.length !== subscriptions.length ||
|
|
536
|
+
if (customers.length !== subscriptions.length ||
|
|
537
|
+
customers.length !== issues.length ||
|
|
538
|
+
customers.length !== messages.length ||
|
|
539
|
+
customers.length !== failures.length ||
|
|
540
|
+
customers.length !== receipts.length)
|
|
339
541
|
throw new SyntheticWorldError("Synthetic world providers have inconsistent workflow state.");
|
|
340
|
-
if (new Set(customers.map((customer) => customer.id)).size !==
|
|
542
|
+
if (new Set(customers.map((customer) => customer.id)).size !==
|
|
543
|
+
customers.length ||
|
|
544
|
+
new Set(subscriptions.map((subscription) => subscription.id)).size !==
|
|
545
|
+
subscriptions.length ||
|
|
546
|
+
new Set(messages.map((message) => message.id)).size !== messages.length ||
|
|
547
|
+
new Set(issues.map((issue) => issue.number)).size !== issues.length ||
|
|
548
|
+
new Set(failures.map((failure) => failure.id)).size !== failures.length) {
|
|
341
549
|
throw new SyntheticWorldError("Synthetic world contains duplicate provider resources.");
|
|
342
550
|
}
|
|
343
551
|
const subscriptionsById = new Map(subscriptions.map((subscription) => [subscription.id, subscription]));
|
|
@@ -346,19 +554,37 @@ function normalizeState(value, manifest) {
|
|
|
346
554
|
const failuresById = new Map(failures.map((failure) => [failure.id, failure]));
|
|
347
555
|
for (const receipt of receipts) {
|
|
348
556
|
const subscription = subscriptionsById.get(receipt.subscriptionId);
|
|
349
|
-
if (subscription?.customerId !== receipt.customerId ||
|
|
557
|
+
if (subscription?.customerId !== receipt.customerId ||
|
|
558
|
+
messagesById.get(receipt.emailId)?.relatedSubscriptionId !==
|
|
559
|
+
receipt.subscriptionId ||
|
|
560
|
+
issuesByNumber.get(receipt.issueNumber)?.relatedSubscriptionId !==
|
|
561
|
+
receipt.subscriptionId ||
|
|
562
|
+
failuresById.get(receipt.genericFailureId)?.relatedSubscriptionId !==
|
|
563
|
+
receipt.subscriptionId) {
|
|
350
564
|
throw new SyntheticWorldError("Synthetic workflow receipt does not link one consistent cross-provider transition.");
|
|
351
565
|
}
|
|
352
566
|
}
|
|
353
|
-
return {
|
|
567
|
+
return {
|
|
568
|
+
clockAt: state.clockAt,
|
|
569
|
+
stripe: { customers, subscriptions },
|
|
570
|
+
github: { issues },
|
|
571
|
+
email: { messages },
|
|
572
|
+
genericRest: { failures },
|
|
573
|
+
receipts,
|
|
574
|
+
};
|
|
354
575
|
}
|
|
355
576
|
function normalizeCustomers(value, manifest) {
|
|
356
577
|
return readArray(value, "Synthetic Stripe customers", MAX_WORKFLOW_RECEIPTS).map((entry) => {
|
|
357
578
|
const customer = readObject(entry, "Synthetic Stripe customer is invalid.");
|
|
358
579
|
assertExactKeys(customer, ["id", "personaId", "organizationId"], "Synthetic Stripe customer");
|
|
359
|
-
if (customer.personaId !== manifest.personas[0].id ||
|
|
580
|
+
if (customer.personaId !== manifest.personas[0].id ||
|
|
581
|
+
customer.organizationId !== manifest.organizations[0].id)
|
|
360
582
|
throw new SyntheticWorldError("Synthetic Stripe customer identity is inconsistent.");
|
|
361
|
-
return {
|
|
583
|
+
return {
|
|
584
|
+
id: normalizeIdentifier(customer.id, "Synthetic Stripe customer id"),
|
|
585
|
+
personaId: manifest.personas[0].id,
|
|
586
|
+
organizationId: manifest.organizations[0].id,
|
|
587
|
+
};
|
|
362
588
|
});
|
|
363
589
|
}
|
|
364
590
|
function normalizeSubscriptions(value, customers, manifest) {
|
|
@@ -366,9 +592,18 @@ function normalizeSubscriptions(value, customers, manifest) {
|
|
|
366
592
|
return readArray(value, "Synthetic Stripe subscriptions", MAX_WORKFLOW_RECEIPTS).map((entry) => {
|
|
367
593
|
const subscription = readObject(entry, "Synthetic Stripe subscription is invalid.");
|
|
368
594
|
assertExactKeys(subscription, ["id", "customerId", "priceId", "status", "failureCode"], "Synthetic Stripe subscription");
|
|
369
|
-
if (!customerIds.has(subscription.customerId) ||
|
|
595
|
+
if (!customerIds.has(subscription.customerId) ||
|
|
596
|
+
subscription.priceId !== manifest.resources.defaultPriceId ||
|
|
597
|
+
subscription.status !== "past_due" ||
|
|
598
|
+
subscription.failureCode !== "card_declined")
|
|
370
599
|
throw new SyntheticWorldError("Synthetic Stripe subscription is inconsistent.");
|
|
371
|
-
return {
|
|
600
|
+
return {
|
|
601
|
+
id: normalizeIdentifier(subscription.id, "Synthetic Stripe subscription id"),
|
|
602
|
+
customerId: subscription.customerId,
|
|
603
|
+
priceId: manifest.resources.defaultPriceId,
|
|
604
|
+
status: "past_due",
|
|
605
|
+
failureCode: "card_declined",
|
|
606
|
+
};
|
|
372
607
|
});
|
|
373
608
|
}
|
|
374
609
|
function normalizeIssues(value, subscriptions, manifest) {
|
|
@@ -376,9 +611,20 @@ function normalizeIssues(value, subscriptions, manifest) {
|
|
|
376
611
|
return readArray(value, "Synthetic GitHub issues", MAX_WORKFLOW_RECEIPTS).map((entry) => {
|
|
377
612
|
const issue = readObject(entry, "Synthetic GitHub issue is invalid.");
|
|
378
613
|
assertExactKeys(issue, ["number", "repository", "title", "status", "relatedSubscriptionId"], "Synthetic GitHub issue");
|
|
379
|
-
if (!Number.isInteger(issue.number) ||
|
|
614
|
+
if (!Number.isInteger(issue.number) ||
|
|
615
|
+
issue.number < 1 ||
|
|
616
|
+
issue.repository !== manifest.providerProjections.github.repository ||
|
|
617
|
+
typeof issue.title !== "string" ||
|
|
618
|
+
issue.status !== "open" ||
|
|
619
|
+
!subscriptionsById.has(issue.relatedSubscriptionId))
|
|
380
620
|
throw new SyntheticWorldError("Synthetic GitHub issue is inconsistent.");
|
|
381
|
-
return {
|
|
621
|
+
return {
|
|
622
|
+
number: issue.number,
|
|
623
|
+
repository: manifest.providerProjections.github.repository,
|
|
624
|
+
title: normalizeSafeText(issue.title, "Synthetic GitHub issue title", 160),
|
|
625
|
+
status: "open",
|
|
626
|
+
relatedSubscriptionId: issue.relatedSubscriptionId,
|
|
627
|
+
};
|
|
382
628
|
});
|
|
383
629
|
}
|
|
384
630
|
function normalizeMessages(value, subscriptions, manifest) {
|
|
@@ -386,9 +632,16 @@ function normalizeMessages(value, subscriptions, manifest) {
|
|
|
386
632
|
return readArray(value, "Synthetic email messages", MAX_WORKFLOW_RECEIPTS).map((entry) => {
|
|
387
633
|
const message = readObject(entry, "Synthetic email message is invalid.");
|
|
388
634
|
assertExactKeys(message, ["id", "to", "template", "relatedSubscriptionId"], "Synthetic email message");
|
|
389
|
-
if (message.to !== manifest.accounts.email.inboxAddress ||
|
|
635
|
+
if (message.to !== manifest.accounts.email.inboxAddress ||
|
|
636
|
+
message.template !== "subscription-payment-failed" ||
|
|
637
|
+
!subscriptionsById.has(message.relatedSubscriptionId))
|
|
390
638
|
throw new SyntheticWorldError("Synthetic email message is inconsistent.");
|
|
391
|
-
return {
|
|
639
|
+
return {
|
|
640
|
+
id: normalizeIdentifier(message.id, "Synthetic email message id"),
|
|
641
|
+
to: manifest.accounts.email.inboxAddress,
|
|
642
|
+
template: "subscription-payment-failed",
|
|
643
|
+
relatedSubscriptionId: message.relatedSubscriptionId,
|
|
644
|
+
};
|
|
392
645
|
});
|
|
393
646
|
}
|
|
394
647
|
function normalizeFailures(value, subscriptions, manifest) {
|
|
@@ -396,9 +649,16 @@ function normalizeFailures(value, subscriptions, manifest) {
|
|
|
396
649
|
return readArray(value, "Synthetic generic REST failures", MAX_WORKFLOW_RECEIPTS).map((entry) => {
|
|
397
650
|
const failure = readObject(entry, "Synthetic generic REST failure is invalid.");
|
|
398
651
|
assertExactKeys(failure, ["id", "tenantId", "code", "relatedSubscriptionId"], "Synthetic generic REST failure");
|
|
399
|
-
if (failure.tenantId !== manifest.accounts.genericRest.tenantId ||
|
|
652
|
+
if (failure.tenantId !== manifest.accounts.genericRest.tenantId ||
|
|
653
|
+
failure.code !== "payment_failed" ||
|
|
654
|
+
!subscriptionsById.has(failure.relatedSubscriptionId))
|
|
400
655
|
throw new SyntheticWorldError("Synthetic generic REST failure is inconsistent.");
|
|
401
|
-
return {
|
|
656
|
+
return {
|
|
657
|
+
id: normalizeIdentifier(failure.id, "Synthetic generic REST failure id"),
|
|
658
|
+
tenantId: manifest.accounts.genericRest.tenantId,
|
|
659
|
+
code: "payment_failed",
|
|
660
|
+
relatedSubscriptionId: failure.relatedSubscriptionId,
|
|
661
|
+
};
|
|
402
662
|
});
|
|
403
663
|
}
|
|
404
664
|
function normalizeReceipts(value, customers, subscriptions, messages, issues, failures, manifest) {
|
|
@@ -410,21 +670,51 @@ function normalizeReceipts(value, customers, subscriptions, messages, issues, fa
|
|
|
410
670
|
const actionIds = new Set();
|
|
411
671
|
return readArray(value, "Synthetic workflow receipts", MAX_WORKFLOW_RECEIPTS).map((entry) => {
|
|
412
672
|
const receipt = readObject(entry, "Synthetic workflow receipt is invalid.");
|
|
413
|
-
assertExactKeys(receipt, [
|
|
673
|
+
assertExactKeys(receipt, [
|
|
674
|
+
"actionId",
|
|
675
|
+
"customerId",
|
|
676
|
+
"subscriptionId",
|
|
677
|
+
"emailId",
|
|
678
|
+
"issueNumber",
|
|
679
|
+
"genericFailureId",
|
|
680
|
+
"completedAt",
|
|
681
|
+
], "Synthetic workflow receipt");
|
|
414
682
|
const actionId = normalizeIdentifier(receipt.actionId, "Synthetic workflow action id");
|
|
415
|
-
if (actionIds.has(actionId) ||
|
|
683
|
+
if (actionIds.has(actionId) ||
|
|
684
|
+
!customerIds.has(receipt.customerId) ||
|
|
685
|
+
!subscriptionIds.has(receipt.subscriptionId) ||
|
|
686
|
+
!messageIds.has(receipt.emailId) ||
|
|
687
|
+
!issueNumbers.has(receipt.issueNumber) ||
|
|
688
|
+
!failureIds.has(receipt.genericFailureId) ||
|
|
689
|
+
!isTimestamp(receipt.completedAt) ||
|
|
690
|
+
receipt.completedAt < manifest.clock.initialAt)
|
|
416
691
|
throw new SyntheticWorldError("Synthetic workflow receipt is inconsistent.");
|
|
417
692
|
actionIds.add(actionId);
|
|
418
|
-
return {
|
|
693
|
+
return {
|
|
694
|
+
actionId,
|
|
695
|
+
customerId: receipt.customerId,
|
|
696
|
+
subscriptionId: receipt.subscriptionId,
|
|
697
|
+
emailId: receipt.emailId,
|
|
698
|
+
issueNumber: receipt.issueNumber,
|
|
699
|
+
genericFailureId: receipt.genericFailureId,
|
|
700
|
+
completedAt: receipt.completedAt,
|
|
701
|
+
};
|
|
419
702
|
});
|
|
420
703
|
}
|
|
421
704
|
function normalizeIdentifier(value, label) {
|
|
422
|
-
if (typeof value !== "string" ||
|
|
705
|
+
if (typeof value !== "string" ||
|
|
706
|
+
!IDENTIFIER.test(value) ||
|
|
707
|
+
sanitizeSecretString(value) !== value)
|
|
423
708
|
throw new SyntheticWorldError(`${label} must be a safe stable identifier.`);
|
|
424
709
|
return value;
|
|
425
710
|
}
|
|
426
711
|
function normalizeSeed(value) {
|
|
427
|
-
if (typeof value !== "string" ||
|
|
712
|
+
if (typeof value !== "string" ||
|
|
713
|
+
value.trim() === "" ||
|
|
714
|
+
value.length > MAX_SEED_LENGTH ||
|
|
715
|
+
/[\r\n\t]/.test(value) ||
|
|
716
|
+
sanitizeSecretString(value) !== value ||
|
|
717
|
+
hasObviousPii(value))
|
|
428
718
|
throw new SyntheticWorldError("World seed must be a non-secret, non-PII 1-128 character string.");
|
|
429
719
|
return value;
|
|
430
720
|
}
|
|
@@ -432,7 +722,12 @@ function normalizeTitle(value) {
|
|
|
432
722
|
return normalizeSafeText(value, "World title", 120);
|
|
433
723
|
}
|
|
434
724
|
function normalizeSafeText(value, label, maximumLength) {
|
|
435
|
-
if (typeof value !== "string" ||
|
|
725
|
+
if (typeof value !== "string" ||
|
|
726
|
+
value.trim() === "" ||
|
|
727
|
+
value.trim().length > maximumLength ||
|
|
728
|
+
/[\r\n\t]/.test(value) ||
|
|
729
|
+
sanitizeSecretString(value) !== value ||
|
|
730
|
+
hasObviousPii(value))
|
|
436
731
|
throw new SyntheticWorldError(`${label} must be safe non-PII plain text.`);
|
|
437
732
|
return value.trim();
|
|
438
733
|
}
|
|
@@ -442,7 +737,8 @@ function normalizeBasePath(value) {
|
|
|
442
737
|
return value;
|
|
443
738
|
}
|
|
444
739
|
function normalizeSyntheticInboxAddress(value) {
|
|
445
|
-
if (typeof value !== "string" ||
|
|
740
|
+
if (typeof value !== "string" ||
|
|
741
|
+
!/^operator-[a-f0-9]{12}@ghostapi\.invalid$/.test(value))
|
|
446
742
|
throw new SyntheticWorldError("Synthetic inbox address must use the generated ghostapi.invalid format.");
|
|
447
743
|
return value;
|
|
448
744
|
}
|
|
@@ -480,10 +776,13 @@ function nextTimestamp(value) {
|
|
|
480
776
|
return new Date(Date.parse(value) + 1000).toISOString();
|
|
481
777
|
}
|
|
482
778
|
function isTimestamp(value) {
|
|
483
|
-
return typeof value === "string" &&
|
|
779
|
+
return (typeof value === "string" &&
|
|
780
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) &&
|
|
781
|
+
Number.isFinite(Date.parse(value)));
|
|
484
782
|
}
|
|
485
783
|
function hasObviousPii(value) {
|
|
486
|
-
return /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i.test(value) ||
|
|
784
|
+
return (/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i.test(value) ||
|
|
785
|
+
/(?:\+\d[\d(). -]{7,}\d|\b\d{3}[ .-]\d{3}[ .-]\d{4}\b)/.test(value));
|
|
487
786
|
}
|
|
488
787
|
function isErrorCode(error, code) {
|
|
489
788
|
return error instanceof Error && "code" in error && error.code === code;
|