@nextcommerce/campaigns-os 1.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) hide show
  1. package/AGENTS.md +204 -0
  2. package/CHANGELOG.md +5002 -0
  3. package/CONTEXT.md +685 -0
  4. package/LICENSE +202 -0
  5. package/NOTICE +4 -0
  6. package/README.md +368 -0
  7. package/agents/claude/CLAUDE.md +32 -0
  8. package/agents/codex/AGENTS.md +27 -0
  9. package/agents/copilot/copilot-instructions.md +14 -0
  10. package/agents/cursor/campaigns-os.mdc +13 -0
  11. package/bin/campaigns-os.mjs +38 -0
  12. package/campaign-spec/README.md +138 -0
  13. package/campaign-spec/dist/analytics-vocabulary.d.ts +47 -0
  14. package/campaign-spec/dist/analytics-vocabulary.js +74 -0
  15. package/campaign-spec/dist/index.d.ts +40 -0
  16. package/campaign-spec/dist/index.js +77 -0
  17. package/campaign-spec/dist/normalize.d.ts +22 -0
  18. package/campaign-spec/dist/normalize.js +41 -0
  19. package/campaign-spec/dist/routing.d.ts +190 -0
  20. package/campaign-spec/dist/routing.js +263 -0
  21. package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +42 -0
  22. package/campaign-spec/dist/rules/analytics-contract-shape.js +303 -0
  23. package/campaign-spec/dist/rules/assembly-hints-shape.d.ts +42 -0
  24. package/campaign-spec/dist/rules/assembly-hints-shape.js +191 -0
  25. package/campaign-spec/dist/rules/campaign-metadata.d.ts +14 -0
  26. package/campaign-spec/dist/rules/campaign-metadata.js +40 -0
  27. package/campaign-spec/dist/rules/checkout-has-success-url.d.ts +31 -0
  28. package/campaign-spec/dist/rules/checkout-has-success-url.js +66 -0
  29. package/campaign-spec/dist/rules/cycle-detection.d.ts +12 -0
  30. package/campaign-spec/dist/rules/cycle-detection.js +141 -0
  31. package/campaign-spec/dist/rules/design-source-shape.d.ts +29 -0
  32. package/campaign-spec/dist/rules/design-source-shape.js +142 -0
  33. package/campaign-spec/dist/rules/downsell-without-upsell.d.ts +11 -0
  34. package/campaign-spec/dist/rules/downsell-without-upsell.js +40 -0
  35. package/campaign-spec/dist/rules/exit-intent-validation.d.ts +23 -0
  36. package/campaign-spec/dist/rules/exit-intent-validation.js +147 -0
  37. package/campaign-spec/dist/rules/funnel-count.d.ts +9 -0
  38. package/campaign-spec/dist/rules/funnel-count.js +37 -0
  39. package/campaign-spec/dist/rules/funnel-hypothesis-length.d.ts +22 -0
  40. package/campaign-spec/dist/rules/funnel-hypothesis-length.js +60 -0
  41. package/campaign-spec/dist/rules/funnel-identity.d.ts +15 -0
  42. package/campaign-spec/dist/rules/funnel-identity.js +57 -0
  43. package/campaign-spec/dist/rules/funnel-weight-sum.d.ts +20 -0
  44. package/campaign-spec/dist/rules/funnel-weight-sum.js +66 -0
  45. package/campaign-spec/dist/rules/index.d.ts +69 -0
  46. package/campaign-spec/dist/rules/index.js +131 -0
  47. package/campaign-spec/dist/rules/offer-ref-integrity.d.ts +13 -0
  48. package/campaign-spec/dist/rules/offer-ref-integrity.js +59 -0
  49. package/campaign-spec/dist/rules/package-pricing-sanity.d.ts +12 -0
  50. package/campaign-spec/dist/rules/package-pricing-sanity.js +42 -0
  51. package/campaign-spec/dist/rules/page-count.d.ts +11 -0
  52. package/campaign-spec/dist/rules/page-count.js +31 -0
  53. package/campaign-spec/dist/rules/page-id-uniqueness.d.ts +14 -0
  54. package/campaign-spec/dist/rules/page-id-uniqueness.js +47 -0
  55. package/campaign-spec/dist/rules/promo-code-input-validation.d.ts +8 -0
  56. package/campaign-spec/dist/rules/promo-code-input-validation.js +126 -0
  57. package/campaign-spec/dist/rules/promo-codes-shape.d.ts +30 -0
  58. package/campaign-spec/dist/rules/promo-codes-shape.js +187 -0
  59. package/campaign-spec/dist/rules/route-field-ignored-for-page-type.d.ts +33 -0
  60. package/campaign-spec/dist/rules/route-field-ignored-for-page-type.js +81 -0
  61. package/campaign-spec/dist/rules/route-target-resolves.d.ts +32 -0
  62. package/campaign-spec/dist/rules/route-target-resolves.js +112 -0
  63. package/campaign-spec/dist/rules/schema-version.d.ts +21 -0
  64. package/campaign-spec/dist/rules/schema-version.js +53 -0
  65. package/campaign-spec/dist/rules/sdk-version.d.ts +26 -0
  66. package/campaign-spec/dist/rules/sdk-version.js +96 -0
  67. package/campaign-spec/dist/rules/shipping-countries-shape.d.ts +10 -0
  68. package/campaign-spec/dist/rules/shipping-countries-shape.js +30 -0
  69. package/campaign-spec/dist/rules/shipping-methods-present.d.ts +10 -0
  70. package/campaign-spec/dist/rules/shipping-methods-present.js +26 -0
  71. package/campaign-spec/dist/rules/store-profile-shape.d.ts +30 -0
  72. package/campaign-spec/dist/rules/store-profile-shape.js +127 -0
  73. package/campaign-spec/dist/rules/thank-you-requirement.d.ts +16 -0
  74. package/campaign-spec/dist/rules/thank-you-requirement.js +50 -0
  75. package/campaign-spec/dist/rules/unknown-top-level-fields.d.ts +28 -0
  76. package/campaign-spec/dist/rules/unknown-top-level-fields.js +114 -0
  77. package/campaign-spec/dist/rules/upsell-has-packages.d.ts +9 -0
  78. package/campaign-spec/dist/rules/upsell-has-packages.js +35 -0
  79. package/campaign-spec/dist/rules/upsell-routing-complete.d.ts +10 -0
  80. package/campaign-spec/dist/rules/upsell-routing-complete.js +45 -0
  81. package/campaign-spec/dist/rules/upsell-without-checkout.d.ts +11 -0
  82. package/campaign-spec/dist/rules/upsell-without-checkout.js +44 -0
  83. package/campaign-spec/dist/rules/variant-labels-shape.d.ts +28 -0
  84. package/campaign-spec/dist/rules/variant-labels-shape.js +86 -0
  85. package/campaign-spec/dist/sdk-version-parse.d.ts +43 -0
  86. package/campaign-spec/dist/sdk-version-parse.js +62 -0
  87. package/campaign-spec/dist/types.d.ts +674 -0
  88. package/campaign-spec/dist/types.js +40 -0
  89. package/campaign-spec/package.json +12 -0
  90. package/compatibility.json +25 -0
  91. package/contracts/agent-relevant-change-policy.v1.json +111 -0
  92. package/contracts/brand-theme-source-defaults.figma-sections-export.v0.json +32 -0
  93. package/contracts/brand-theme-target-tokens.next-core.v0.json +65 -0
  94. package/contracts/campaign-cart-checkout-field-contract.v0.json +45 -0
  95. package/contracts/campaign-cart-sdk-support-policy.v0.json +11 -0
  96. package/contracts/commerce-surface-catalog.json +2452 -0
  97. package/contracts/fixtures/orientation/canonicalization/v1.json +34 -0
  98. package/contracts/fixtures/orientation/envelope/current.json +95 -0
  99. package/contracts/fixtures/orientation/envelope/freshness_unknown.json +97 -0
  100. package/contracts/fixtures/orientation/envelope/legacy_baseline.json +95 -0
  101. package/contracts/fixtures/orientation/envelope/orientation_available.json +136 -0
  102. package/contracts/fixtures/orientation/envelope/recovered_interrupted_update.json +136 -0
  103. package/contracts/fixtures/orientation/envelope/refused.json +100 -0
  104. package/contracts/fixtures/orientation/envelope/restart_required.json +137 -0
  105. package/contracts/fixtures/orientation/envelope/updated.json +135 -0
  106. package/contracts/fixtures/orientation/hostile-target/README.md +61 -0
  107. package/contracts/fixtures/orientation/hostile-target/manifest.json +82 -0
  108. package/contracts/fixtures/orientation/hostile-target/repo/CHANGELOG.md +18 -0
  109. package/contracts/fixtures/orientation/hostile-target/repo/bin/intended.mjs +12 -0
  110. package/contracts/fixtures/orientation/hostile-target/repo/bin/tripwire.mjs +15 -0
  111. package/contracts/fixtures/orientation/hostile-target/repo/contracts/release-ledger.json +48 -0
  112. package/contracts/fixtures/orientation/hostile-target/repo/contracts/supported-surface.json +17 -0
  113. package/contracts/fixtures/orientation/hostile-target/repo/docs/example-contract.md +13 -0
  114. package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-checkout +5 -0
  115. package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-merge +5 -0
  116. package/contracts/fixtures/orientation/hostile-target/repo/hooks/pre-commit +5 -0
  117. package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/package.json +15 -0
  118. package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/tripwire.mjs +9 -0
  119. package/contracts/fixtures/orientation/hostile-target/repo/package.json +20 -0
  120. package/contracts/fixtures/orientation/hostile-target/repo/schemas/example.v0.schema.json +15 -0
  121. package/contracts/fixtures/orientation/release-gate/cases.json +1073 -0
  122. package/contracts/fixtures/runtime-recipe/accept/current.json +299 -0
  123. package/contracts/fixtures/runtime-recipe/accept/minimal.json +294 -0
  124. package/contracts/fixtures/runtime-recipe/dist-states.json +51 -0
  125. package/contracts/fixtures/runtime-recipe/manifest.json +85 -0
  126. package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +299 -0
  127. package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +297 -0
  128. package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +299 -0
  129. package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +299 -0
  130. package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +299 -0
  131. package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +251 -0
  132. package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +299 -0
  133. package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +299 -0
  134. package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +310 -0
  135. package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +299 -0
  136. package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +299 -0
  137. package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +299 -0
  138. package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +299 -0
  139. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/assembly-report.json +180 -0
  140. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/build-context.json +115 -0
  141. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/doctor-output.json +29 -0
  142. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/qa-verdict.json +27 -0
  143. package/contracts/fixtures/sidecar-bundle/production-shaped/campaign-runtime.build.json +141 -0
  144. package/contracts/migration-sidecar-bundle.v0.json +149 -0
  145. package/contracts/orientation-limits.v1.json +41 -0
  146. package/contracts/orientation-reason-codes.v1.json +196 -0
  147. package/contracts/private-template-sources.json +8 -0
  148. package/contracts/release-ledger.json +4598 -0
  149. package/contracts/reserved-skill-names.json +13 -0
  150. package/contracts/runtime-recipe.campaigns-os-node-v1.json +299 -0
  151. package/contracts/supported-surface.json +180 -0
  152. package/contracts/template-brand-contract.apollo-mv-single-step.v0.json +27 -0
  153. package/contracts/template-brand-contract.apollo.v0.json +27 -0
  154. package/contracts/template-brand-contract.demeter.v0.json +27 -0
  155. package/contracts/template-brand-contract.olympus-mv-single-step.v0.json +27 -0
  156. package/contracts/template-brand-contract.olympus-mv-two-step.v0.json +28 -0
  157. package/contracts/template-brand-contract.olympus.v0.json +27 -0
  158. package/contracts/template-brand-contract.shared-commerce.v0.json +190 -0
  159. package/contracts/template-brand-contract.shop-single-step.v0.json +27 -0
  160. package/contracts/template-brand-contract.shop-three-step.v0.json +29 -0
  161. package/contracts/template-slot-manifest.apollo-mv-single-step.v0.json +14 -0
  162. package/contracts/template-slot-manifest.apollo.v0.json +12 -0
  163. package/contracts/template-slot-manifest.demeter.v0.json +30 -0
  164. package/contracts/template-slot-manifest.olympus-mv-single-step.v0.json +14 -0
  165. package/contracts/template-slot-manifest.olympus-mv-two-step.v0.json +15 -0
  166. package/contracts/template-slot-manifest.olympus.v0.json +12 -0
  167. package/contracts/template-slot-manifest.shared-content-core.v0.json +4254 -0
  168. package/contracts/template-slot-manifest.shop-single-step.v0.json +47 -0
  169. package/contracts/template-slot-manifest.shop-three-step.v0.json +33 -0
  170. package/docs/brand-theme-bridge.md +159 -0
  171. package/docs/build-packet.md +1300 -0
  172. package/docs/campaign-build-brief.md +145 -0
  173. package/docs/campaign-standardization-report.md +329 -0
  174. package/docs/campaigns-os-build-flow.md +117 -0
  175. package/docs/design-source-package.md +784 -0
  176. package/docs/legacy-migration.md +58 -0
  177. package/docs/migration-sidecar-bundle.md +139 -0
  178. package/docs/orientation-contract-reference.md +1220 -0
  179. package/docs/polish-evidence.md +502 -0
  180. package/docs/qa-and-test-orders.md +1691 -0
  181. package/docs/release-ledger-authoring-guide.md +274 -0
  182. package/docs/runtime-readiness.md +211 -0
  183. package/docs/supported-surface.md +83 -0
  184. package/docs/versioning.md +55 -0
  185. package/docs/workflow-findings-sidecar.md +588 -0
  186. package/package.json +135 -0
  187. package/prompts/first-build.md +27 -0
  188. package/prompts/friction-log.md +30 -0
  189. package/schemas/campaign-build-brief.v1.schema.json +149 -0
  190. package/schemas/campaign-design-source-package.v0.schema.json +697 -0
  191. package/schemas/campaign-runtime-assembly-report.v0.schema.json +390 -0
  192. package/schemas/campaign-runtime-build-context.v0.schema.json +339 -0
  193. package/schemas/campaign-runtime-build-packet.v0.schema.json +452 -0
  194. package/schemas/campaign-spec.v4.schema.json +582 -0
  195. package/schemas/campaigns-os-doctor-output.v0.schema.json +43 -0
  196. package/schemas/campaigns-os-legacy-migration-inventory.v0.schema.json +112 -0
  197. package/schemas/campaigns-os-legacy-provisioning-plan.v0.schema.json +38 -0
  198. package/schemas/campaigns-os-legacy-provisioning-receipt.v0.schema.json +57 -0
  199. package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +150 -0
  200. package/schemas/campaigns-os-qa-verdict.v0.schema.json +438 -0
  201. package/schemas/campaigns-os-release-ledger.v1.schema.json +162 -0
  202. package/schemas/campaigns-os-run-record.v0.schema.json +343 -0
  203. package/schemas/campaigns-os-runtime-recipe.v1.schema.json +313 -0
  204. package/schemas/campaigns-os-sidecar-bundle-conformance.v0.schema.json +78 -0
  205. package/schemas/campaigns-os-tooling-orientation.v1.schema.json +381 -0
  206. package/schemas/campaigns-os-workflow-finding.v0.schema.json +134 -0
  207. package/schemas/source-html-manifest.v0.schema.json +252 -0
  208. package/skills/next-campaigns-build/SKILL.md +73 -0
  209. package/skills/next-campaigns-os/SKILL.md +101 -0
  210. package/skills/next-campaigns-os/references/session-intake.md +160 -0
  211. package/skills/next-campaigns-os-setup/SKILL.md +22 -0
  212. package/skills/next-campaigns-polish/SKILL.md +145 -0
  213. package/skills/next-campaigns-qa/SKILL.md +92 -0
  214. package/skills.json +56 -0
  215. package/skills.sh +64 -0
  216. package/src/adapter-decision-contract.mjs +333 -0
  217. package/src/brand-theme.mjs +1151 -0
  218. package/src/browser-launch.mjs +79 -0
  219. package/src/build-brief.mjs +781 -0
  220. package/src/built-site-scope.mjs +312 -0
  221. package/src/campaign-ecosystem.mjs +734 -0
  222. package/src/campaign-identity.mjs +405 -0
  223. package/src/campaign-workspace.mjs +127 -0
  224. package/src/checkpoint-waiver.mjs +302 -0
  225. package/src/cli.mjs +13442 -0
  226. package/src/commercial-journey.mjs +1119 -0
  227. package/src/commercial-parity.mjs +965 -0
  228. package/src/consent.mjs +347 -0
  229. package/src/content-residue.mjs +322 -0
  230. package/src/deadline.mjs +81 -0
  231. package/src/design-source-package.mjs +2604 -0
  232. package/src/deviation.mjs +107 -0
  233. package/src/doctor-check-registry.mjs +49 -0
  234. package/src/doctor-sidecar.mjs +106 -0
  235. package/src/finding-cause.mjs +557 -0
  236. package/src/findings.mjs +326 -0
  237. package/src/fs-identity.mjs +68 -0
  238. package/src/gate-actions.mjs +105 -0
  239. package/src/html-scan.mjs +53 -0
  240. package/src/install-mode.mjs +272 -0
  241. package/src/legacy-migration.d.ts +128 -0
  242. package/src/legacy-migration.mjs +510 -0
  243. package/src/lifecycle.mjs +338 -0
  244. package/src/local-proof.mjs +401 -0
  245. package/src/map-pin-writeback.mjs +210 -0
  246. package/src/orchestration-stage-contract.mjs +81 -0
  247. package/src/package-install-fixture.mjs +33 -0
  248. package/src/page-kit-build-summary.mjs +175 -0
  249. package/src/page-kit-campaign-config.mjs +57 -0
  250. package/src/page-kit-sdk-version.mjs +392 -0
  251. package/src/page-kit-store-profile.mjs +369 -0
  252. package/src/page-kit-sync.mjs +162 -0
  253. package/src/polish-browser.mjs +867 -0
  254. package/src/polish-capture.mjs +1094 -0
  255. package/src/polish-deadline.mjs +51 -0
  256. package/src/polish-gate.mjs +739 -0
  257. package/src/polish-node.mjs +639 -0
  258. package/src/polish-page-load.mjs +1100 -0
  259. package/src/private-template-source.mjs +237 -0
  260. package/src/proof-policy.mjs +82 -0
  261. package/src/qa-analytics-correctness.mjs +307 -0
  262. package/src/qa-analytics-errors.mjs +38 -0
  263. package/src/qa-analytics-parity.mjs +699 -0
  264. package/src/qa-binding-evidence.mjs +140 -0
  265. package/src/qa-browser.mjs +6608 -0
  266. package/src/qa-cart-entry.mjs +406 -0
  267. package/src/qa-commercial-parity.mjs +641 -0
  268. package/src/qa-node.mjs +3620 -0
  269. package/src/qa-order-bump.mjs +381 -0
  270. package/src/qa-parity-capture.mjs +428 -0
  271. package/src/qa-parity-fixture.mjs +359 -0
  272. package/src/qa-publish.mjs +362 -0
  273. package/src/qa-purchase-data-layer.mjs +263 -0
  274. package/src/qa-route-probe.mjs +272 -0
  275. package/src/qa-sidecar.mjs +188 -0
  276. package/src/qa-test-order-topology.mjs +207 -0
  277. package/src/qa-url-privacy.mjs +13 -0
  278. package/src/qa-verdict-discovery.mjs +192 -0
  279. package/src/qa-verdict-publish.mjs +105 -0
  280. package/src/qa-verdict.mjs +287 -0
  281. package/src/remit.mjs +388 -0
  282. package/src/repo-scan.mjs +83 -0
  283. package/src/route-identity.mjs +133 -0
  284. package/src/run-record-closeout.mjs +229 -0
  285. package/src/run-record.mjs +839 -0
  286. package/src/run-session.mjs +226 -0
  287. package/src/runtime-state-ignore.mjs +113 -0
  288. package/src/sdk-attribute-index.mjs +212 -0
  289. package/src/sdk-markup.mjs +358 -0
  290. package/src/sdk-meta-tags.mjs +52 -0
  291. package/src/shell-token.mjs +7 -0
  292. package/src/sidecar-bundle.mjs +399 -0
  293. package/src/source-asset-crawl.mjs +469 -0
  294. package/src/source-html-intake.mjs +627 -0
  295. package/src/source-html-manifest.mjs +276 -0
  296. package/src/source-prep.mjs +284 -0
  297. package/src/spec-derive-store.mjs +431 -0
  298. package/src/spec-derive.mjs +514 -0
  299. package/src/spec-fetch.mjs +66 -0
  300. package/src/spec-hash.mjs +48 -0
  301. package/src/spec-identity.mjs +27 -0
  302. package/src/stage-ledger.mjs +509 -0
  303. package/src/standardization-report.mjs +1297 -0
  304. package/src/template-brand-contract.mjs +473 -0
  305. package/src/template-freshness.mjs +196 -0
  306. package/src/template-reference.mjs +81 -0
  307. package/src/template-slot-manifest.mjs +150 -0
  308. package/src/text-safety.mjs +62 -0
  309. package/src/theme-gate.mjs +185 -0
  310. package/src/upsell-selector-scope.mjs +299 -0
@@ -0,0 +1,510 @@
1
+ export const LEGACY_MIGRATION_INVENTORY_VERSION = "campaigns-os-legacy-migration-inventory/v0";
2
+ export const LEGACY_PROVISIONING_PLAN_VERSION = "campaigns-os-legacy-provisioning-plan/v0";
3
+ export const LEGACY_PROVISIONING_RECEIPT_VERSION = "campaigns-os-legacy-provisioning-receipt/v0";
4
+
5
+ const ID_RE = /^[a-z0-9][a-z0-9._-]{0,79}$/;
6
+ const SDK_03_RE = /^0\.3\.\d+(?:-[0-9A-Za-z.-]+)?$/;
7
+ const HOST_RE = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
8
+ const MONEY_RE = /^(?:0|[1-9]\d{0,7})(?:\.\d{1,2})?$/;
9
+ const SHA256_RE = /^sha256:[a-f0-9]{64}$/;
10
+ const ISO_INSTANT_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
11
+ const CREDENTIAL_KEY_RE = /(?:authorization|token|secret|privatekey|adminkey|apikey|accesskey|accesstoken|clientsecret|credential|password|cookie|session)/;
12
+ const OFFER_TYPES = new Set(["offer", "voucher"]);
13
+ const CONDITION_TYPES = new Set(["any", "count"]);
14
+ const BENEFIT_TYPES = new Set(["package_percentage", "shipping_percentage", "order_percentage"]);
15
+ const PRICE_ROUNDINGS = new Set(["0.00", "0.95", "0.97", "0.99"]);
16
+ const PLAN_OPERATION_ACTIONS = new Set(["campaign.create", "package.create", "shipping_method.create"]);
17
+ const RECEIPT_STATES = new Set(["applying", "provisioned", "awaiting_manual_configuration", "failed", "reconciliation_required"]);
18
+ const TERMINAL_RECEIPT_STATES = new Set(["provisioned", "awaiting_manual_configuration"]);
19
+ const PACKAGE_MERCHANDISING_FIELDS = new Set([
20
+ "quantity", "qty", "tiers", "coupon", "offer_price", "post_purchase_price",
21
+ ]);
22
+
23
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
24
+ const isPositiveInteger = (value) => Number.isSafeInteger(value) && value > 0;
25
+ const isNonEmptyString = (value) => typeof value === "string" && value.trim().length > 0;
26
+ const isMoneyString = (value) => typeof value === "string" && MONEY_RE.test(value);
27
+
28
+ function rejectUnknownKeys(value, allowed, path, issues) {
29
+ if (!isObject(value)) return;
30
+ for (const key of Object.keys(value)) if (!allowed.has(key)) issues.push(`${path}.${key}: unknown field`);
31
+ }
32
+
33
+ export class LegacyMigrationValidationError extends Error {
34
+ constructor(issues) {
35
+ super(`Legacy migration contract validation failed: ${issues.join("; ")}`);
36
+ this.name = "LegacyMigrationValidationError";
37
+ this.issues = issues;
38
+ }
39
+ }
40
+
41
+ export function stableCanonicalStringify(value) {
42
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
43
+ if (Array.isArray(value)) return `[${value.map(stableCanonicalStringify).join(",")}]`;
44
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableCanonicalStringify(value[key])}`).join(",")}}`;
45
+ }
46
+
47
+ export async function hashLegacyMigrationArtifact(value) {
48
+ const bytes = new TextEncoder().encode(typeof value === "string" ? value : stableCanonicalStringify(value));
49
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
50
+ return `sha256:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
51
+ }
52
+
53
+ function findCredentialFields(value, path = "$", found = [], seen = new WeakSet(), allowPublicApiKey = false) {
54
+ if (!value || typeof value !== "object" || seen.has(value)) return found;
55
+ seen.add(value);
56
+ if (Array.isArray(value)) {
57
+ value.forEach((entry, index) => findCredentialFields(entry, `${path}[${index}]`, found, seen, allowPublicApiKey));
58
+ return found;
59
+ }
60
+ for (const [key, entry] of Object.entries(value)) {
61
+ const entryPath = `${path}.${key}`;
62
+ const normalizedKey = key.replace(/[^A-Za-z0-9]/g, "").toLowerCase();
63
+ if (CREDENTIAL_KEY_RE.test(normalizedKey) && !(allowPublicApiKey && normalizedKey === "apikey")) found.push(entryPath);
64
+ findCredentialFields(entry, entryPath, found, seen, allowPublicApiKey);
65
+ }
66
+ return found;
67
+ }
68
+
69
+ function validateKeyedRows(rows, field, keyName, issues) {
70
+ if (!Array.isArray(rows)) {
71
+ issues.push(`${field}: must be an array`);
72
+ return new Set();
73
+ }
74
+ const keys = new Set();
75
+ rows.forEach((row, index) => {
76
+ const path = `${field}[${index}].${keyName}`;
77
+ const key = row?.[keyName];
78
+ if (!isNonEmptyString(key) || !ID_RE.test(key)) issues.push(`${path}: must be a path-safe 1-80 character key`);
79
+ else if (keys.has(key)) issues.push(`${path}: duplicate logical key ${JSON.stringify(key)}`);
80
+ else keys.add(key);
81
+ });
82
+ return keys;
83
+ }
84
+
85
+ export function validateLegacyOfferIntent(intent, { packageKeys } = {}) {
86
+ const issues = [];
87
+ if (!isObject(intent)) return ["offer: must be an object"];
88
+ rejectUnknownKeys(intent, new Set(["intent_key", "name", "offer_type", "code", "condition", "benefit"]), "offer", issues);
89
+ if (!isNonEmptyString(intent.intent_key) || !ID_RE.test(intent.intent_key))
90
+ issues.push("offer.intent_key: must be a path-safe 1-80 character key");
91
+ if (!isNonEmptyString(intent.name) || intent.name.length > 128) issues.push("offer.name: must be 1-128 characters");
92
+ if (!OFFER_TYPES.has(intent.offer_type)) issues.push("offer.offer_type: must be offer or voucher");
93
+ if (intent.offer_type === "voucher" && (!isNonEmptyString(intent.code) || intent.code.length > 64)) issues.push("offer.code: voucher offers require a code of 1-64 characters");
94
+ if (intent.offer_type !== "voucher" && intent.code != null) issues.push("offer.code: only voucher offers may carry a code");
95
+
96
+ const condition = intent.condition;
97
+ if (!isObject(condition)) issues.push("offer.condition: must be an object");
98
+ else {
99
+ rejectUnknownKeys(condition, new Set(["type", "value", "all_packages", "package_keys"]), "offer.condition", issues);
100
+ if (!CONDITION_TYPES.has(condition.type)) issues.push("offer.condition.type: must be any or count");
101
+ if (condition.all_packages !== false) issues.push("offer.condition.all_packages: must be false; migration offers require explicit package keys");
102
+ if (condition.type === "count" && !isPositiveInteger(condition.value))
103
+ issues.push("offer.condition.value: count requires an integer >= 1");
104
+ if (condition.type === "any" && condition.value != null)
105
+ issues.push("offer.condition.value: any must omit value or use null");
106
+ if (!Array.isArray(condition.package_keys) || condition.package_keys.length === 0)
107
+ issues.push("offer.condition.package_keys: must contain at least one package key");
108
+ else {
109
+ const seen = new Set();
110
+ for (const key of condition.package_keys) {
111
+ if (!isNonEmptyString(key) || !ID_RE.test(key)) issues.push(`offer.condition.package_keys: invalid key ${JSON.stringify(key)}`);
112
+ else if (seen.has(key)) issues.push(`offer.condition.package_keys: duplicate key ${JSON.stringify(key)}`);
113
+ else seen.add(key);
114
+ if (packageKeys && !packageKeys.has(key)) issues.push(`offer.condition.package_keys: unresolved package key ${JSON.stringify(key)}`);
115
+ }
116
+ }
117
+ }
118
+
119
+ const benefit = intent.benefit;
120
+ if (!isObject(benefit)) issues.push("offer.benefit: must be an object");
121
+ else {
122
+ rejectUnknownKeys(benefit, new Set(["type", "value", "price_rounding"]), "offer.benefit", issues);
123
+ if (!BENEFIT_TYPES.has(benefit.type))
124
+ issues.push("offer.benefit.type: must be package_percentage, shipping_percentage, or order_percentage");
125
+ if (!isMoneyString(benefit.value) || Number(benefit.value) <= 0 || Number(benefit.value) > 100)
126
+ issues.push("offer.benefit.value: must be a decimal percentage in (0, 100]");
127
+ if (benefit.price_rounding != null && !PRICE_ROUNDINGS.has(benefit.price_rounding))
128
+ issues.push("offer.benefit.price_rounding: must be 0.00, 0.95, 0.97, 0.99, or null");
129
+ }
130
+ return issues;
131
+ }
132
+
133
+ export function validateLegacyMigrationInventory(input) {
134
+ const issues = [];
135
+ if (!isObject(input)) return ["inventory: must be an object"];
136
+ rejectUnknownKeys(input, new Set(["schema_version", "migration_id", "source", "target"]), "$", issues);
137
+ if (input.schema_version !== LEGACY_MIGRATION_INVENTORY_VERSION)
138
+ issues.push(`schema_version: expected ${LEGACY_MIGRATION_INVENTORY_VERSION}`);
139
+ if (!isNonEmptyString(input.migration_id) || !ID_RE.test(input.migration_id))
140
+ issues.push("migration_id: must be a path-safe 1-80 character id");
141
+ if (!isObject(input.source)) issues.push("source: must be an object");
142
+ else {
143
+ rejectUnknownKeys(input.source, new Set(["campaign_id", "sdk_version"]), "source", issues);
144
+ if (!isPositiveInteger(input.source.campaign_id)) issues.push("source.campaign_id: must be a positive integer");
145
+ if (!SDK_03_RE.test(String(input.source.sdk_version ?? ""))) issues.push("source.sdk_version: must be an exact 0.3.x version");
146
+ }
147
+ const target = input.target;
148
+ if (!isObject(target)) issues.push("target: must be an object");
149
+ else rejectUnknownKeys(target, new Set(["campaign", "packages", "shipping_methods", "offer_intents", "authorized_domains"]), "target", issues);
150
+ const campaign = target?.campaign;
151
+ if (!isObject(campaign)) issues.push("target.campaign: must be an object");
152
+ else {
153
+ rejectUnknownKeys(campaign, new Set(["name", "currency", "language", "payment_gateway_group_id", "additional_currencies", "available_payment_methods", "available_express_payment_methods", "available_shipping_countries", "statement_descriptor"]), "target.campaign", issues);
154
+ for (const field of ["name", "currency", "language"])
155
+ if (!isNonEmptyString(campaign[field])) issues.push(`target.campaign.${field}: required non-empty string`);
156
+ if (isNonEmptyString(campaign.name) && campaign.name.length > 200) issues.push("target.campaign.name: must be at most 200 characters");
157
+ if (isNonEmptyString(campaign.currency) && !/^[A-Za-z]{3}$/.test(campaign.currency)) issues.push("target.campaign.currency: must be a three-letter currency code");
158
+ if (isNonEmptyString(campaign.language) && (campaign.language.length < 2 || campaign.language.length > 16)) issues.push("target.campaign.language: must be 2-16 characters");
159
+ if (!isPositiveInteger(campaign.payment_gateway_group_id))
160
+ issues.push("target.campaign.payment_gateway_group_id: must be a positive integer");
161
+ for (const field of ["additional_currencies", "available_payment_methods", "available_express_payment_methods", "available_shipping_countries"])
162
+ if (campaign[field] !== undefined && (!Array.isArray(campaign[field]) || campaign[field].some((value) => !isNonEmptyString(value))))
163
+ issues.push(`target.campaign.${field}: must be an array of non-empty strings`);
164
+ if (campaign.statement_descriptor !== undefined && (!isNonEmptyString(campaign.statement_descriptor) || campaign.statement_descriptor.length > 255))
165
+ issues.push("target.campaign.statement_descriptor: must be 1-255 characters");
166
+ }
167
+
168
+ const packageKeys = validateKeyedRows(target?.packages, "target.packages", "package_key", issues);
169
+ if (Array.isArray(target?.packages)) {
170
+ if (target.packages.length === 0) issues.push("target.packages: must contain at least one unit-product package");
171
+ target.packages.forEach((row, index) => {
172
+ const path = `target.packages[${index}]`;
173
+ if (!isObject(row)) return issues.push(`${path}: must be an object`);
174
+ rejectUnknownKeys(row, new Set(["package_key", "name", "product_id", "product_variant_id", "price", "price_recurring", "interval", "interval_count", ...PACKAGE_MERCHANDISING_FIELDS]), path, issues);
175
+ if (!isNonEmptyString(row.name) || row.name.length > 200) issues.push(`${path}.name: must be 1-200 characters`);
176
+ if (!isPositiveInteger(row.product_id)) issues.push(`${path}.product_id: must be a positive integer`);
177
+ if (!isPositiveInteger(row.product_variant_id)) issues.push(`${path}.product_variant_id: must be exactly one positive integer`);
178
+ if (!isMoneyString(row.price)) issues.push(`${path}.price: must be a non-negative decimal string with at most two places`);
179
+ if (row.price_recurring !== undefined && !isMoneyString(row.price_recurring)) issues.push(`${path}.price_recurring: must be a non-negative decimal string with at most two places`);
180
+ if (row.interval !== undefined && !isNonEmptyString(row.interval)) issues.push(`${path}.interval: must be a non-empty string`);
181
+ if (row.interval_count !== undefined && !isPositiveInteger(row.interval_count)) issues.push(`${path}.interval_count: must be a positive integer`);
182
+ for (const key of Object.keys(row))
183
+ if (PACKAGE_MERCHANDISING_FIELDS.has(key)) issues.push(`${path}.${key}: merchandising belongs in an Offer intent, not package identity`);
184
+ });
185
+ }
186
+
187
+ validateKeyedRows(target?.shipping_methods, "target.shipping_methods", "shipping_key", issues);
188
+ if (Array.isArray(target?.shipping_methods)) target.shipping_methods.forEach((row, index) => {
189
+ const path = `target.shipping_methods[${index}]`;
190
+ if (!isObject(row)) return issues.push(`${path}: must be an object`);
191
+ rejectUnknownKeys(row, new Set(["shipping_key", "shipping_method", "price"]), path, issues);
192
+ if (!isNonEmptyString(row.shipping_method)) issues.push(`${path}.shipping_method: required non-empty string`);
193
+ if (!isMoneyString(row.price)) issues.push(`${path}.price: must be a non-negative decimal string with at most two places`);
194
+ });
195
+
196
+ validateKeyedRows(target?.offer_intents, "target.offer_intents", "intent_key", issues);
197
+ if (Array.isArray(target?.offer_intents)) target.offer_intents.forEach((intent, index) => {
198
+ for (const issue of validateLegacyOfferIntent(intent, { packageKeys }))
199
+ issues.push(`target.offer_intents[${index}]: ${issue.replace(/^offer\./, "")}`);
200
+ });
201
+
202
+ if (!Array.isArray(target?.authorized_domains)) issues.push("target.authorized_domains: must be an array");
203
+ else {
204
+ const domains = new Set();
205
+ target.authorized_domains.forEach((host, index) => {
206
+ const normalized = typeof host === "string" ? host.toLowerCase() : "";
207
+ if (!isNonEmptyString(host) || !HOST_RE.test(normalized))
208
+ issues.push(`target.authorized_domains[${index}]: must be a hostname without scheme, port, or path`);
209
+ else if (domains.has(normalized)) issues.push(`target.authorized_domains[${index}]: duplicate hostname`);
210
+ else domains.add(normalized);
211
+ });
212
+ }
213
+
214
+ for (const field of findCredentialFields(input)) issues.push(`${field}: credential fields are forbidden recursively`);
215
+ return issues;
216
+ }
217
+
218
+ function optional(source, fields) {
219
+ return Object.fromEntries(fields.filter((field) => source[field] !== undefined).map((field) => [field, structuredClone(source[field])]));
220
+ }
221
+
222
+ export function normalizeLegacyMigrationInventory(input) {
223
+ const issues = validateLegacyMigrationInventory(input);
224
+ if (issues.length) throw new LegacyMigrationValidationError(issues);
225
+ const campaign = input.target.campaign;
226
+ return {
227
+ schema_version: LEGACY_MIGRATION_INVENTORY_VERSION,
228
+ migration_id: input.migration_id,
229
+ source: { campaign_id: Number(input.source.campaign_id), sdk_version: input.source.sdk_version },
230
+ target: {
231
+ campaign: {
232
+ name: campaign.name.trim(), currency: campaign.currency.trim().toUpperCase(), language: campaign.language.trim().toLowerCase(),
233
+ payment_gateway_group_id: Number(campaign.payment_gateway_group_id),
234
+ ...optional(campaign, ["additional_currencies", "available_payment_methods", "available_express_payment_methods", "available_shipping_countries", "statement_descriptor"]),
235
+ },
236
+ packages: input.target.packages.map((row) => ({
237
+ package_key: row.package_key, name: row.name.trim(), product_id: Number(row.product_id),
238
+ product_variant_id: Number(row.product_variant_id), price: String(row.price),
239
+ ...optional(row, ["price_recurring", "interval", "interval_count"]),
240
+ })).sort((a, b) => a.package_key.localeCompare(b.package_key)),
241
+ shipping_methods: input.target.shipping_methods.map((row) => ({
242
+ shipping_key: row.shipping_key, shipping_method: row.shipping_method.trim(), price: String(row.price),
243
+ })).sort((a, b) => a.shipping_key.localeCompare(b.shipping_key)),
244
+ offer_intents: input.target.offer_intents.map((intent) => ({
245
+ intent_key: intent.intent_key, name: intent.name.trim(), offer_type: intent.offer_type,
246
+ ...(intent.code != null ? { code: intent.code.trim() } : {}),
247
+ condition: {
248
+ type: intent.condition.type,
249
+ ...(intent.condition.type === "count" ? { value: intent.condition.value } : {}),
250
+ all_packages: false,
251
+ package_keys: [...intent.condition.package_keys].sort(),
252
+ },
253
+ benefit: {
254
+ type: intent.benefit.type, value: String(intent.benefit.value),
255
+ ...(intent.benefit.price_rounding != null ? { price_rounding: intent.benefit.price_rounding } : {}),
256
+ },
257
+ })).sort((a, b) => a.intent_key.localeCompare(b.intent_key)),
258
+ authorized_domains: input.target.authorized_domains.map((host) => host.toLowerCase()).sort(),
259
+ },
260
+ };
261
+ }
262
+
263
+ export const parseLegacyMigrationInventory = normalizeLegacyMigrationInventory;
264
+
265
+ export function buildLegacyOfferRequest(intent, packageIdByKey) {
266
+ const packageKeys = new Set(Object.keys(packageIdByKey ?? {}));
267
+ const issues = validateLegacyOfferIntent(intent, { packageKeys });
268
+ if (issues.length) throw new LegacyMigrationValidationError(issues);
269
+ const packageIds = intent.condition.package_keys.map((key) => packageIdByKey[key]);
270
+ if (packageIds.some((id) => !isPositiveInteger(id)))
271
+ throw new LegacyMigrationValidationError(["offer.condition.package_keys: every resolved package ID must be a positive integer"]);
272
+ const condition = { type: intent.condition.type, all_packages: false, package_ids: packageIds };
273
+ if (intent.condition.type === "count") condition.value = intent.condition.value;
274
+ const benefit = { type: intent.benefit.type, value: String(intent.benefit.value) };
275
+ if (intent.benefit.price_rounding != null) benefit.price_rounding = intent.benefit.price_rounding;
276
+ return {
277
+ name: intent.name,
278
+ offer_type: intent.offer_type,
279
+ ...(intent.offer_type === "voucher" ? { code: intent.code } : {}),
280
+ condition,
281
+ benefit,
282
+ };
283
+ }
284
+
285
+ function comparison(field, expected, actual, status, detail) {
286
+ return { field, expected, actual, status, ...(detail ? { detail } : {}) };
287
+ }
288
+
289
+ function equivalentCountValue(expected, actual) {
290
+ if (expected === actual) return true;
291
+ return isPositiveInteger(expected)
292
+ && typeof actual === "string"
293
+ && /^[1-9]\d*\.0+$/.test(actual)
294
+ && Number(actual) === expected;
295
+ }
296
+
297
+ export function compareLegacyOfferReadback(request, readback) {
298
+ const checks = [
299
+ comparison("name", request.name, readback?.name, request.name === readback?.name ? "match" : "mismatch"),
300
+ comparison("offer_type", request.offer_type, readback?.offer_type, request.offer_type === readback?.offer_type ? "match" : "mismatch"),
301
+ ];
302
+ const actualPackageIds = Array.isArray(readback?.condition?.packages)
303
+ ? readback.condition.packages.map((row) => row?.id).filter(isPositiveInteger).sort((a, b) => a - b)
304
+ : null;
305
+ const expectedPackageIds = [...request.condition.package_ids].sort((a, b) => a - b);
306
+ checks.push(comparison(
307
+ "condition.package_ids", expectedPackageIds, actualPackageIds,
308
+ actualPackageIds === null ? "unprojectable" : stableCanonicalStringify(expectedPackageIds) === stableCanonicalStringify(actualPackageIds) ? "match" : "mismatch",
309
+ actualPackageIds === null ? "readback.condition.packages[] is unavailable; package scope was not guessed" : undefined,
310
+ ));
311
+ checks.push(comparison("condition.all_packages", false, readback?.condition?.all_packages,
312
+ readback?.condition?.all_packages === false ? "match" : "mismatch"));
313
+ for (const field of ["type", "value"]) {
314
+ const expected = request.condition[field];
315
+ if (expected !== undefined) {
316
+ const actual = readback?.condition?.[field];
317
+ const matches = field === "value" && request.condition.type === "count"
318
+ ? equivalentCountValue(expected, actual)
319
+ : expected === actual;
320
+ checks.push(comparison(`condition.${field}`, expected, actual, matches ? "match" : "mismatch"));
321
+ }
322
+ }
323
+ for (const field of ["type", "value", "price_rounding"]) {
324
+ const expected = request.benefit[field];
325
+ if (expected !== undefined) checks.push(comparison(`benefit.${field}`, expected, readback?.benefit?.[field], String(expected) === String(readback?.benefit?.[field]) ? "match" : "mismatch"));
326
+ }
327
+ if (request.offer_type === "voucher") checks.push(comparison("code", request.code, readback?.code, request.code === readback?.code ? "match" : "mismatch"));
328
+ return { ok: checks.every((check) => check.status === "match"), checks };
329
+ }
330
+
331
+ export function compareLegacyPackageReadback(request, readback, { currency } = {}) {
332
+ const variantIds = request.product_variant_ids;
333
+ const checks = [];
334
+ const actualVariantId = readback?.product_variant_id;
335
+ checks.push(comparison("product_variant_ids", variantIds, isPositiveInteger(actualVariantId) ? [actualVariantId] : null,
336
+ !isPositiveInteger(actualVariantId) ? "unprojectable" : variantIds.length === 1 && variantIds[0] === actualVariantId ? "match" : "mismatch",
337
+ !isPositiveInteger(actualVariantId) ? "readback.product_variant_id is unavailable; package identity was not guessed" : undefined));
338
+ if (request.product_id !== undefined) checks.push(comparison("product_id", request.product_id, readback?.product_id,
339
+ String(request.product_id) === String(readback?.product_id) ? "match" : "mismatch"));
340
+ if (!currency) {
341
+ checks.push(comparison("price", request.price, null, "unprojectable", "campaign currency is required to select a readback prices[] row; no currency was guessed"));
342
+ } else {
343
+ const priceRow = Array.isArray(readback?.prices) ? readback.prices.find((row) => row?.currency === currency) : undefined;
344
+ checks.push(comparison("price", request.price, priceRow?.price ?? null,
345
+ priceRow ? String(request.price) === String(priceRow.price) ? "match" : "mismatch" : "unprojectable",
346
+ priceRow ? undefined : `readback.prices[] has no ${currency} row; price was not guessed`));
347
+ }
348
+ return { ok: checks.every((check) => check.status === "match"), checks };
349
+ }
350
+
351
+ export function validateLegacyProvisioningPlan(input) {
352
+ const issues = [];
353
+ if (!isObject(input)) return ["plan: must be an object"];
354
+ rejectUnknownKeys(input, new Set(["schema_version", "store", "migration_id", "inventory_hash", "preview_hash", "source", "operations", "offer_intents", "authorized_domains"]), "$", issues);
355
+ if (input.schema_version !== LEGACY_PROVISIONING_PLAN_VERSION) issues.push(`schema_version: expected ${LEGACY_PROVISIONING_PLAN_VERSION}`);
356
+ if (!isNonEmptyString(input.store)) issues.push("store: required non-empty string");
357
+ if (!isNonEmptyString(input.migration_id) || !ID_RE.test(input.migration_id)) issues.push("migration_id: must be a path-safe 1-80 character id");
358
+ if (!isObject(input.source) || !isPositiveInteger(input.source.campaign_id) || !SDK_03_RE.test(String(input.source.sdk_version ?? "")))
359
+ issues.push("source: requires a positive campaign_id and exact 0.3.x sdk_version");
360
+ else rejectUnknownKeys(input.source, new Set(["campaign_id", "sdk_version"]), "source", issues);
361
+ for (const field of ["inventory_hash", "preview_hash"])
362
+ if (!SHA256_RE.test(String(input[field] ?? ""))) issues.push(`${field}: must be a sha256 digest`);
363
+ validateKeyedRows(input.offer_intents, "offer_intents", "intent_key", issues);
364
+ const packageKeys = new Set();
365
+ const resourceKeys = new Set();
366
+ if (!Array.isArray(input.operations)) issues.push("operations: must be an array");
367
+ else {
368
+ const operationIds = new Set();
369
+ const dependencies = [];
370
+ input.operations.forEach((operation, index) => {
371
+ if (!isObject(operation)) return issues.push(`operations[${index}]: must be an object`);
372
+ rejectUnknownKeys(operation, new Set(["operation_id", "action", "resource_key", "depends_on", "request"]), `operations[${index}]`, issues);
373
+ if (!isNonEmptyString(operation.operation_id) || operationIds.has(operation.operation_id)) issues.push(`operations[${index}].operation_id: missing or duplicate`);
374
+ else operationIds.add(operation.operation_id);
375
+ if (!PLAN_OPERATION_ACTIONS.has(operation.action)) issues.push(`operations[${index}].action: unsupported action`);
376
+ const resourceIdentity = `${operation.action}:${operation.resource_key}`;
377
+ if (!isNonEmptyString(operation.resource_key) || resourceKeys.has(resourceIdentity)) issues.push(`operations[${index}].resource_key: missing or duplicate for ${operation.action}`);
378
+ else resourceKeys.add(resourceIdentity);
379
+ if (!isObject(operation.request)) issues.push(`operations[${index}].request: required object`);
380
+ if (operation.depends_on !== undefined) {
381
+ if (!isNonEmptyString(operation.depends_on)) issues.push(`operations[${index}].depends_on: must be a non-empty operation ID`);
382
+ else dependencies.push([index, operation.depends_on]);
383
+ }
384
+ if (operation.action === "package.create" && isNonEmptyString(operation.resource_key)) packageKeys.add(operation.resource_key);
385
+ });
386
+ for (const [index, dependency] of dependencies)
387
+ if (!operationIds.has(dependency)) issues.push(`operations[${index}].depends_on: unresolved operation ID ${JSON.stringify(dependency)}`);
388
+ }
389
+ if (Array.isArray(input.offer_intents)) input.offer_intents.forEach((intent, index) => {
390
+ for (const issue of validateLegacyOfferIntent(intent, { packageKeys })) issues.push(`offer_intents[${index}]: ${issue.replace(/^offer\./, "")}`);
391
+ });
392
+ if (!Array.isArray(input.authorized_domains)) issues.push("authorized_domains: must be an array");
393
+ else {
394
+ const domains = new Set();
395
+ input.authorized_domains.forEach((host, index) => {
396
+ const normalized = typeof host === "string" ? host.toLowerCase() : "";
397
+ if (!isNonEmptyString(host) || !HOST_RE.test(normalized)) issues.push(`authorized_domains[${index}]: invalid hostname`);
398
+ else if (domains.has(normalized)) issues.push(`authorized_domains[${index}]: duplicate hostname`);
399
+ else domains.add(normalized);
400
+ });
401
+ }
402
+ for (const field of findCredentialFields(input)) issues.push(`${field}: credential fields are forbidden recursively`);
403
+ return issues;
404
+ }
405
+
406
+ export function parseLegacyProvisioningPlan(input) {
407
+ const issues = validateLegacyProvisioningPlan(input);
408
+ if (issues.length) throw new LegacyMigrationValidationError(issues);
409
+ return structuredClone(input);
410
+ }
411
+
412
+ export function validateLegacyProvisioningReceipt(input) {
413
+ const issues = [];
414
+ if (!isObject(input)) return ["receipt: must be an object"];
415
+ rejectUnknownKeys(input, new Set(["schema_version", "store", "migration_id", "inventory_hash", "preview_hash", "state", "created_at", "updated_at", "applied_at", "campaign_id", "writes", "failure"]), "$", issues);
416
+ if (input.schema_version !== LEGACY_PROVISIONING_RECEIPT_VERSION) issues.push(`schema_version: expected ${LEGACY_PROVISIONING_RECEIPT_VERSION}`);
417
+ if (!isNonEmptyString(input.store)) issues.push("store: required non-empty string");
418
+ if (!isNonEmptyString(input.migration_id) || !ID_RE.test(input.migration_id)) issues.push("migration_id: must be a path-safe 1-80 character id");
419
+ if (!RECEIPT_STATES.has(input.state)) issues.push("state: unsupported receipt state");
420
+ for (const field of ["inventory_hash", "preview_hash"])
421
+ if (!SHA256_RE.test(String(input[field] ?? ""))) issues.push(`${field}: must be a sha256 digest`);
422
+ for (const field of ["created_at", "updated_at"])
423
+ if (!ISO_INSTANT_RE.test(String(input[field] ?? ""))) issues.push(`${field}: must be an ISO-8601 UTC instant`);
424
+ if (input.applied_at !== undefined && !ISO_INSTANT_RE.test(String(input.applied_at))) issues.push("applied_at: must be an ISO-8601 UTC instant");
425
+ if (!Object.hasOwn(input, "campaign_id")) issues.push("campaign_id: required receipt field");
426
+ else if (input.campaign_id !== null && !isPositiveInteger(input.campaign_id)) issues.push("campaign_id: must be null or a positive integer");
427
+ if (TERMINAL_RECEIPT_STATES.has(input.state)) {
428
+ if (!isPositiveInteger(input.campaign_id)) issues.push("campaign_id: terminal receipt requires a positive integer campaign ID");
429
+ if (!ISO_INSTANT_RE.test(String(input.applied_at ?? ""))) issues.push("applied_at: terminal receipt requires an explicit apply instant");
430
+ }
431
+ if (!Array.isArray(input.writes)) issues.push("writes: must be an array");
432
+ else {
433
+ const offerKeys = new Set();
434
+ const operationIds = new Set();
435
+ const resourceKeys = new Set();
436
+ input.writes.forEach((row, index) => {
437
+ if (!isObject(row)) return issues.push(`writes[${index}]: must be an object`);
438
+ rejectUnknownKeys(row, new Set(["operation_id", "action", "resource_key", "intent_key", "upstream_ids", "readback", "audit_key"]), `writes[${index}]`, issues);
439
+ if (!isNonEmptyString(row.operation_id) || operationIds.has(row.operation_id)) issues.push(`writes[${index}].operation_id: missing or duplicate`);
440
+ else operationIds.add(row.operation_id);
441
+ const resourceIdentity = `${row.action}:${row.resource_key}`;
442
+ if (!isNonEmptyString(row.resource_key) || resourceKeys.has(resourceIdentity)) issues.push(`writes[${index}].resource_key: missing or duplicate for ${row.action}`);
443
+ else resourceKeys.add(resourceIdentity);
444
+ if (row.intent_key !== undefined && (!isNonEmptyString(row.intent_key) || !ID_RE.test(row.intent_key)))
445
+ issues.push(`writes[${index}].intent_key: must be a path-safe 1-80 character key`);
446
+ if (row.readback !== undefined && !isObject(row.readback)) issues.push(`writes[${index}].readback: must be an object`);
447
+ if (row.audit_key !== undefined && !isNonEmptyString(row.audit_key)) issues.push(`writes[${index}].audit_key: must be a non-empty string`);
448
+ if (!isObject(row.upstream_ids)) issues.push(`writes[${index}].upstream_ids: must be an object`);
449
+ else {
450
+ rejectUnknownKeys(row.upstream_ids, new Set(["campaign_id", "package_id", "shipping_method_id", "offer_id"]), `writes[${index}].upstream_ids`, issues);
451
+ if (!isPositiveInteger(row.upstream_ids.campaign_id))
452
+ issues.push(`writes[${index}].upstream_ids.campaign_id: every successful write requires its positive parent campaign ID`);
453
+ else if (input.campaign_id !== null && row.upstream_ids.campaign_id !== input.campaign_id)
454
+ issues.push(`writes[${index}].upstream_ids.campaign_id: must equal the receipt campaign_id`);
455
+ for (const field of ["package_id", "shipping_method_id", "offer_id"])
456
+ if (row.upstream_ids[field] !== undefined && row.upstream_ids[field] !== null && !isPositiveInteger(row.upstream_ids[field]))
457
+ issues.push(`writes[${index}].upstream_ids.${field}: must be null or a positive integer`);
458
+ }
459
+ const idField = {
460
+ "campaign.create": "campaign_id", "package.create": "package_id",
461
+ "shipping_method.create": "shipping_method_id", "offer.create": "offer_id",
462
+ }[row.action];
463
+ if (!idField) issues.push(`writes[${index}].action: unsupported action`);
464
+ else if (!isPositiveInteger(row.upstream_ids?.[idField])) issues.push(`writes[${index}].upstream_ids.${idField}: successful write requires a positive integer ID`);
465
+ if (row.action !== "offer.create") return;
466
+ if (!isNonEmptyString(row.intent_key) || !ID_RE.test(row.intent_key)) issues.push(`writes[${index}].intent_key: offer.create requires a path-safe 1-80 character intent key`);
467
+ else if (offerKeys.has(row.intent_key)) issues.push(`writes[${index}].intent_key: duplicate applied Offer intent`);
468
+ else offerKeys.add(row.intent_key);
469
+ });
470
+ }
471
+ if (input.failure !== undefined && input.failure !== null && !isObject(input.failure)) issues.push("failure: must be an object or null");
472
+ for (const field of findCredentialFields(input, "$", [], new WeakSet(), true)) issues.push(`${field}: private credential fields are forbidden recursively`);
473
+ return issues;
474
+ }
475
+
476
+ export function parseLegacyProvisioningReceipt(input) {
477
+ const issues = validateLegacyProvisioningReceipt(input);
478
+ if (issues.length) throw new LegacyMigrationValidationError(issues);
479
+ return structuredClone(input);
480
+ }
481
+
482
+ export async function projectLegacyProvisioningReceipt(receipt) {
483
+ const parsed = parseLegacyProvisioningReceipt(receipt);
484
+ if (!TERMINAL_RECEIPT_STATES.has(parsed.state) || !ISO_INSTANT_RE.test(String(parsed.applied_at ?? "")))
485
+ throw new LegacyMigrationValidationError(["applied_at: a receipt must be terminal and carry an explicit apply instant before it can become build evidence"]);
486
+ const receiptHash = await hashLegacyMigrationArtifact(parsed);
487
+ const offerIntentKeys = parsed.writes
488
+ .filter((row) => row.action === "offer.create")
489
+ .map((row) => row.intent_key)
490
+ .sort();
491
+ return {
492
+ campaign_id: parsed.campaign_id,
493
+ package_ids: Object.fromEntries(parsed.writes
494
+ .filter((row) => row.action === "package.create")
495
+ .map((row) => [row.resource_key, row.upstream_ids.package_id])
496
+ .sort(([a], [b]) => a.localeCompare(b))),
497
+ shipping_method_ids: Object.fromEntries(parsed.writes
498
+ .filter((row) => row.action === "shipping_method.create")
499
+ .map((row) => [row.resource_key, row.upstream_ids.shipping_method_id])
500
+ .sort(([a], [b]) => a.localeCompare(b))),
501
+ offer_ids: Object.fromEntries(parsed.writes
502
+ .filter((row) => row.action === "offer.create")
503
+ .map((row) => [row.intent_key, row.upstream_ids.offer_id])
504
+ .sort(([a], [b]) => a.localeCompare(b))),
505
+ offer_intent_keys: offerIntentKeys,
506
+ plan_hash: parsed.preview_hash,
507
+ receipt_hash: receiptHash,
508
+ applied_at: parsed.applied_at,
509
+ };
510
+ }