@yiaany/ghostapi 0.1.6 → 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.
Files changed (323) hide show
  1. package/CHANGELOG.md +48 -2
  2. package/CONTRIBUTING.md +119 -117
  3. package/LICENSE +21 -21
  4. package/README.md +208 -185
  5. package/SECURITY.md +53 -37
  6. package/dist/actions/gateway.d.ts +179 -0
  7. package/dist/actions/gateway.js +514 -0
  8. package/dist/actions/gateway.js.map +1 -0
  9. package/dist/actions/index.d.ts +2 -0
  10. package/dist/actions/index.js +2 -0
  11. package/dist/actions/index.js.map +1 -0
  12. package/dist/ai/aiClient.js +4 -4
  13. package/dist/ai/aiClient.js.map +1 -1
  14. package/dist/ai/aiGenerator.d.ts +2 -2
  15. package/dist/ai/aiGenerator.js +21 -72
  16. package/dist/ai/aiGenerator.js.map +1 -1
  17. package/dist/ai/prompts.js +51 -51
  18. package/dist/ai/prompts.js.map +1 -1
  19. package/dist/approvals/approvalInbox.d.ts +150 -0
  20. package/dist/approvals/approvalInbox.js +412 -0
  21. package/dist/approvals/approvalInbox.js.map +1 -0
  22. package/dist/approvals/index.d.ts +2 -0
  23. package/dist/approvals/index.js +2 -0
  24. package/dist/approvals/index.js.map +1 -0
  25. package/dist/behavior/behaviorStore.d.ts +2 -0
  26. package/dist/behavior/behaviorStore.js +50 -46
  27. package/dist/behavior/behaviorStore.js.map +1 -1
  28. package/dist/cache/index.d.ts +2 -0
  29. package/dist/cache/index.js +58 -14
  30. package/dist/cache/index.js.map +1 -1
  31. package/dist/cli/index.js +438 -58
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/cli/openUrl.d.ts +10 -0
  34. package/dist/cli/openUrl.js +31 -0
  35. package/dist/cli/openUrl.js.map +1 -0
  36. package/dist/cli/parser.d.ts +155 -0
  37. package/dist/cli/parser.js +504 -2
  38. package/dist/cli/parser.js.map +1 -1
  39. package/dist/config/dataPaths.d.ts +31 -0
  40. package/dist/config/dataPaths.js +45 -0
  41. package/dist/config/dataPaths.js.map +1 -0
  42. package/dist/config/localConfig.d.ts +1 -2
  43. package/dist/config/localConfig.js +27 -21
  44. package/dist/config/localConfig.js.map +1 -1
  45. package/dist/config/serverConfig.d.ts +2 -0
  46. package/dist/config/serverConfig.js +8 -1
  47. package/dist/config/serverConfig.js.map +1 -1
  48. package/dist/contracts/contract.d.ts +76 -0
  49. package/dist/contracts/contract.js +603 -0
  50. package/dist/contracts/contract.js.map +1 -0
  51. package/dist/contracts/index.d.ts +2 -0
  52. package/dist/contracts/index.js +2 -0
  53. package/dist/contracts/index.js.map +1 -0
  54. package/dist/credentials/credentialBroker.d.ts +207 -0
  55. package/dist/credentials/credentialBroker.js +588 -0
  56. package/dist/credentials/credentialBroker.js.map +1 -0
  57. package/dist/credentials/index.d.ts +2 -0
  58. package/dist/credentials/index.js +2 -0
  59. package/dist/credentials/index.js.map +1 -0
  60. package/dist/dashboard/app.js +685 -615
  61. package/dist/dashboard/index.html +13 -7
  62. package/dist/dashboard/styles.css +349 -349
  63. package/dist/egress/capabilities.d.ts +32 -0
  64. package/dist/egress/capabilities.js +165 -0
  65. package/dist/egress/capabilities.js.map +1 -0
  66. package/dist/egress/linuxBootstrap.d.ts +1 -0
  67. package/dist/egress/linuxBootstrap.js +121 -0
  68. package/dist/egress/linuxBootstrap.js.map +1 -0
  69. package/dist/egress/run.d.ts +18 -0
  70. package/dist/egress/run.js +305 -0
  71. package/dist/egress/run.js.map +1 -0
  72. package/dist/errors/errorEngine.d.ts +2 -2
  73. package/dist/errors/errorEngine.js +3 -40
  74. package/dist/errors/errorEngine.js.map +1 -1
  75. package/dist/evals/evals.d.ts +126 -0
  76. package/dist/evals/evals.js +466 -0
  77. package/dist/evals/evals.js.map +1 -0
  78. package/dist/evals/index.d.ts +2 -0
  79. package/dist/evals/index.js +2 -0
  80. package/dist/evals/index.js.map +1 -0
  81. package/dist/evidence/index.d.ts +2 -0
  82. package/dist/evidence/index.js +2 -0
  83. package/dist/evidence/index.js.map +1 -0
  84. package/dist/evidence/report.d.ts +143 -0
  85. package/dist/evidence/report.js +556 -0
  86. package/dist/evidence/report.js.map +1 -0
  87. package/dist/fault/faultLab.d.ts +5 -4
  88. package/dist/fault/faultLab.js +66 -24
  89. package/dist/fault/faultLab.js.map +1 -1
  90. package/dist/index.d.ts +46 -0
  91. package/dist/index.js +23 -0
  92. package/dist/index.js.map +1 -1
  93. package/dist/inventory/analysis.d.ts +12 -0
  94. package/dist/inventory/analysis.js +457 -0
  95. package/dist/inventory/analysis.js.map +1 -0
  96. package/dist/inventory/index.d.ts +5 -0
  97. package/dist/inventory/index.js +4 -0
  98. package/dist/inventory/index.js.map +1 -0
  99. package/dist/inventory/inventoryController.d.ts +53 -0
  100. package/dist/inventory/inventoryController.js +1044 -0
  101. package/dist/inventory/inventoryController.js.map +1 -0
  102. package/dist/inventory/types.d.ts +579 -0
  103. package/dist/inventory/types.js +787 -0
  104. package/dist/inventory/types.js.map +1 -0
  105. package/dist/landing/assets/index-CxWsopzP.css +1 -0
  106. package/dist/landing/assets/index-DZlpyhqd.js +55 -0
  107. package/dist/landing/assets/index-Dk-Xezs3.js +55 -0
  108. package/dist/landing/index.html +15 -15
  109. package/dist/ledger/actionLedger.d.ts +125 -0
  110. package/dist/ledger/actionLedger.js +594 -0
  111. package/dist/ledger/actionLedger.js.map +1 -0
  112. package/dist/ledger/index.d.ts +2 -0
  113. package/dist/ledger/index.js +2 -0
  114. package/dist/ledger/index.js.map +1 -0
  115. package/dist/mcp/server.js +4 -3
  116. package/dist/mcp/server.js.map +1 -1
  117. package/dist/policy/index.d.ts +3 -0
  118. package/dist/policy/index.js +2 -0
  119. package/dist/policy/index.js.map +1 -0
  120. package/dist/policy/policy.d.ts +14 -0
  121. package/dist/policy/policy.js +282 -0
  122. package/dist/policy/policy.js.map +1 -0
  123. package/dist/policy/types.d.ts +52 -0
  124. package/dist/policy/types.js +2 -0
  125. package/dist/policy/types.js.map +1 -0
  126. package/dist/productTelemetry/index.d.ts +16 -0
  127. package/dist/productTelemetry/index.js +142 -0
  128. package/dist/productTelemetry/index.js.map +1 -0
  129. package/dist/providers/conformance.d.ts +7 -0
  130. package/dist/providers/conformance.js +48 -0
  131. package/dist/providers/conformance.js.map +1 -0
  132. package/dist/providers/index.d.ts +4 -2
  133. package/dist/providers/index.js +3 -1
  134. package/dist/providers/index.js.map +1 -1
  135. package/dist/providers/packs/resendPack.d.ts +2 -0
  136. package/dist/providers/packs/resendPack.js +147 -0
  137. package/dist/providers/packs/resendPack.js.map +1 -0
  138. package/dist/providers/packs/stripePack.d.ts +2 -0
  139. package/dist/providers/packs/stripePack.js +852 -0
  140. package/dist/providers/packs/stripePack.js.map +1 -0
  141. package/dist/providers/registry.d.ts +7 -3
  142. package/dist/providers/registry.js +55 -4
  143. package/dist/providers/registry.js.map +1 -1
  144. package/dist/providers/resend.js +2 -11
  145. package/dist/providers/resend.js.map +1 -1
  146. package/dist/providers/runtime.d.ts +8 -0
  147. package/dist/providers/runtime.js +48 -0
  148. package/dist/providers/runtime.js.map +1 -0
  149. package/dist/providers/stripeWebhook.d.ts +13 -0
  150. package/dist/providers/stripeWebhook.js +50 -0
  151. package/dist/providers/stripeWebhook.js.map +1 -0
  152. package/dist/providers/types.d.ts +153 -0
  153. package/dist/proxy/cacheKey.js +3 -3
  154. package/dist/proxy/cacheKey.js.map +1 -1
  155. package/dist/proxy/providerDetector.d.ts +2 -7
  156. package/dist/proxy/providerDetector.js +4 -4
  157. package/dist/proxy/providerDetector.js.map +1 -1
  158. package/dist/proxy/proxyHandler.js +116 -12
  159. package/dist/proxy/proxyHandler.js.map +1 -1
  160. package/dist/proxy/requestNormalizer.d.ts +0 -1
  161. package/dist/proxy/requestNormalizer.js +2 -5
  162. package/dist/proxy/requestNormalizer.js.map +1 -1
  163. package/dist/reliability/costGovernance.d.ts +175 -0
  164. package/dist/reliability/costGovernance.js +515 -0
  165. package/dist/reliability/costGovernance.js.map +1 -0
  166. package/dist/reliability/index.d.ts +8 -0
  167. package/dist/reliability/index.js +5 -0
  168. package/dist/reliability/index.js.map +1 -0
  169. package/dist/reliability/reconciliation.d.ts +157 -0
  170. package/dist/reliability/reconciliation.js +522 -0
  171. package/dist/reliability/reconciliation.js.map +1 -0
  172. package/dist/reliability/runtimeHealth.d.ts +60 -0
  173. package/dist/reliability/runtimeHealth.js +356 -0
  174. package/dist/reliability/runtimeHealth.js.map +1 -0
  175. package/dist/reliability/slo.d.ts +131 -0
  176. package/dist/reliability/slo.js +375 -0
  177. package/dist/reliability/slo.js.map +1 -0
  178. package/dist/report/safetyReport.js +7 -3
  179. package/dist/report/safetyReport.js.map +1 -1
  180. package/dist/safety/index.d.ts +2 -0
  181. package/dist/safety/index.js +2 -0
  182. package/dist/safety/index.js.map +1 -0
  183. package/dist/safety/safetyController.d.ts +187 -0
  184. package/dist/safety/safetyController.js +477 -0
  185. package/dist/safety/safetyController.js.map +1 -0
  186. package/dist/scenarios/scenarioBundle.d.ts +75 -0
  187. package/dist/scenarios/scenarioBundle.js +750 -0
  188. package/dist/scenarios/scenarioBundle.js.map +1 -0
  189. package/dist/scenarios/scenarioStore.d.ts +1 -0
  190. package/dist/scenarios/scenarioStore.js +47 -32
  191. package/dist/scenarios/scenarioStore.js.map +1 -1
  192. package/dist/security/headerSanitizer.d.ts +2 -0
  193. package/dist/security/headerSanitizer.js +36 -1
  194. package/dist/security/headerSanitizer.js.map +1 -1
  195. package/dist/security/secrets.js +24 -4
  196. package/dist/security/secrets.js.map +1 -1
  197. package/dist/server/accessControl.d.ts +5 -0
  198. package/dist/server/accessControl.js +100 -0
  199. package/dist/server/accessControl.js.map +1 -0
  200. package/dist/server/createServer.d.ts +0 -5
  201. package/dist/server/createServer.js +9 -29
  202. package/dist/server/createServer.js.map +1 -1
  203. package/dist/server/eventsStore.d.ts +6 -1
  204. package/dist/server/eventsStore.js +83 -15
  205. package/dist/server/eventsStore.js.map +1 -1
  206. package/dist/server/routes.js +36 -10
  207. package/dist/server/routes.js.map +1 -1
  208. package/dist/server/sse.d.ts +2 -1
  209. package/dist/server/sse.js +15 -3
  210. package/dist/server/sse.js.map +1 -1
  211. package/dist/setup/setupGenerator.js +53 -6
  212. package/dist/setup/setupGenerator.js.map +1 -1
  213. package/dist/state/stateStore.d.ts +5 -0
  214. package/dist/state/stateStore.js +48 -62
  215. package/dist/state/stateStore.js.map +1 -1
  216. package/dist/storage/fileStore.d.ts +10 -0
  217. package/dist/storage/fileStore.js +131 -0
  218. package/dist/storage/fileStore.js.map +1 -0
  219. package/dist/teamControl/controlPlane.d.ts +255 -0
  220. package/dist/teamControl/controlPlane.js +918 -0
  221. package/dist/teamControl/controlPlane.js.map +1 -0
  222. package/dist/teamControl/deployment.d.ts +33 -0
  223. package/dist/teamControl/deployment.js +67 -0
  224. package/dist/teamControl/deployment.js.map +1 -0
  225. package/dist/teamControl/index.d.ts +4 -0
  226. package/dist/teamControl/index.js +3 -0
  227. package/dist/teamControl/index.js.map +1 -0
  228. package/dist/tests/testGenerator.js +4 -1
  229. package/dist/tests/testGenerator.js.map +1 -1
  230. package/dist/trust/index.d.ts +2 -0
  231. package/dist/trust/index.js +2 -0
  232. package/dist/trust/index.js.map +1 -0
  233. package/dist/trust/trustLadder.d.ts +182 -0
  234. package/dist/trust/trustLadder.js +560 -0
  235. package/dist/trust/trustLadder.js.map +1 -0
  236. package/dist/worlds/index.d.ts +2 -0
  237. package/dist/worlds/index.js +2 -0
  238. package/dist/worlds/index.js.map +1 -0
  239. package/dist/worlds/worlds.d.ts +160 -0
  240. package/dist/worlds/worlds.js +491 -0
  241. package/dist/worlds/worlds.js.map +1 -0
  242. package/docs/assets/dashboard.png +0 -0
  243. package/docs/ci.md +49 -0
  244. package/docs/commercial/README.md +29 -0
  245. package/docs/commercial/data-inventory.md +36 -0
  246. package/docs/commercial/manual-invoicing.md +44 -0
  247. package/docs/commercial/metrics.md +51 -0
  248. package/docs/commercial/pricing.md +103 -0
  249. package/docs/commercial/privacy-policy-draft.md +41 -0
  250. package/docs/commercial/terms-topics-for-counsel.md +51 -0
  251. package/docs/design-partners/README.md +52 -0
  252. package/docs/design-partners/demo-narratives.md +33 -0
  253. package/docs/design-partners/design-partner-offer.md +39 -0
  254. package/docs/design-partners/discovery-questions.md +28 -0
  255. package/docs/design-partners/feedback-capture-template.md +48 -0
  256. package/docs/design-partners/icp-one-pager.md +43 -0
  257. package/docs/design-partners/onboarding-checklist.md +33 -0
  258. package/docs/design-partners/pilot-success-criteria.md +27 -0
  259. package/docs/design-partners/pricing-interview-script.md +18 -0
  260. package/docs/design-partners/telemetry-plan.md +48 -0
  261. package/docs/development/baseline.md +114 -0
  262. package/docs/development/onboarding-smoke.md +37 -0
  263. package/docs/development/verification-0.1.8.md +25 -0
  264. package/docs/enterprise-product-roadmap-ru.md +1879 -0
  265. package/docs/fundraising/README.md +87 -0
  266. package/docs/fundraising/data-room-checklist.md +72 -0
  267. package/docs/fundraising/demo-script.md +52 -0
  268. package/docs/fundraising/design-partners-50.md +74 -0
  269. package/docs/fundraising/launch-posts.md +37 -0
  270. package/docs/fundraising/metrics-and-evidence.md +30 -0
  271. package/docs/fundraising/roadmap-12-month.md +25 -0
  272. package/docs/fundraising/technical-due-diligence-index.md +37 -0
  273. package/docs/fundraising/yc-application.md +25 -0
  274. package/docs/github-actions.md +84 -0
  275. package/docs/hosted-pilot.md +111 -0
  276. package/docs/mcp.md +52 -50
  277. package/docs/operations/disaster-recovery-runbook.md +81 -0
  278. package/docs/operations/kill-switch-runbook.md +32 -0
  279. package/docs/policy.md +61 -0
  280. package/docs/providers/authoring-packs.md +175 -0
  281. package/docs/providers/stripe-core-pack.md +82 -0
  282. package/docs/release-checklist.md +83 -71
  283. package/docs/release-migration-and-rollback.md +63 -0
  284. package/docs/release-readiness.md +39 -0
  285. package/docs/security/action-gateway-threat-model.md +34 -0
  286. package/docs/security/action-ledger-incident-replay-threat-model.md +42 -0
  287. package/docs/security/approval-inbox-threat-model.md +35 -0
  288. package/docs/security/credential-broker-threat-model.md +52 -0
  289. package/docs/security/egress-threat-model.md +126 -0
  290. package/docs/security/inventory-threat-model.md +104 -0
  291. package/docs/security/kill-switch-budgets-threat-model.md +37 -0
  292. package/docs/security/reliability-threat-model.md +111 -0
  293. package/docs/security/trust-ladder-threat-model.md +33 -0
  294. package/docs/team-control-plane.md +72 -0
  295. package/docs/usage.md +571 -57
  296. package/examples/README.md +10 -0
  297. package/examples/agent-instructions/README.md +39 -39
  298. package/examples/ci-smoke/README.md +21 -0
  299. package/examples/ci-smoke/ghostapi.policy.yaml +22 -0
  300. package/examples/ci-smoke/package.json +9 -0
  301. package/examples/ci-smoke/production-egress.mjs +2 -0
  302. package/examples/ci-smoke/safe.mjs +17 -0
  303. package/examples/evals/README.md +24 -0
  304. package/examples/evals/retry-after.eval.json +52 -0
  305. package/examples/generic-rest/README.md +27 -27
  306. package/examples/github-fetch/README.md +35 -35
  307. package/examples/openai-streaming/README.md +16 -0
  308. package/examples/openai-streaming/streaming-tool-call.mjs +31 -0
  309. package/examples/policy/ghostapi.policy.yaml +27 -0
  310. package/examples/record-replay/README.md +19 -0
  311. package/examples/record-replay/replay-requests.json +22 -0
  312. package/examples/record-replay/stripe-checkout.har.json +30 -0
  313. package/examples/resend-node/README.md +22 -22
  314. package/examples/stripe-node/README.md +35 -29
  315. package/examples/stripe-node/checkout-flow.mjs +61 -0
  316. package/examples/twilio-node/README.md +21 -21
  317. package/examples/worlds/README.md +19 -0
  318. package/examples/worlds/subscription-recovery.mjs +12 -0
  319. package/package.json +26 -11
  320. package/dist/providers/stripe.d.ts +0 -2
  321. package/dist/providers/stripe.js +0 -15
  322. package/dist/providers/stripe.js.map +0 -1
  323. package/docs/assets/ghostapi-avatar.png +0 -0
@@ -0,0 +1,160 @@
1
+ export type WorldScenarioReference = {
2
+ id: string;
3
+ version: string;
4
+ seed: string;
5
+ };
6
+ export type SyntheticWorldManifest = {
7
+ id: string;
8
+ title: string;
9
+ version: string;
10
+ seed: string;
11
+ clock: {
12
+ initialAt: string;
13
+ timezone: "UTC";
14
+ };
15
+ personas: Array<{
16
+ id: string;
17
+ displayName: string;
18
+ handle: string;
19
+ }>;
20
+ organizations: Array<{
21
+ id: string;
22
+ name: string;
23
+ slug: string;
24
+ }>;
25
+ accounts: {
26
+ stripe: {
27
+ accountId: string;
28
+ customerPrefix: string;
29
+ };
30
+ github: {
31
+ userLogin: string;
32
+ owner: string;
33
+ repository: string;
34
+ };
35
+ email: {
36
+ domain: string;
37
+ inboxAddress: string;
38
+ };
39
+ genericRest: {
40
+ tenantId: string;
41
+ basePath: string;
42
+ };
43
+ };
44
+ resources: {
45
+ defaultPriceId: string;
46
+ recoveryIssueLabel: string;
47
+ };
48
+ relationships: Array<{
49
+ personaId: string;
50
+ organizationId: string;
51
+ role: "owner";
52
+ }>;
53
+ providerProjections: {
54
+ stripe: {
55
+ personaId: string;
56
+ organizationId: string;
57
+ customerPrefix: string;
58
+ };
59
+ github: {
60
+ personaId: string;
61
+ organizationId: string;
62
+ repository: string;
63
+ };
64
+ email: {
65
+ personaId: string;
66
+ organizationId: string;
67
+ inboxAddress: string;
68
+ };
69
+ genericRest: {
70
+ personaId: string;
71
+ organizationId: string;
72
+ tenantId: string;
73
+ };
74
+ };
75
+ lineage?: {
76
+ parentId: string;
77
+ parentRevision: number;
78
+ };
79
+ };
80
+ export type WorldWorkflowReceipt = {
81
+ actionId: string;
82
+ customerId: string;
83
+ subscriptionId: string;
84
+ emailId: string;
85
+ issueNumber: number;
86
+ genericFailureId: string;
87
+ completedAt: string;
88
+ };
89
+ export type SyntheticWorldState = {
90
+ clockAt: string;
91
+ stripe: {
92
+ customers: Array<{
93
+ id: string;
94
+ personaId: string;
95
+ organizationId: string;
96
+ }>;
97
+ subscriptions: Array<{
98
+ id: string;
99
+ customerId: string;
100
+ priceId: string;
101
+ status: "past_due";
102
+ failureCode: "card_declined";
103
+ }>;
104
+ };
105
+ github: {
106
+ issues: Array<{
107
+ number: number;
108
+ repository: string;
109
+ title: string;
110
+ status: "open";
111
+ relatedSubscriptionId: string;
112
+ }>;
113
+ };
114
+ email: {
115
+ messages: Array<{
116
+ id: string;
117
+ to: string;
118
+ template: "subscription-payment-failed";
119
+ relatedSubscriptionId: string;
120
+ }>;
121
+ };
122
+ genericRest: {
123
+ failures: Array<{
124
+ id: string;
125
+ tenantId: string;
126
+ code: "payment_failed";
127
+ relatedSubscriptionId: string;
128
+ }>;
129
+ };
130
+ receipts: WorldWorkflowReceipt[];
131
+ };
132
+ export type SyntheticWorld = {
133
+ schemaVersion: 1;
134
+ kind: "ghostapi.world";
135
+ manifest: SyntheticWorldManifest;
136
+ revision: number;
137
+ baseline: SyntheticWorldState;
138
+ state: SyntheticWorldState;
139
+ };
140
+ export type CreateWorldOptions = {
141
+ id: string;
142
+ seed: string;
143
+ title?: string;
144
+ };
145
+ export declare class SyntheticWorldError extends Error {
146
+ readonly code?: string;
147
+ constructor(message: string, code?: string);
148
+ }
149
+ export declare function createSyntheticWorld(options: CreateWorldOptions): SyntheticWorld;
150
+ export declare function createWorld(options: CreateWorldOptions): Promise<SyntheticWorld>;
151
+ export declare function inspectWorld(id: string): Promise<SyntheticWorld>;
152
+ export declare function resetWorld(id: string): Promise<SyntheticWorld>;
153
+ export declare function forkWorld(sourceId: string, options: {
154
+ id: string;
155
+ title?: string;
156
+ }): Promise<SyntheticWorld>;
157
+ export declare function runSubscriptionFailureWorkflow(id: string, actionId: string, commit?: <T>(operation: () => Promise<T>) => Promise<T>): Promise<WorldWorkflowReceipt>;
158
+ export declare function getWorldPath(id: string): string;
159
+ export declare function formatWorld(world: SyntheticWorld): string;
160
+ export declare function validateSyntheticWorld(value: unknown): SyntheticWorld;
@@ -0,0 +1,491 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { getDataPaths } from "../config/dataPaths.js";
5
+ import { isSecretFieldName, sanitizeSecretString } from "../security/secrets.js";
6
+ import { atomicWriteJson, ensurePrivateDirectory, withFileLock } from "../storage/fileStore.js";
7
+ const WORLD_SCHEMA_VERSION = 1;
8
+ const WORLD_KIND = "ghostapi.world";
9
+ const WORLD_VERSION = "1.0.0";
10
+ const MAX_WORLD_BYTES = 512 * 1024;
11
+ const MAX_WORKFLOW_RECEIPTS = 100;
12
+ const IDENTIFIER = /^[a-z0-9][a-z0-9_-]{0,63}$/;
13
+ const MAX_SEED_LENGTH = 128;
14
+ export class SyntheticWorldError extends Error {
15
+ code;
16
+ constructor(message, code) {
17
+ super(message);
18
+ this.name = "SyntheticWorldError";
19
+ if (code !== undefined)
20
+ this.code = code;
21
+ }
22
+ }
23
+ export function createSyntheticWorld(options) {
24
+ const id = normalizeIdentifier(options.id, "World id");
25
+ const seed = normalizeSeed(options.seed);
26
+ const title = normalizeTitle(options.title ?? `Synthetic world ${id}`);
27
+ const suffix = shortHash(`${id}:${seed}`);
28
+ const personaId = `persona_${suffix}`;
29
+ const organizationId = `org_${suffix}`;
30
+ const slug = `${id}-${suffix}`.slice(0, 63);
31
+ const initialAt = deterministicTimestamp(seed);
32
+ const manifest = {
33
+ id,
34
+ title,
35
+ version: WORLD_VERSION,
36
+ seed,
37
+ clock: { initialAt, timezone: "UTC" },
38
+ personas: [{ id: personaId, displayName: `Operator ${suffix.toUpperCase()}`, handle: `operator-${suffix}` }],
39
+ organizations: [{ id: organizationId, name: `Sandbox ${suffix.toUpperCase()}`, slug }],
40
+ accounts: {
41
+ stripe: { accountId: `acct_world_${suffix}`, customerPrefix: `cus_world_${suffix}` },
42
+ github: { userLogin: `world-${suffix}`, owner: `sandbox-${suffix}`, repository: `integration-${suffix}` },
43
+ email: { domain: "ghostapi.invalid", inboxAddress: `operator-${suffix}@ghostapi.invalid` },
44
+ genericRest: { tenantId: `tenant_world_${suffix}`, basePath: `/tenants/tenant_world_${suffix}` }
45
+ },
46
+ resources: { defaultPriceId: `price_world_${suffix}`, recoveryIssueLabel: "payment-recovery" },
47
+ relationships: [{ personaId, organizationId, role: "owner" }],
48
+ providerProjections: {
49
+ stripe: { personaId, organizationId, customerPrefix: `cus_world_${suffix}` },
50
+ github: { personaId, organizationId, repository: `sandbox-${suffix}/integration-${suffix}` },
51
+ email: { personaId, organizationId, inboxAddress: `operator-${suffix}@ghostapi.invalid` },
52
+ genericRest: { personaId, organizationId, tenantId: `tenant_world_${suffix}` }
53
+ }
54
+ };
55
+ const initialState = {
56
+ clockAt: initialAt,
57
+ stripe: { customers: [], subscriptions: [] },
58
+ github: { issues: [] },
59
+ email: { messages: [] },
60
+ genericRest: { failures: [] },
61
+ receipts: []
62
+ };
63
+ return validateSyntheticWorld({ schemaVersion: WORLD_SCHEMA_VERSION, kind: WORLD_KIND, manifest, revision: 0, baseline: initialState, state: initialState });
64
+ }
65
+ export async function createWorld(options) {
66
+ const world = createSyntheticWorld(options);
67
+ const path = getWorldPath(world.manifest.id);
68
+ await withFileLock(path, async () => {
69
+ await ensureWorldFileIsSafe(path, false);
70
+ const existing = await lstat(path).catch((error) => isErrorCode(error, "ENOENT") ? null : Promise.reject(error));
71
+ if (existing !== null)
72
+ throw new SyntheticWorldError(`Synthetic world already exists: ${world.manifest.id}`);
73
+ await atomicWriteJson(path, world);
74
+ });
75
+ return world;
76
+ }
77
+ export async function inspectWorld(id) {
78
+ return readWorld(normalizeIdentifier(id, "World id"));
79
+ }
80
+ export async function resetWorld(id) {
81
+ return mutateWorld(normalizeIdentifier(id, "World id"), (world) => ({
82
+ ...world,
83
+ revision: world.revision + 1,
84
+ state: structuredClone(world.baseline)
85
+ }));
86
+ }
87
+ export async function forkWorld(sourceId, options) {
88
+ const source = await readWorld(normalizeIdentifier(sourceId, "Source world id"));
89
+ const id = normalizeIdentifier(options.id, "Fork world id");
90
+ const title = normalizeTitle(options.title ?? `${source.manifest.title} fork`);
91
+ const path = getWorldPath(id);
92
+ const fork = structuredClone(source);
93
+ fork.manifest.id = id;
94
+ fork.manifest.title = title;
95
+ fork.manifest.lineage = { parentId: source.manifest.id, parentRevision: source.revision };
96
+ fork.revision = 0;
97
+ fork.baseline = structuredClone(source.state);
98
+ fork.state = structuredClone(source.state);
99
+ const valid = validateSyntheticWorld(fork);
100
+ await withFileLock(path, async () => {
101
+ await ensureWorldFileIsSafe(path, false);
102
+ const existing = await lstat(path).catch((error) => isErrorCode(error, "ENOENT") ? null : Promise.reject(error));
103
+ if (existing !== null)
104
+ throw new SyntheticWorldError(`Synthetic world already exists: ${id}`);
105
+ await atomicWriteJson(path, valid);
106
+ });
107
+ return valid;
108
+ }
109
+ export async function runSubscriptionFailureWorkflow(id, actionId, commit) {
110
+ const normalizedActionId = normalizeIdentifier(actionId, "Workflow action id");
111
+ let receipt;
112
+ await mutateWorld(normalizeIdentifier(id, "World id"), (world) => {
113
+ const existing = world.state.receipts.find((candidate) => candidate.actionId === normalizedActionId);
114
+ if (existing !== undefined) {
115
+ receipt = existing;
116
+ return world;
117
+ }
118
+ if (world.state.receipts.length >= MAX_WORKFLOW_RECEIPTS)
119
+ throw new SyntheticWorldError(`Synthetic world permits at most ${MAX_WORKFLOW_RECEIPTS} workflow receipts.`);
120
+ const suffix = shortHash(`${world.manifest.seed}:${normalizedActionId}`);
121
+ const persona = world.manifest.personas[0];
122
+ const organization = world.manifest.organizations[0];
123
+ const customerId = `${world.manifest.accounts.stripe.customerPrefix}_${suffix}`;
124
+ const subscriptionId = `sub_world_${suffix}`;
125
+ const emailId = `email_world_${suffix}`;
126
+ const genericFailureId = `failure_world_${suffix}`;
127
+ receipt = {
128
+ actionId: normalizedActionId,
129
+ customerId,
130
+ subscriptionId,
131
+ emailId,
132
+ issueNumber: world.state.github.issues.length + 1,
133
+ genericFailureId,
134
+ completedAt: nextTimestamp(world.state.clockAt)
135
+ };
136
+ const nextState = {
137
+ clockAt: receipt.completedAt,
138
+ stripe: {
139
+ customers: [...world.state.stripe.customers, { id: customerId, personaId: persona.id, organizationId: organization.id }],
140
+ subscriptions: [...world.state.stripe.subscriptions, { id: subscriptionId, customerId, priceId: world.manifest.resources.defaultPriceId, status: "past_due", failureCode: "card_declined" }]
141
+ },
142
+ github: { issues: [...world.state.github.issues, { number: receipt.issueNumber, repository: world.manifest.providerProjections.github.repository, title: `Recover ${subscriptionId}`, status: "open", relatedSubscriptionId: subscriptionId }] },
143
+ email: { messages: [...world.state.email.messages, { id: emailId, to: world.manifest.accounts.email.inboxAddress, template: "subscription-payment-failed", relatedSubscriptionId: subscriptionId }] },
144
+ genericRest: { failures: [...world.state.genericRest.failures, { id: genericFailureId, tenantId: world.manifest.accounts.genericRest.tenantId, code: "payment_failed", relatedSubscriptionId: subscriptionId }] },
145
+ receipts: [...world.state.receipts, receipt]
146
+ };
147
+ return { ...world, revision: world.revision + 1, state: nextState };
148
+ }, commit);
149
+ if (receipt === undefined)
150
+ throw new SyntheticWorldError("World workflow did not produce a receipt.");
151
+ return receipt;
152
+ }
153
+ export function getWorldPath(id) {
154
+ return join(getDataPaths().worlds, `${normalizeIdentifier(id, "World id")}.world.json`);
155
+ }
156
+ export function formatWorld(world) {
157
+ return [
158
+ `GhostAPI synthetic world: ${world.manifest.id}`,
159
+ `Title: ${world.manifest.title}`,
160
+ `Version: ${world.manifest.version}`,
161
+ `Seed: ${world.manifest.seed}`,
162
+ `Revision: ${world.revision}`,
163
+ `Persona: ${world.manifest.personas[0].id}`,
164
+ `Organization: ${world.manifest.organizations[0].id}`,
165
+ `Providers: stripe, github, email, generic-rest`,
166
+ `Workflow receipts: ${world.state.receipts.length}`
167
+ ].join("\n");
168
+ }
169
+ export function validateSyntheticWorld(value) {
170
+ const root = readObject(value, "Synthetic world must be an object.");
171
+ assertExactKeys(root, ["schemaVersion", "kind", "manifest", "revision", "baseline", "state"], "Synthetic world");
172
+ if (root.schemaVersion !== WORLD_SCHEMA_VERSION || root.kind !== WORLD_KIND)
173
+ throw new SyntheticWorldError("Unsupported synthetic world schema.");
174
+ if (!Number.isInteger(root.revision) || root.revision < 0 || root.revision > 1_000_000)
175
+ throw new SyntheticWorldError("Synthetic world revision is invalid.");
176
+ const manifest = normalizeManifest(root.manifest);
177
+ const baseline = normalizeState(root.baseline, manifest);
178
+ const state = normalizeState(root.state, manifest);
179
+ const world = { schemaVersion: 1, kind: "ghostapi.world", manifest, revision: root.revision, baseline, state };
180
+ assertWorldSize(world);
181
+ return world;
182
+ }
183
+ async function readWorld(id) {
184
+ const path = getWorldPath(id);
185
+ await ensureWorldFileIsSafe(path, true);
186
+ const source = await readFile(path, "utf8");
187
+ if (Buffer.byteLength(source, "utf8") > MAX_WORLD_BYTES)
188
+ throw new SyntheticWorldError(`Synthetic world exceeds ${MAX_WORLD_BYTES} bytes.`);
189
+ try {
190
+ const world = validateSyntheticWorld(JSON.parse(source));
191
+ if (world.manifest.id !== id)
192
+ throw new SyntheticWorldError("Synthetic world manifest id does not match its file identity.");
193
+ return world;
194
+ }
195
+ catch (error) {
196
+ if (error instanceof SyntheticWorldError)
197
+ throw error;
198
+ throw new SyntheticWorldError("Synthetic world is not valid JSON.");
199
+ }
200
+ }
201
+ async function mutateWorld(id, mutation, commit) {
202
+ const path = getWorldPath(id);
203
+ return withFileLock(path, async () => {
204
+ const current = await readWorld(id);
205
+ const next = validateSyntheticWorld(await mutation(structuredClone(current)));
206
+ const write = () => atomicWriteJson(path, next);
207
+ if (commit === undefined)
208
+ await write();
209
+ else
210
+ await commit(write);
211
+ return next;
212
+ });
213
+ }
214
+ async function ensureWorldFileIsSafe(path, required) {
215
+ await ensurePrivateDirectory(getDataPaths().worlds);
216
+ const info = await lstat(path).catch((error) => isErrorCode(error, "ENOENT") ? null : Promise.reject(error));
217
+ if (info === null) {
218
+ if (required)
219
+ throw new SyntheticWorldError(`Synthetic world was not found: ${path}`, "WORLD_NOT_FOUND");
220
+ return;
221
+ }
222
+ if (!info.isFile() || info.isSymbolicLink())
223
+ throw new SyntheticWorldError("Synthetic world must be a regular non-symlink file.");
224
+ }
225
+ function normalizeManifest(value) {
226
+ const manifest = readObject(value, "Synthetic world manifest is invalid.");
227
+ assertExactKeys(manifest, ["id", "title", "version", "seed", "clock", "personas", "organizations", "accounts", "resources", "relationships", "providerProjections", "lineage"], "Synthetic world manifest");
228
+ const id = normalizeIdentifier(manifest.id, "World id");
229
+ const title = normalizeTitle(manifest.title);
230
+ if (manifest.version !== WORLD_VERSION)
231
+ throw new SyntheticWorldError(`Unsupported synthetic world version: ${String(manifest.version)}`);
232
+ const seed = normalizeSeed(manifest.seed);
233
+ const clock = readObject(manifest.clock, "Synthetic world clock is invalid.");
234
+ assertExactKeys(clock, ["initialAt", "timezone"], "Synthetic world clock");
235
+ if (!isTimestamp(clock.initialAt) || clock.timezone !== "UTC")
236
+ throw new SyntheticWorldError("Synthetic world clock is invalid.");
237
+ const personas = normalizePersonas(manifest.personas);
238
+ const organizations = normalizeOrganizations(manifest.organizations);
239
+ const accounts = normalizeAccounts(manifest.accounts);
240
+ const resources = normalizeResources(manifest.resources);
241
+ const relationships = normalizeRelationships(manifest.relationships, personas[0].id, organizations[0].id);
242
+ const providerProjections = normalizeProjections(manifest.providerProjections, personas[0].id, organizations[0].id, accounts);
243
+ const lineage = manifest.lineage === undefined ? undefined : normalizeLineage(manifest.lineage);
244
+ return { id, title, version: WORLD_VERSION, seed, clock: { initialAt: clock.initialAt, timezone: "UTC" }, personas, organizations, accounts, resources, relationships, providerProjections, ...(lineage === undefined ? {} : { lineage }) };
245
+ }
246
+ function normalizePersonas(value) {
247
+ if (!Array.isArray(value) || value.length !== 1)
248
+ throw new SyntheticWorldError("Synthetic world requires exactly one synthetic persona.");
249
+ const persona = readObject(value[0], "Synthetic persona is invalid.");
250
+ assertExactKeys(persona, ["id", "displayName", "handle"], "Synthetic persona");
251
+ return [{ id: normalizeIdentifier(persona.id, "Synthetic persona id"), displayName: normalizeSafeText(persona.displayName, "Synthetic persona displayName", 80), handle: normalizeIdentifier(persona.handle, "Synthetic persona handle") }];
252
+ }
253
+ function normalizeOrganizations(value) {
254
+ if (!Array.isArray(value) || value.length !== 1)
255
+ throw new SyntheticWorldError("Synthetic world requires exactly one synthetic organization.");
256
+ const organization = readObject(value[0], "Synthetic organization is invalid.");
257
+ assertExactKeys(organization, ["id", "name", "slug"], "Synthetic organization");
258
+ return [{ id: normalizeIdentifier(organization.id, "Synthetic organization id"), name: normalizeSafeText(organization.name, "Synthetic organization name", 80), slug: normalizeIdentifier(organization.slug, "Synthetic organization slug") }];
259
+ }
260
+ function normalizeAccounts(value) {
261
+ const accounts = readObject(value, "Synthetic world accounts are invalid.");
262
+ assertExactKeys(accounts, ["stripe", "github", "email", "genericRest"], "Synthetic world accounts");
263
+ const stripe = readObject(accounts.stripe, "Synthetic Stripe account is invalid.");
264
+ const github = readObject(accounts.github, "Synthetic GitHub account is invalid.");
265
+ const email = readObject(accounts.email, "Synthetic email account is invalid.");
266
+ const genericRest = readObject(accounts.genericRest, "Synthetic generic REST account is invalid.");
267
+ assertExactKeys(stripe, ["accountId", "customerPrefix"], "Synthetic Stripe account");
268
+ assertExactKeys(github, ["userLogin", "owner", "repository"], "Synthetic GitHub account");
269
+ assertExactKeys(email, ["domain", "inboxAddress"], "Synthetic email account");
270
+ assertExactKeys(genericRest, ["tenantId", "basePath"], "Synthetic generic REST account");
271
+ const domain = normalizeSafeText(email.domain, "Synthetic email domain", 120);
272
+ const inboxAddress = normalizeSyntheticInboxAddress(email.inboxAddress);
273
+ if (domain !== "ghostapi.invalid")
274
+ throw new SyntheticWorldError("Synthetic world email addresses must use ghostapi.invalid.");
275
+ return {
276
+ stripe: { accountId: normalizeIdentifier(stripe.accountId, "Synthetic Stripe account id"), customerPrefix: normalizeIdentifier(stripe.customerPrefix, "Synthetic Stripe customer prefix") },
277
+ github: { userLogin: normalizeIdentifier(github.userLogin, "Synthetic GitHub login"), owner: normalizeIdentifier(github.owner, "Synthetic GitHub owner"), repository: normalizeIdentifier(github.repository, "Synthetic GitHub repository") },
278
+ email: { domain, inboxAddress },
279
+ genericRest: { tenantId: normalizeIdentifier(genericRest.tenantId, "Synthetic tenant id"), basePath: normalizeBasePath(genericRest.basePath) }
280
+ };
281
+ }
282
+ function normalizeResources(value) {
283
+ const resources = readObject(value, "Synthetic world resources are invalid.");
284
+ assertExactKeys(resources, ["defaultPriceId", "recoveryIssueLabel"], "Synthetic world resources");
285
+ return { defaultPriceId: normalizeIdentifier(resources.defaultPriceId, "Synthetic price id"), recoveryIssueLabel: normalizeIdentifier(resources.recoveryIssueLabel, "Synthetic issue label") };
286
+ }
287
+ function normalizeRelationships(value, personaId, organizationId) {
288
+ if (!Array.isArray(value) || value.length !== 1)
289
+ throw new SyntheticWorldError("Synthetic world requires one persona-to-organization relationship.");
290
+ const relationship = readObject(value[0], "Synthetic world relationship is invalid.");
291
+ assertExactKeys(relationship, ["personaId", "organizationId", "role"], "Synthetic world relationship");
292
+ if (relationship.personaId !== personaId || relationship.organizationId !== organizationId || relationship.role !== "owner")
293
+ throw new SyntheticWorldError("Synthetic world relationship does not match canonical identity.");
294
+ return [{ personaId, organizationId, role: "owner" }];
295
+ }
296
+ function normalizeProjections(value, personaId, organizationId, accounts) {
297
+ const projections = readObject(value, "Synthetic world provider projections are invalid.");
298
+ assertExactKeys(projections, ["stripe", "github", "email", "genericRest"], "Synthetic world provider projections");
299
+ const stripe = readObject(projections.stripe, "Synthetic Stripe projection is invalid.");
300
+ const github = readObject(projections.github, "Synthetic GitHub projection is invalid.");
301
+ const email = readObject(projections.email, "Synthetic email projection is invalid.");
302
+ const genericRest = readObject(projections.genericRest, "Synthetic generic REST projection is invalid.");
303
+ assertExactKeys(stripe, ["personaId", "organizationId", "customerPrefix"], "Synthetic Stripe projection");
304
+ assertExactKeys(github, ["personaId", "organizationId", "repository"], "Synthetic GitHub projection");
305
+ assertExactKeys(email, ["personaId", "organizationId", "inboxAddress"], "Synthetic email projection");
306
+ assertExactKeys(genericRest, ["personaId", "organizationId", "tenantId"], "Synthetic generic REST projection");
307
+ if (stripe.personaId !== personaId || stripe.organizationId !== organizationId || stripe.customerPrefix !== accounts.stripe.customerPrefix || github.personaId !== personaId || github.organizationId !== organizationId || github.repository !== `${accounts.github.owner}/${accounts.github.repository}` || email.personaId !== personaId || email.organizationId !== organizationId || email.inboxAddress !== accounts.email.inboxAddress || genericRest.personaId !== personaId || genericRest.organizationId !== organizationId || genericRest.tenantId !== accounts.genericRest.tenantId) {
308
+ throw new SyntheticWorldError("Provider projections do not match canonical synthetic identity.");
309
+ }
310
+ return { stripe: { personaId, organizationId, customerPrefix: accounts.stripe.customerPrefix }, github: { personaId, organizationId, repository: `${accounts.github.owner}/${accounts.github.repository}` }, email: { personaId, organizationId, inboxAddress: accounts.email.inboxAddress }, genericRest: { personaId, organizationId, tenantId: accounts.genericRest.tenantId } };
311
+ }
312
+ function normalizeLineage(value) {
313
+ const lineage = readObject(value, "Synthetic world lineage is invalid.");
314
+ assertExactKeys(lineage, ["parentId", "parentRevision"], "Synthetic world lineage");
315
+ if (!Number.isInteger(lineage.parentRevision) || lineage.parentRevision < 0)
316
+ throw new SyntheticWorldError("Synthetic world lineage revision is invalid.");
317
+ return { parentId: normalizeIdentifier(lineage.parentId, "Synthetic world parent id"), parentRevision: lineage.parentRevision };
318
+ }
319
+ function normalizeState(value, manifest) {
320
+ const state = readObject(value, "Synthetic world state is invalid.");
321
+ assertExactKeys(state, ["clockAt", "stripe", "github", "email", "genericRest", "receipts"], "Synthetic world state");
322
+ if (!isTimestamp(state.clockAt) || state.clockAt < manifest.clock.initialAt)
323
+ throw new SyntheticWorldError("Synthetic world state clock is invalid.");
324
+ const stripe = readObject(state.stripe, "Synthetic Stripe state is invalid.");
325
+ const github = readObject(state.github, "Synthetic GitHub state is invalid.");
326
+ const email = readObject(state.email, "Synthetic email state is invalid.");
327
+ const genericRest = readObject(state.genericRest, "Synthetic generic REST state is invalid.");
328
+ assertExactKeys(stripe, ["customers", "subscriptions"], "Synthetic Stripe state");
329
+ assertExactKeys(github, ["issues"], "Synthetic GitHub state");
330
+ assertExactKeys(email, ["messages"], "Synthetic email state");
331
+ assertExactKeys(genericRest, ["failures"], "Synthetic generic REST state");
332
+ const customers = normalizeCustomers(stripe.customers, manifest);
333
+ const subscriptions = normalizeSubscriptions(stripe.subscriptions, customers, manifest);
334
+ const issues = normalizeIssues(github.issues, subscriptions, manifest);
335
+ const messages = normalizeMessages(email.messages, subscriptions, manifest);
336
+ const failures = normalizeFailures(genericRest.failures, subscriptions, manifest);
337
+ const receipts = normalizeReceipts(state.receipts, customers, subscriptions, messages, issues, failures, manifest);
338
+ if (customers.length !== subscriptions.length || customers.length !== issues.length || customers.length !== messages.length || customers.length !== failures.length || customers.length !== receipts.length)
339
+ throw new SyntheticWorldError("Synthetic world providers have inconsistent workflow state.");
340
+ if (new Set(customers.map((customer) => customer.id)).size !== customers.length || new Set(subscriptions.map((subscription) => subscription.id)).size !== subscriptions.length || new Set(messages.map((message) => message.id)).size !== messages.length || new Set(issues.map((issue) => issue.number)).size !== issues.length || new Set(failures.map((failure) => failure.id)).size !== failures.length) {
341
+ throw new SyntheticWorldError("Synthetic world contains duplicate provider resources.");
342
+ }
343
+ const subscriptionsById = new Map(subscriptions.map((subscription) => [subscription.id, subscription]));
344
+ const messagesById = new Map(messages.map((message) => [message.id, message]));
345
+ const issuesByNumber = new Map(issues.map((issue) => [issue.number, issue]));
346
+ const failuresById = new Map(failures.map((failure) => [failure.id, failure]));
347
+ for (const receipt of receipts) {
348
+ const subscription = subscriptionsById.get(receipt.subscriptionId);
349
+ if (subscription?.customerId !== receipt.customerId || messagesById.get(receipt.emailId)?.relatedSubscriptionId !== receipt.subscriptionId || issuesByNumber.get(receipt.issueNumber)?.relatedSubscriptionId !== receipt.subscriptionId || failuresById.get(receipt.genericFailureId)?.relatedSubscriptionId !== receipt.subscriptionId) {
350
+ throw new SyntheticWorldError("Synthetic workflow receipt does not link one consistent cross-provider transition.");
351
+ }
352
+ }
353
+ return { clockAt: state.clockAt, stripe: { customers, subscriptions }, github: { issues }, email: { messages }, genericRest: { failures }, receipts };
354
+ }
355
+ function normalizeCustomers(value, manifest) {
356
+ return readArray(value, "Synthetic Stripe customers", MAX_WORKFLOW_RECEIPTS).map((entry) => {
357
+ const customer = readObject(entry, "Synthetic Stripe customer is invalid.");
358
+ assertExactKeys(customer, ["id", "personaId", "organizationId"], "Synthetic Stripe customer");
359
+ if (customer.personaId !== manifest.personas[0].id || customer.organizationId !== manifest.organizations[0].id)
360
+ throw new SyntheticWorldError("Synthetic Stripe customer identity is inconsistent.");
361
+ return { id: normalizeIdentifier(customer.id, "Synthetic Stripe customer id"), personaId: manifest.personas[0].id, organizationId: manifest.organizations[0].id };
362
+ });
363
+ }
364
+ function normalizeSubscriptions(value, customers, manifest) {
365
+ const customerIds = new Set(customers.map((customer) => customer.id));
366
+ return readArray(value, "Synthetic Stripe subscriptions", MAX_WORKFLOW_RECEIPTS).map((entry) => {
367
+ const subscription = readObject(entry, "Synthetic Stripe subscription is invalid.");
368
+ assertExactKeys(subscription, ["id", "customerId", "priceId", "status", "failureCode"], "Synthetic Stripe subscription");
369
+ if (!customerIds.has(subscription.customerId) || subscription.priceId !== manifest.resources.defaultPriceId || subscription.status !== "past_due" || subscription.failureCode !== "card_declined")
370
+ throw new SyntheticWorldError("Synthetic Stripe subscription is inconsistent.");
371
+ return { id: normalizeIdentifier(subscription.id, "Synthetic Stripe subscription id"), customerId: subscription.customerId, priceId: manifest.resources.defaultPriceId, status: "past_due", failureCode: "card_declined" };
372
+ });
373
+ }
374
+ function normalizeIssues(value, subscriptions, manifest) {
375
+ const subscriptionsById = new Set(subscriptions.map((subscription) => subscription.id));
376
+ return readArray(value, "Synthetic GitHub issues", MAX_WORKFLOW_RECEIPTS).map((entry) => {
377
+ const issue = readObject(entry, "Synthetic GitHub issue is invalid.");
378
+ assertExactKeys(issue, ["number", "repository", "title", "status", "relatedSubscriptionId"], "Synthetic GitHub issue");
379
+ if (!Number.isInteger(issue.number) || issue.number < 1 || issue.repository !== manifest.providerProjections.github.repository || typeof issue.title !== "string" || issue.status !== "open" || !subscriptionsById.has(issue.relatedSubscriptionId))
380
+ throw new SyntheticWorldError("Synthetic GitHub issue is inconsistent.");
381
+ return { number: issue.number, repository: manifest.providerProjections.github.repository, title: normalizeSafeText(issue.title, "Synthetic GitHub issue title", 160), status: "open", relatedSubscriptionId: issue.relatedSubscriptionId };
382
+ });
383
+ }
384
+ function normalizeMessages(value, subscriptions, manifest) {
385
+ const subscriptionsById = new Set(subscriptions.map((subscription) => subscription.id));
386
+ return readArray(value, "Synthetic email messages", MAX_WORKFLOW_RECEIPTS).map((entry) => {
387
+ const message = readObject(entry, "Synthetic email message is invalid.");
388
+ assertExactKeys(message, ["id", "to", "template", "relatedSubscriptionId"], "Synthetic email message");
389
+ if (message.to !== manifest.accounts.email.inboxAddress || message.template !== "subscription-payment-failed" || !subscriptionsById.has(message.relatedSubscriptionId))
390
+ throw new SyntheticWorldError("Synthetic email message is inconsistent.");
391
+ return { id: normalizeIdentifier(message.id, "Synthetic email message id"), to: manifest.accounts.email.inboxAddress, template: "subscription-payment-failed", relatedSubscriptionId: message.relatedSubscriptionId };
392
+ });
393
+ }
394
+ function normalizeFailures(value, subscriptions, manifest) {
395
+ const subscriptionsById = new Set(subscriptions.map((subscription) => subscription.id));
396
+ return readArray(value, "Synthetic generic REST failures", MAX_WORKFLOW_RECEIPTS).map((entry) => {
397
+ const failure = readObject(entry, "Synthetic generic REST failure is invalid.");
398
+ assertExactKeys(failure, ["id", "tenantId", "code", "relatedSubscriptionId"], "Synthetic generic REST failure");
399
+ if (failure.tenantId !== manifest.accounts.genericRest.tenantId || failure.code !== "payment_failed" || !subscriptionsById.has(failure.relatedSubscriptionId))
400
+ throw new SyntheticWorldError("Synthetic generic REST failure is inconsistent.");
401
+ return { id: normalizeIdentifier(failure.id, "Synthetic generic REST failure id"), tenantId: manifest.accounts.genericRest.tenantId, code: "payment_failed", relatedSubscriptionId: failure.relatedSubscriptionId };
402
+ });
403
+ }
404
+ function normalizeReceipts(value, customers, subscriptions, messages, issues, failures, manifest) {
405
+ const customerIds = new Set(customers.map((customer) => customer.id));
406
+ const subscriptionIds = new Set(subscriptions.map((subscription) => subscription.id));
407
+ const messageIds = new Set(messages.map((message) => message.id));
408
+ const issueNumbers = new Set(issues.map((issue) => issue.number));
409
+ const failureIds = new Set(failures.map((failure) => failure.id));
410
+ const actionIds = new Set();
411
+ return readArray(value, "Synthetic workflow receipts", MAX_WORKFLOW_RECEIPTS).map((entry) => {
412
+ const receipt = readObject(entry, "Synthetic workflow receipt is invalid.");
413
+ assertExactKeys(receipt, ["actionId", "customerId", "subscriptionId", "emailId", "issueNumber", "genericFailureId", "completedAt"], "Synthetic workflow receipt");
414
+ const actionId = normalizeIdentifier(receipt.actionId, "Synthetic workflow action id");
415
+ if (actionIds.has(actionId) || !customerIds.has(receipt.customerId) || !subscriptionIds.has(receipt.subscriptionId) || !messageIds.has(receipt.emailId) || !issueNumbers.has(receipt.issueNumber) || !failureIds.has(receipt.genericFailureId) || !isTimestamp(receipt.completedAt) || receipt.completedAt < manifest.clock.initialAt)
416
+ throw new SyntheticWorldError("Synthetic workflow receipt is inconsistent.");
417
+ actionIds.add(actionId);
418
+ return { actionId, customerId: receipt.customerId, subscriptionId: receipt.subscriptionId, emailId: receipt.emailId, issueNumber: receipt.issueNumber, genericFailureId: receipt.genericFailureId, completedAt: receipt.completedAt };
419
+ });
420
+ }
421
+ function normalizeIdentifier(value, label) {
422
+ if (typeof value !== "string" || !IDENTIFIER.test(value) || sanitizeSecretString(value) !== value)
423
+ throw new SyntheticWorldError(`${label} must be a safe stable identifier.`);
424
+ return value;
425
+ }
426
+ function normalizeSeed(value) {
427
+ if (typeof value !== "string" || value.trim() === "" || value.length > MAX_SEED_LENGTH || /[\r\n\t]/.test(value) || sanitizeSecretString(value) !== value || hasObviousPii(value))
428
+ throw new SyntheticWorldError("World seed must be a non-secret, non-PII 1-128 character string.");
429
+ return value;
430
+ }
431
+ function normalizeTitle(value) {
432
+ return normalizeSafeText(value, "World title", 120);
433
+ }
434
+ function normalizeSafeText(value, label, maximumLength) {
435
+ if (typeof value !== "string" || value.trim() === "" || value.trim().length > maximumLength || /[\r\n\t]/.test(value) || sanitizeSecretString(value) !== value || hasObviousPii(value))
436
+ throw new SyntheticWorldError(`${label} must be safe non-PII plain text.`);
437
+ return value.trim();
438
+ }
439
+ function normalizeBasePath(value) {
440
+ if (typeof value !== "string" || !/^\/tenants\/[a-z0-9_-]+$/.test(value))
441
+ throw new SyntheticWorldError("Synthetic generic REST base path is invalid.");
442
+ return value;
443
+ }
444
+ function normalizeSyntheticInboxAddress(value) {
445
+ if (typeof value !== "string" || !/^operator-[a-f0-9]{12}@ghostapi\.invalid$/.test(value))
446
+ throw new SyntheticWorldError("Synthetic inbox address must use the generated ghostapi.invalid format.");
447
+ return value;
448
+ }
449
+ function readArray(value, label, maximumLength) {
450
+ if (!Array.isArray(value) || value.length > maximumLength)
451
+ throw new SyntheticWorldError(`${label} must be a bounded array.`);
452
+ return value;
453
+ }
454
+ function readObject(value, message) {
455
+ if (value === null || typeof value !== "object" || Array.isArray(value))
456
+ throw new SyntheticWorldError(message);
457
+ return value;
458
+ }
459
+ function assertExactKeys(value, allowed, label) {
460
+ const unknown = Object.keys(value).find((key) => !allowed.includes(key));
461
+ if (unknown !== undefined)
462
+ throw new SyntheticWorldError(`${label} contains unknown field: ${unknown}`);
463
+ const sensitive = Object.keys(value).find(isSecretFieldName);
464
+ if (sensitive !== undefined)
465
+ throw new SyntheticWorldError(`${label} contains secret-shaped field: ${sensitive}`);
466
+ }
467
+ function assertWorldSize(world) {
468
+ const serialized = JSON.stringify(world);
469
+ if (Buffer.byteLength(serialized, "utf8") > MAX_WORLD_BYTES)
470
+ throw new SyntheticWorldError(`Synthetic world exceeds ${MAX_WORLD_BYTES} bytes.`);
471
+ }
472
+ function shortHash(value) {
473
+ return createHash("sha256").update(value, "utf8").digest("hex").slice(0, 12);
474
+ }
475
+ function deterministicTimestamp(seed) {
476
+ const seconds = Number.parseInt(shortHash(seed).slice(0, 8), 16) % (365 * 24 * 60 * 60);
477
+ return new Date(Date.UTC(2024, 0, 1) + seconds * 1000).toISOString();
478
+ }
479
+ function nextTimestamp(value) {
480
+ return new Date(Date.parse(value) + 1000).toISOString();
481
+ }
482
+ function isTimestamp(value) {
483
+ return 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));
484
+ }
485
+ function hasObviousPii(value) {
486
+ return /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i.test(value) || /(?:\+\d[\d(). -]{7,}\d|\b\d{3}[ .-]\d{3}[ .-]\d{4}\b)/.test(value);
487
+ }
488
+ function isErrorCode(error, code) {
489
+ return error instanceof Error && "code" in error && error.code === code;
490
+ }
491
+ //# sourceMappingURL=worlds.js.map