@tokenoftrust/storefront-runner 1.3.4-rc.4 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/apps/storefront/astro.config.mjs +15 -0
  2. package/apps/storefront/dev-plugins/dev-publish.mjs +55 -0
  3. package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +86 -7
  4. package/apps/storefront/drizzle.config.apps.ts +13 -0
  5. package/apps/storefront/env.d.ts +10 -0
  6. package/apps/storefront/migrations/README.md +13 -7
  7. package/apps/storefront/migrations-apps/0000_fast_millenium_guard.sql +129 -0
  8. package/apps/storefront/migrations-apps/0001_woozy_lyja.sql +22 -0
  9. package/apps/storefront/migrations-apps/meta/0000_snapshot.json +843 -0
  10. package/apps/storefront/migrations-apps/meta/0001_snapshot.json +990 -0
  11. package/apps/storefront/migrations-apps/meta/_journal.json +20 -0
  12. package/apps/storefront/package.json +12 -1
  13. package/apps/storefront/perf/README.md +64 -0
  14. package/apps/storefront/perf/assert-budgets.ts +159 -0
  15. package/apps/storefront/playwright.config.ts +23 -0
  16. package/apps/storefront/public/js/dashboard-apps.js +173 -0
  17. package/apps/storefront/public/shared/commerce-marketing.css +221 -0
  18. package/apps/storefront/src/components/CollectionCard.astro +1 -0
  19. package/apps/storefront/src/components/ProductCard.astro +1 -0
  20. package/apps/storefront/src/components/admin/AdminPublishTab.astro +3562 -0
  21. package/apps/storefront/src/components/apps/AppWidgetFrame.astro +30 -0
  22. package/apps/storefront/src/components/chrome/NavDropdown.astro +6 -3
  23. package/apps/storefront/src/components/chrome/SiteFooter.astro +10 -0
  24. package/apps/storefront/src/components/chrome/SiteHeader.astro +10 -0
  25. package/apps/storefront/src/components/commerce/RatingStars.astro +3 -2
  26. package/apps/storefront/src/components/content/Callout.astro +75 -0
  27. package/apps/storefront/src/components/content/NeedsReviewCallout.astro +66 -0
  28. package/apps/storefront/src/components/content/ProseSections.astro +121 -0
  29. package/apps/storefront/src/components/content/ProseToc.astro +34 -0
  30. package/apps/storefront/src/components/content/RichText.astro +44 -0
  31. package/apps/storefront/src/components/content/TrustStrip.astro +46 -0
  32. package/apps/storefront/src/components/home/Hero.astro +14 -0
  33. package/apps/storefront/src/components/islands/CheckoutComplianceGate.tsx +295 -0
  34. package/apps/storefront/src/components/islands/ImageGallery.tsx +42 -21
  35. package/apps/storefront/src/components/islands/VariantSelector.tsx +62 -12
  36. package/apps/storefront/src/components/plp/FacetSidebar.astro +2 -2
  37. package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +1 -0
  38. package/apps/storefront/src/config/compliance/rulesets.ts +79 -0
  39. package/apps/storefront/src/config/storeName.ts +34 -0
  40. package/apps/storefront/src/layouts/Layout.astro +97 -23
  41. package/apps/storefront/src/lib/activity/alerts.ts +428 -0
  42. package/apps/storefront/src/lib/activity/changeActorAttribution.ts +85 -0
  43. package/apps/storefront/src/lib/activity/deployVersion.ts +127 -0
  44. package/apps/storefront/src/lib/activity/ingest.ts +188 -0
  45. package/apps/storefront/src/lib/activity/ingestAuth.ts +105 -0
  46. package/apps/storefront/src/lib/activity/killSwitch.ts +80 -0
  47. package/apps/storefront/src/lib/activity/query.ts +403 -0
  48. package/apps/storefront/src/lib/activity/recordActivity.ts +105 -0
  49. package/apps/storefront/src/lib/activity/store.ts +122 -0
  50. package/apps/storefront/src/lib/activity/uiActor.ts +150 -0
  51. package/apps/storefront/src/lib/activity/workerCommit.ts +70 -0
  52. package/apps/storefront/src/lib/analytics/budgets.json +69 -0
  53. package/apps/storefront/src/lib/analytics/lighthouseReport.ts +109 -0
  54. package/apps/storefront/src/lib/analytics/perfBudgets.ts +452 -0
  55. package/apps/storefront/src/lib/analytics/rumAlert.ts +179 -0
  56. package/apps/storefront/src/lib/analytics/webVitals.ts +269 -0
  57. package/apps/storefront/src/lib/apps/adminService.ts +106 -0
  58. package/apps/storefront/src/lib/apps/adminSession.ts +205 -0
  59. package/apps/storefront/src/lib/apps/apiAuth.ts +91 -0
  60. package/apps/storefront/src/lib/apps/apiRoute.ts +35 -0
  61. package/apps/storefront/src/lib/apps/catalogMapper.ts +39 -0
  62. package/apps/storefront/src/lib/apps/credentials.ts +153 -0
  63. package/apps/storefront/src/lib/apps/gatewayKeys.ts +156 -0
  64. package/apps/storefront/src/lib/apps/healthAggregate.ts +66 -0
  65. package/apps/storefront/src/lib/apps/orders/attributionService.ts +206 -0
  66. package/apps/storefront/src/lib/apps/orders/customerHash.ts +28 -0
  67. package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +203 -0
  68. package/apps/storefront/src/lib/apps/orders/idempotency.ts +100 -0
  69. package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +117 -0
  70. package/apps/storefront/src/lib/apps/orders/orderMapper.ts +91 -0
  71. package/apps/storefront/src/lib/apps/orders/ordersStore.ts +181 -0
  72. package/apps/storefront/src/lib/apps/registryService.ts +579 -0
  73. package/apps/storefront/src/lib/apps/scopes.ts +79 -0
  74. package/apps/storefront/src/lib/apps/tokenIssuer.ts +121 -0
  75. package/apps/storefront/src/lib/apps/tokenVerifier.ts +148 -0
  76. package/apps/storefront/src/lib/apps/widgets/eligibility.ts +18 -0
  77. package/apps/storefront/src/lib/apps/widgets/frameProps.ts +52 -0
  78. package/apps/storefront/src/lib/apps/widgets/launchToken.ts +84 -0
  79. package/apps/storefront/src/lib/apps/widgets/placements.ts +57 -0
  80. package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +111 -0
  81. package/apps/storefront/src/lib/auth/adminEntry.ts +119 -0
  82. package/apps/storefront/src/lib/auth/identityToken.ts +21 -2
  83. package/apps/storefront/src/lib/auth/loginGate.ts +102 -18
  84. package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +219 -0
  85. package/apps/storefront/src/lib/auth/route.ts +60 -5
  86. package/apps/storefront/src/lib/auth/session.ts +8 -0
  87. package/apps/storefront/src/lib/auth/stepUpChallenge.ts +107 -0
  88. package/apps/storefront/src/lib/auth/totAccessClient.ts +208 -0
  89. package/apps/storefront/src/lib/blog/provider.ts +39 -0
  90. package/apps/storefront/src/lib/blog/types.ts +26 -0
  91. package/apps/storefront/src/lib/checkoutCommerce.ts +39 -1
  92. package/apps/storefront/src/lib/chrome/model.ts +11 -0
  93. package/apps/storefront/src/lib/compliance/enforcement.ts +95 -0
  94. package/apps/storefront/src/lib/content/callout.ts +78 -0
  95. package/apps/storefront/src/lib/content/index.ts +27 -0
  96. package/apps/storefront/src/lib/content/needsReview.ts +58 -0
  97. package/apps/storefront/src/lib/content/prose.ts +186 -0
  98. package/apps/storefront/src/lib/content/richtext.ts +76 -0
  99. package/apps/storefront/src/lib/content/trustStrip.ts +74 -0
  100. package/apps/storefront/src/lib/content-edit/client.ts +70 -14
  101. package/apps/storefront/src/lib/d1/catalog.ts +12 -0
  102. package/apps/storefront/src/lib/d1/schema-apps.ts +267 -0
  103. package/apps/storefront/src/lib/dev/apiBase.ts +8 -2
  104. package/apps/storefront/src/lib/dev/cliSignInCode.ts +65 -115
  105. package/apps/storefront/src/lib/dev/cockpitStore.ts +65 -0
  106. package/apps/storefront/src/lib/dev/previewStatus.ts +112 -0
  107. package/apps/storefront/src/lib/dev/rendezvousBroker.ts +241 -0
  108. package/apps/storefront/src/lib/dev/subjectReissue.ts +67 -0
  109. package/apps/storefront/src/lib/dev/vcBinding.ts +80 -0
  110. package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +10 -10
  111. package/apps/storefront/src/lib/env.ts +63 -0
  112. package/apps/storefront/src/lib/jsonld.ts +12 -17
  113. package/apps/storefront/src/lib/membership/eligibility.ts +37 -0
  114. package/apps/storefront/src/lib/monitoring/manifest.ts +302 -0
  115. package/apps/storefront/src/lib/privacy/emailHint.ts +13 -5
  116. package/apps/storefront/src/lib/publish/apex-readiness.ts +337 -0
  117. package/apps/storefront/src/lib/publish/dispatchHealth.ts +269 -0
  118. package/apps/storefront/src/lib/publish/domainState.ts +351 -0
  119. package/apps/storefront/src/lib/publish/shipWorkspace.ts +369 -0
  120. package/apps/storefront/src/lib/rawChrome.ts +34 -3
  121. package/apps/storefront/src/lib/storyblok/content-model.ts +34 -2
  122. package/apps/storefront/src/lib/storyblok/provider.ts +11 -4
  123. package/apps/storefront/src/lib/subscription/model.ts +114 -0
  124. package/apps/storefront/src/lib/tot/ToTClient.ts +3 -3
  125. package/apps/storefront/src/lib/tot/query.ts +32 -0
  126. package/apps/storefront/src/lib/webhooks/cloudflareQueueDispatcher.ts +82 -0
  127. package/apps/storefront/src/lib/webhooks/deliveryEngine.ts +245 -0
  128. package/apps/storefront/src/lib/webhooks/deliveryMapper.ts +34 -0
  129. package/apps/storefront/src/lib/webhooks/deliveryStore.ts +668 -0
  130. package/apps/storefront/src/lib/webhooks/dispatcher.ts +168 -0
  131. package/apps/storefront/src/lib/webhooks/emit.ts +167 -0
  132. package/apps/storefront/src/lib/webhooks/endpointGuard.ts +135 -0
  133. package/apps/storefront/src/lib/webhooks/events.ts +98 -0
  134. package/apps/storefront/src/lib/webhooks/getDispatcher.ts +49 -0
  135. package/apps/storefront/src/middleware/index.ts +45 -13
  136. package/apps/storefront/src/pages/404.astro +21 -9
  137. package/apps/storefront/src/pages/[...slug].astro +36 -2
  138. package/apps/storefront/src/pages/admin/ops-timeline.astro +420 -0
  139. package/apps/storefront/src/pages/admin.astro +183 -9
  140. package/apps/storefront/src/pages/api/activity.ts +137 -0
  141. package/apps/storefront/src/pages/api/admin/activity-alerts.ts +73 -0
  142. package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +86 -0
  143. package/apps/storefront/src/pages/api/apps/admin/health.ts +44 -0
  144. package/apps/storefront/src/pages/api/apps/admin/install.ts +125 -0
  145. package/apps/storefront/src/pages/api/apps/admin/list.ts +26 -0
  146. package/apps/storefront/src/pages/api/apps/admin/resume.ts +79 -0
  147. package/apps/storefront/src/pages/api/apps/admin/suspend.ts +80 -0
  148. package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +98 -0
  149. package/apps/storefront/src/pages/api/apps/admin/update.ts +126 -0
  150. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +70 -0
  151. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +51 -0
  152. package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +183 -0
  153. package/apps/storefront/src/pages/api/apps/oauth/token.ts +87 -0
  154. package/apps/storefront/src/pages/api/apps/v1/attribution.ts +162 -0
  155. package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +39 -0
  156. package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +49 -0
  157. package/apps/storefront/src/pages/api/apps/v1/health.ts +32 -0
  158. package/apps/storefront/src/pages/api/apps/v1/inventory.ts +59 -0
  159. package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +48 -0
  160. package/apps/storefront/src/pages/api/apps/v1/orders.ts +73 -0
  161. package/apps/storefront/src/pages/api/apps/v1/reports.ts +21 -0
  162. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +74 -0
  163. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +47 -0
  164. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +48 -1
  165. package/apps/storefront/src/pages/api/auth/step-up-send.ts +57 -0
  166. package/apps/storefront/src/pages/api/auth/step-up-verify.ts +129 -0
  167. package/apps/storefront/src/pages/api/auth/verify.ts +23 -0
  168. package/apps/storefront/src/pages/api/cache-purge.ts +11 -0
  169. package/apps/storefront/src/pages/api/compliance/preflight.ts +206 -0
  170. package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +30 -0
  171. package/apps/storefront/src/pages/api/rum/vitals.ts +54 -0
  172. package/apps/storefront/src/pages/api/test/dev-session.ts +133 -0
  173. package/apps/storefront/src/pages/auth/login.astro +202 -45
  174. package/apps/storefront/src/pages/auth/magic.astro +75 -43
  175. package/apps/storefront/src/pages/blog/[slug].astro +107 -0
  176. package/apps/storefront/src/pages/blog/index.astro +98 -0
  177. package/apps/storefront/src/pages/capabilities.astro +8 -0
  178. package/apps/storefront/src/pages/cockpit.astro +470 -69
  179. package/apps/storefront/src/pages/collections/[handle].astro +8 -0
  180. package/apps/storefront/src/pages/collections/index.astro +10 -2
  181. package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +119 -0
  182. package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +5 -0
  183. package/apps/storefront/src/pages/index.astro +57 -0
  184. package/apps/storefront/src/pages/llms.txt.ts +31 -10
  185. package/apps/storefront/src/pages/products/[handle].astro +100 -9
  186. package/apps/storefront/src/pages/sitemap.xml.ts +21 -4
  187. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +198 -0
  188. package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +7 -0
  189. package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +7 -0
  190. package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +7 -0
  191. package/apps/storefront/src/pages/style-guide/index.astro +10 -0
  192. package/apps/storefront/src/styles/fonts.css +54 -0
  193. package/apps/storefront/src/styles/global.css +22 -2
  194. package/apps/storefront/src/themes/schema.ts +3 -25
  195. package/apps/storefront/tsconfig.json +1 -1
  196. package/apps/storefront/vitest.config.ts +4 -1
  197. package/package.json +1 -1
  198. package/packages/public-runtime/src/activity/README.md +146 -0
  199. package/packages/public-runtime/src/activity/catalog.ts +501 -0
  200. package/packages/public-runtime/src/activity/event.ts +168 -0
  201. package/packages/public-runtime/src/activity/index.ts +16 -0
  202. package/packages/public-runtime/src/activity/redaction.ts +263 -0
  203. package/packages/public-runtime/src/candidate-index.ts +324 -0
  204. package/packages/public-runtime/src/checkout.ts +94 -2
  205. package/packages/public-runtime/src/compliance/evaluate.ts +265 -0
  206. package/packages/public-runtime/src/compliance/evidence-signals.ts +81 -0
  207. package/packages/public-runtime/src/compliance/index.ts +22 -0
  208. package/packages/public-runtime/src/compliance/pact-report.ts +94 -0
  209. package/packages/public-runtime/src/compliance/profile.ts +198 -0
  210. package/packages/public-runtime/src/compliance/ruleset.ts +117 -0
  211. package/packages/public-runtime/src/compliance/verification.ts +81 -0
  212. package/packages/public-runtime/src/csp.ts +27 -3
  213. package/packages/public-runtime/src/customization-reconcile.ts +35 -0
  214. package/packages/public-runtime/src/customization-runtime.ts +8 -0
  215. package/packages/public-runtime/src/customization-versioning.ts +17 -0
  216. package/packages/public-runtime/src/extension-contract.ts +2 -1
  217. package/packages/public-runtime/src/hash.ts +25 -0
  218. package/packages/public-runtime/src/index.ts +12 -0
  219. package/packages/public-runtime/src/membership.ts +353 -0
  220. package/packages/public-runtime/src/product.ts +24 -2
  221. package/packages/public-runtime/src/review-trust-proof.ts +194 -0
  222. package/packages/public-runtime/src/tenant-assets.ts +40 -5
  223. package/packages/public-runtime/src/tenant.ts +236 -0
  224. package/packages/public-runtime/src/widget-postmessage.ts +205 -0
  225. package/scripts/dev/publish.mjs +158 -0
  226. package/scripts/dev/transient-files.mjs +2 -1
  227. package/tenants/home/public/fonts/inter-latin-400-normal.woff2 +0 -0
  228. package/tenants/home/public/fonts/inter-latin-500-normal.woff2 +0 -0
  229. package/tenants/home/public/fonts/inter-latin-600-normal.woff2 +0 -0
  230. package/tenants/home/public/fonts/inter-latin-ext-400-normal.woff2 +0 -0
  231. package/tenants/home/public/fonts/inter-latin-ext-500-normal.woff2 +0 -0
  232. package/tenants/home/public/fonts/inter-latin-ext-600-normal.woff2 +0 -0
  233. package/tenants/home/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  234. package/tenants/home/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
  235. package/tenants/home/public/fonts/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
  236. package/tenants/home/public/fonts/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
  237. package/tenants/home/public/fonts/plus-jakarta-sans-latin-500-normal.woff2 +0 -0
  238. package/tenants/home/public/fonts/plus-jakarta-sans-latin-600-normal.woff2 +0 -0
  239. package/tenants/home/public/fonts/plus-jakarta-sans-latin-700-normal.woff2 +0 -0
  240. package/tenants/home/public/fonts/plus-jakarta-sans-latin-800-normal.woff2 +0 -0
  241. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-500-normal.woff2 +0 -0
  242. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-600-normal.woff2 +0 -0
  243. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-700-normal.woff2 +0 -0
  244. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-800-normal.woff2 +0 -0
  245. package/tenants/home/public/pages/storefront.css +23 -0
  246. package/apps/storefront/src/lib/dev/hostedCockpit.ts +0 -169
@@ -0,0 +1,501 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // Action catalog — the ALLOWLISTED registry of actionKeys (card D0).
3
+ //
4
+ // The `action` field of an ActivityEvent is NEVER a free string. It is one of
5
+ // the keys below, and each key ships a spec: which actor kinds may emit it,
6
+ // which source, which store sink(s) it lands in, and — critically — the
7
+ // default-DENY allowlist of payload fields that survive redaction for it.
8
+ //
9
+ // WHY A FIXED CATALOG. Free-string actions make the downstream query/alert
10
+ // surface (D5/D6/D7) intractable: you cannot build a dashboard, an alert rule,
11
+ // or a redaction allowlist against an open vocabulary. Adding a NEW action is a
12
+ // deliberate edit HERE (plus, for the CLI, the JS mirror + its keep-in-step
13
+ // test — see the module README), not an ad-hoc string at a call site.
14
+ //
15
+ // GROUNDING. These keys were reverse-engineered from the activity surfaces that
16
+ // already exist informally in this repo, so D1–D3 map onto them 1:1 rather than
17
+ // inventing new vocab:
18
+ // • CLI command dispatch + ~/.tot/activity.log → cli.command.*
19
+ // • obstacle beacon (obstacle-beacon.cjs) → cli.obstacle.reported
20
+ // • local→hosted dev bridge (dev/activityStore, runtimeStore, obstacleStore)
21
+ // → dev.*
22
+ // • preview serve / reconcile / Gitea webhook → preview.* / reconcile.* / webhook.*
23
+ // • deploy version observed (provenance, D2) → deploy.version_observed
24
+ // • ShipLoop CHANGE_STATES + admin routes
25
+ // (accept/reject/ship/revert/rollback/integrate/publish/invite/retire)
26
+ // → change.* / content.* / grant.* / preview.*
27
+ // • app installs (STOREFRONT_APPS_DB) → app.*
28
+ // • agent cohort (later) → agent.*
29
+ // ─────────────────────────────────────────────────────────────────────────────
30
+ import type { ActorKind, ActivitySource, ActivitySink } from "./event.js";
31
+
32
+ /**
33
+ * Metadata for one catalog action. `argsAllow` / `renderedAllow` are the
34
+ * default-DENY payload allowlists consumed by redaction.ts: ONLY these top-level
35
+ * keys survive; everything else is dropped. An empty allowlist means the action
36
+ * stores no payload of that lane at all.
37
+ */
38
+ export interface ActionSpec {
39
+ /** Actor kinds legitimately allowed to emit this action (ingest rejects others). */
40
+ actors: readonly ActorKind[];
41
+ /** Where the action originates. */
42
+ source: ActivitySource;
43
+ /**
44
+ * Store routing (the per-action half of the store decision, DECISIONS.md §8).
45
+ * Every action includes "logs" (universal, zero-provision firehose). High-
46
+ * volume/query-light actions add "analytics" (Cloudflare Analytics Engine).
47
+ * Low-volume/high-value lifecycle actions add "timeline" (D1, queryable ops UI).
48
+ */
49
+ sinks: readonly ActivitySink[];
50
+ /** Allowlist: payload.args keys that survive redaction. Default-deny. */
51
+ argsAllow: readonly string[];
52
+ /** Allowlist: payload.rendered keys that survive redaction. Default-deny. */
53
+ renderedAllow: readonly string[];
54
+ /** One-line human description for a query legend / UI. */
55
+ desc: string;
56
+ }
57
+
58
+ // Sink shorthands — keep the registry readable and the routing policy in one place.
59
+ const LOGS: readonly ActivitySink[] = ["logs"];
60
+ const HIGH_VOLUME: readonly ActivitySink[] = ["logs", "analytics"];
61
+ const LIFECYCLE: readonly ActivitySink[] = ["logs", "timeline"];
62
+ const PROVENANCE: readonly ActivitySink[] = ["logs", "analytics", "timeline"];
63
+
64
+ /**
65
+ * THE CATALOG. Add a new action by adding a key here (and its JS mirror for the
66
+ * CLI).
67
+ *
68
+ * NAMING STANDARD (two-tier — every key is dot-namespaced; the FIRST segment is a
69
+ * DOMAIN from the closed set below, and there are NO un-namespaced keys):
70
+ *
71
+ * • `<domain>.<verb>` — IMPERATIVE, for an ATOMIC action that happens in one
72
+ * shot: `change.ship`, `grant.invite`, `store.connect`, `change.go_live`.
73
+ * • `<domain>.<entity>.<verb|state>` — PARTICIPLE/STATE, for a TWO-PHASE / async
74
+ * action that opens then closes (an invoke/result pair, or a received/terminal
75
+ * span): `cli.command.invoked` + `cli.command.result`, `preview.build.requested`
76
+ * + `preview.build.completed`, `webhook.reconcile.received` + `reconcile.terminal`.
77
+ *
78
+ * The DOMAIN prefix is stable (dashboards and alert rules glob on it) and MUST be one
79
+ * of the closed set: cli, dev, preview, webhook, reconcile, deploy, change, content,
80
+ * grant, store, app, agent, system.
81
+ *
82
+ * TWO-PHASE PAIRING GOING FORWARD: a NEW two-phase action uses `.invoked` / `.result`.
83
+ * The pre-existing `.requested` / `.completed` (preview.build.*) and `.received` /
84
+ * `.terminal` (webhook.reconcile.received + reconcile.terminal) pairs are GRANDFATHERED
85
+ * verbatim — do NOT rename them (alerts.ts hardcodes some of these literals).
86
+ */
87
+ export const ACTION_CATALOG = {
88
+ // ── CLI (source: cli) ──────────────────────────────────────────────────────
89
+ // Grounded in packages/cli command dispatch + activity-log.mjs. High-volume:
90
+ // a developer runs dozens–hundreds of commands/day. NOTE the redaction
91
+ // allowlist excludes raw `args` argv (paths/emails/codes) — only the command
92
+ // NAME + version survive; the CLI's own activity-log.mjs already redacts
93
+ // --code/--token, and here we go further (deny-by-default).
94
+ "cli.command.invoked": {
95
+ actors: ["dev", "agent"], source: "cli", sinks: HIGH_VOLUME,
96
+ argsAllow: ["command", "subcommand", "cliVersion", "node"], renderedAllow: [],
97
+ desc: "A `tot` CLI command was invoked.",
98
+ },
99
+ "cli.command.result": {
100
+ actors: ["dev", "agent"], source: "cli", sinks: HIGH_VOLUME,
101
+ argsAllow: ["command", "subcommand", "cliVersion", "exitCode", "durationMs"], renderedAllow: [],
102
+ desc: "A `tot` CLI command finished (exit code + duration).",
103
+ },
104
+ "cli.obstacle.reported": {
105
+ // The obstacle beacon (obstacle-beacon.cjs) — a failed `tot start`/`tot clone`
106
+ // preflight. Low volume but support-valuable → also timelined.
107
+ actors: ["dev"], source: "cli", sinks: PROVENANCE,
108
+ argsAllow: ["kind", "have", "need", "cliVersion"], renderedAllow: [],
109
+ desc: "A local `tot start`/`tot clone` failed preflight (node-too-old, pnpm-missing, install/clone).",
110
+ },
111
+ "cli.signin.requested": {
112
+ actors: ["dev"], source: "cli", sinks: LOGS,
113
+ argsAllow: ["cliVersion"], renderedAllow: [],
114
+ desc: "The CLI requested a browserless sign-in code.",
115
+ },
116
+
117
+ // ── Local dev loop → hosted bridge (source: cli) ───────────────────────────
118
+ // Grounded in dev/activityStore.ts, runtimeStore.ts. Heartbeats are the single
119
+ // highest-volume stream (one every few seconds while `tot dev` runs) → analytics
120
+ // only, never timelined. Local paths (cwd/url/editor) are DROPPED by allowlist.
121
+ "dev.runtime.heartbeat": {
122
+ actors: ["dev"], source: "cli", sinks: HIGH_VOLUME,
123
+ argsAllow: ["cliVersion", "runnerVersion", "os", "osVersion", "arch"], renderedAllow: [],
124
+ desc: "The developer's local dev loop reported a liveness heartbeat.",
125
+ },
126
+ "dev.file.changed": {
127
+ actors: ["dev"], source: "cli", sinks: HIGH_VOLUME,
128
+ // Raw file path is DROPPED (local, may carry names); only the event kind +
129
+ // the server-composed business description (activityStore.describeActivity)
130
+ // survive.
131
+ argsAllow: ["event"], renderedAllow: ["description"],
132
+ desc: "A file save was reported by the local dev loop.",
133
+ },
134
+ "dev.serve.up": {
135
+ actors: ["dev"], source: "cli", sinks: LOGS,
136
+ argsAllow: [], renderedAllow: [],
137
+ desc: "The developer's store came up locally.",
138
+ },
139
+
140
+ // ── Preview / reconcile / webhook (source: server) ─────────────────────────
141
+ "preview.serve": {
142
+ actors: ["system"], source: "server", sinks: HIGH_VOLUME,
143
+ argsAllow: [], renderedAllow: [],
144
+ desc: "A preview environment served a request (per-request, high volume).",
145
+ },
146
+ "preview.build.requested": {
147
+ actors: ["admin", "dev", "system"], source: "server", sinks: LIFECYCLE,
148
+ argsAllow: ["changeId"], renderedAllow: [],
149
+ desc: "A preview build was requested.",
150
+ },
151
+ "preview.build.completed": {
152
+ actors: ["system"], source: "server", sinks: LIFECYCLE,
153
+ argsAllow: ["changeId", "result"], renderedAllow: [],
154
+ desc: "A preview build finished (result: ok|failed).",
155
+ },
156
+ "preview.override": {
157
+ // D9: an AUTHENTICATED operator accepted the risk of a blocking-safety finding for a
158
+ // specific commit (/api/preview/override), letting the reconcile materialize past it.
159
+ // Cockpit-gated (signed-in real developer capability + session tenant), so the acting
160
+ // cohort is owner→merchant, staff→admin, or a signed-in developer→dev. The required
161
+ // justification is operator-authored → rendered (truncated + canary-scanned).
162
+ actors: ["admin", "merchant", "dev"], source: "ui", sinks: LIFECYCLE,
163
+ argsAllow: ["commit"], renderedAllow: ["reason"],
164
+ desc: "An operator overrode a blocking-safety finding for a preview commit (auditable).",
165
+ },
166
+ "webhook.reconcile.received": {
167
+ // Gitea candidate-reconcile webhook (api/changes/candidate-reconcile.ts). Low
168
+ // volume (one per PR push, NOT per request) and the lifecycle PARTNER of
169
+ // `reconcile.terminal` — timelined so D7's reconcile-hang watchdog can correlate
170
+ // "received" against "terminal" for the same changeId and detect an ABSENCE.
171
+ actors: ["system"], source: "server", sinks: LIFECYCLE,
172
+ argsAllow: ["repo", "action", "branch", "changeId"], renderedAllow: [],
173
+ desc: "A Gitea reconcile webhook was received.",
174
+ },
175
+ "reconcile.terminal": {
176
+ // A reconcile reached a terminal state (reconciled|blocked|failed). Low
177
+ // volume, high value → timelined for the ops UI (D5).
178
+ actors: ["system"], source: "server", sinks: LIFECYCLE,
179
+ argsAllow: ["changeId", "result", "evidenceStatus"], renderedAllow: [],
180
+ desc: "A candidate reconcile reached a terminal state.",
181
+ },
182
+
183
+ // ── Deploy provenance (source: server or cli) — card D2 ────────────────────
184
+ "deploy.version_observed": {
185
+ actors: ["system", "dev"], source: "server", sinks: PROVENANCE,
186
+ argsAllow: ["version", "commit", "env", "observedFrom"], renderedAllow: [],
187
+ desc: "A deployed worker version/commit was observed (provenance).",
188
+ },
189
+
190
+ // ── ShipLoop change lifecycle (source: ui/server) ──────────────────────────
191
+ // 1:1 with the admin ship routes + CHANGE_STATES (change-record.ts). Low
192
+ // volume, high value, needs joins/point-queries → timelined. `reason` is
193
+ // operator-authored justification → goes through rendered (truncated +
194
+ // canary-scanned), never core.
195
+ "change.submit": {
196
+ actors: ["dev", "admin"], source: "server", sinks: LIFECYCLE,
197
+ argsAllow: ["changeId"], renderedAllow: ["title"],
198
+ desc: "A change/candidate was submitted for review.",
199
+ },
200
+ "change.accept": {
201
+ actors: ["admin"], source: "ui", sinks: LIFECYCLE,
202
+ argsAllow: ["changeId"], renderedAllow: [],
203
+ desc: "A change was accepted/approved.",
204
+ },
205
+ "change.reject": {
206
+ // Supersedes the interim admin slice's "candidate.reject" (recordActivity.ts).
207
+ actors: ["admin"], source: "ui", sinks: LIFECYCLE,
208
+ argsAllow: ["changeId"], renderedAllow: ["reason"],
209
+ desc: "A change was rejected (reason required).",
210
+ },
211
+ "change.ship": {
212
+ // D6: the admin/cockpit "Publish" (go-live) handler admits the store OWNER
213
+ // (merchant), ToT staff (admin), a ship-on-behalf developer (dev), AND the
214
+ // headless Bearer/CLI operator (agent) — the actor list reflects who actually
215
+ // reaches POST /api/changes/ship, not a placeholder subset.
216
+ actors: ["admin", "merchant", "dev", "agent"], source: "ui", sinks: LIFECYCLE,
217
+ argsAllow: ["changeId", "version"], renderedAllow: [],
218
+ desc: "A change was shipped to the live channel.",
219
+ },
220
+ "change.revert": {
221
+ // D9: same admission ladder as change.ship/rollback (resolveShipSession) — owner
222
+ // (merchant), staff (admin), ship-on-behalf dev, headless operator (agent). The
223
+ // `/api/changes/revert` route gates on the SAME seam, so the emitter cohort matches.
224
+ actors: ["admin", "merchant", "dev", "agent"], source: "ui", sinks: LIFECYCLE,
225
+ argsAllow: ["changeId"], renderedAllow: ["reason"],
226
+ desc: "A change was reverted.",
227
+ },
228
+ "change.hotfix": {
229
+ // D9: the owner-only emergency HOTFIX lane (/api/changes/hotfix) — a DISTINCT verb
230
+ // from change.ship (never a `--base main` flag): a bypass-normal-flow live promote
231
+ // from `main` while `preview` holds other unshipped work. Same resolveShipSession
232
+ // ladder as ship/rollback (owner ⊇ ship-on-behalf), so the same cohort set.
233
+ actors: ["admin", "merchant", "dev", "agent"], source: "ui", sinks: LIFECYCLE,
234
+ argsAllow: ["changeId"], renderedAllow: ["reason"],
235
+ desc: "An emergency hotfix was released to the live channel (bypassing unshipped preview work).",
236
+ },
237
+ "change.rollback": {
238
+ // D6: same admission ladder as change.ship (resolveShipSession) — owner
239
+ // (merchant), staff (admin), ship-on-behalf dev, headless operator (agent).
240
+ actors: ["admin", "merchant", "dev", "agent"], source: "ui", sinks: LIFECYCLE,
241
+ argsAllow: ["changeId", "toVersion"], renderedAllow: ["reason"],
242
+ desc: "The live channel was rolled back to a prior version.",
243
+ },
244
+ "change.integrate": {
245
+ // D6: accept/integrate (resolveShipSession) admits the same cohort set.
246
+ actors: ["admin", "merchant", "dev", "agent"], source: "ui", sinks: LIFECYCLE,
247
+ argsAllow: ["changeId"], renderedAllow: [],
248
+ desc: "A change was integrated into the shared preview.",
249
+ },
250
+ "change.transition": {
251
+ // Generic SDLC transition for states without a dedicated verb above
252
+ // (triage↔in_progress↔ready_for_review↔announced↔closed).
253
+ actors: ["admin", "dev", "system"], source: "server", sinks: LIFECYCLE,
254
+ argsAllow: ["changeId", "from", "to"], renderedAllow: ["reason"],
255
+ desc: "A ShipLoop change moved between SDLC states.",
256
+ },
257
+
258
+ // ── Content / customization publish (source: ui/server) ────────────────────
259
+ "content.publish": {
260
+ actors: ["admin", "merchant", "dev"], source: "server", sinks: LIFECYCLE,
261
+ argsAllow: ["versionId", "scope"], renderedAllow: [],
262
+ desc: "Tenant customization/content was published to a version.",
263
+ },
264
+ "content.checkout_style.saved": {
265
+ // NEW (D10): a tenant's checkout styling was saved to a versioned test artifact
266
+ // (Settings → Checkout styling → publishToTest spine). A persistent tenant
267
+ // customization — low volume, high value → timelined. Actor cohort mirrors the
268
+ // route's own auth (resolveOwnerSession): a merchant saving their own tenant, or
269
+ // a ToT-staff operator "operating as" another tenant via the admin picker.
270
+ actors: ["merchant", "admin"], source: "server", sinks: LIFECYCLE,
271
+ argsAllow: ["versionId", "scope"], renderedAllow: [],
272
+ desc: "A tenant's checkout styling was saved to a version.",
273
+ },
274
+ "change.go_live": {
275
+ // D9: renamed from the orphaned, un-namespaced `go_live` (the ONE key that broke the
276
+ // domain-prefix rule). The snake_case suffix is kept (the `deploy.version_observed`
277
+ // precedent) — only the missing `change.` domain prefix is added. Wired into
278
+ // /api/preview/promote (promote the current preview snapshot to live).
279
+ actors: ["admin", "merchant"], source: "ui", sinks: LIFECYCLE,
280
+ argsAllow: ["versionId"], renderedAllow: [],
281
+ desc: "A store's current preview was promoted to go-live.",
282
+ },
283
+
284
+ // ── Access / grants / connect (source: ui/server) ──────────────────────────
285
+ // Invitee identity is PII → DROPPED by allowlist; only the role/resource +
286
+ // an opaque hashed invitee ref (rendered) survive.
287
+ "grant.invite": {
288
+ // D6: the self-serve "Invite a Developer" handler admits an owner (merchant) or
289
+ // staff (admin) session, and the headless operator (agent) via the Bearer seam.
290
+ // D11: the SECOND invite path (grants/team-invite.ts) is session-authed by vendor
291
+ // MEMBERSHIP — a signed-in team member who is neither owner nor staff classifies as
292
+ // "dev" (see uiActor.classifyActorKind), so `dev` joins the admission set.
293
+ actors: ["admin", "merchant", "dev", "agent"], source: "ui", sinks: LIFECYCLE,
294
+ argsAllow: ["role", "resource"], renderedAllow: ["inviteeRef"],
295
+ desc: "A developer/team invite was issued.",
296
+ },
297
+ "grant.approve": {
298
+ // D6: the browserless CLI sign-in approval is taken by whoever is signed in
299
+ // (owner→merchant, staff→admin, or a signed-in developer→dev).
300
+ actors: ["admin", "merchant", "dev"], source: "ui", sinks: LIFECYCLE,
301
+ argsAllow: ["role", "resource"], renderedAllow: [],
302
+ desc: "A pending access request / CLI sign-in was approved.",
303
+ },
304
+ "grant.revoke": {
305
+ // D9: the self-serve "remove a developer from my team" handler (/api/grants/revoke),
306
+ // the inverse of grant.invite — same resolveOwnerSession seam, so the same cohort
307
+ // (owner→merchant, staff→admin, headless operator→agent). The revoked developer's
308
+ // identity is PII → DROPPED by the allowlist; only an opaque hashed ref survives.
309
+ actors: ["admin", "merchant", "agent"], source: "ui", sinks: LIFECYCLE,
310
+ argsAllow: ["role", "resource"], renderedAllow: ["principalRef"],
311
+ desc: "A developer was revoked from the owner's team.",
312
+ },
313
+ "preview.invite": {
314
+ // D10 wired this into POST /api/preview/invite (the DEV-4 developer-onboarding
315
+ // invite), a Bearer-operator (headless) route with no human session — so `agent`
316
+ // is added to the cohort, mirroring how grant.invite admits the headless
317
+ // Bearer seam. The route mints a signed repo-onboarding LINK (owner/repo/
318
+ // cloneUrl), not an email invite, so there is no invitee PII to hash.
319
+ actors: ["admin", "merchant", "agent"], source: "ui", sinks: LIFECYCLE,
320
+ argsAllow: ["changeId"], renderedAllow: ["inviteeRef"],
321
+ desc: "A reviewer/developer was invited to a preview.",
322
+ },
323
+ "preview.retire": {
324
+ actors: ["admin", "merchant"], source: "ui", sinks: LIFECYCLE,
325
+ argsAllow: ["changeId"], renderedAllow: [],
326
+ desc: "A preview environment was retired.",
327
+ },
328
+ "store.connect": {
329
+ actors: ["merchant", "admin"], source: "ui", sinks: LIFECYCLE,
330
+ argsAllow: ["provider"], renderedAllow: [],
331
+ desc: "A store/provider was connected.",
332
+ },
333
+
334
+ // ── App installs (source: server) — STOREFRONT_APPS_DB ─────────────────────
335
+ "app.install": {
336
+ actors: ["merchant", "admin"], source: "server", sinks: LIFECYCLE,
337
+ argsAllow: ["appId", "appVersion", "env"], renderedAllow: [],
338
+ desc: "A private app was installed.",
339
+ },
340
+ "app.uninstall": {
341
+ actors: ["merchant", "admin"], source: "server", sinks: LIFECYCLE,
342
+ argsAllow: ["appId", "env"], renderedAllow: [],
343
+ desc: "A private app was uninstalled.",
344
+ },
345
+ // ── App lifecycle mutations (source: server) — card D10 ─────────────────────
346
+ // Modeled on app.install/app.uninstall: the SAME `/api/apps/admin/*` operator
347
+ // cohort reaches these (a store OWNER managing their own app → merchant, ToT
348
+ // staff operating-as → admin), so the actor list mirrors those two, NOT a
349
+ // narrower placeholder. Security note (app.credential.rotated): the rotated
350
+ // secret NEVER enters the payload — the allowlist names only the non-sensitive
351
+ // install handle + rotation time (redaction would scrub a secret anyway, but the
352
+ // allowlist is deliberately minimal so there is nothing to scrub).
353
+ "app.credential.rotated": {
354
+ // The rotated secret NEVER enters the payload. We identify WHICH app rotated by
355
+ // its (appId, env) — NOT the installId, which is a UUID and would be scrubbed by
356
+ // the high-entropy canary anyway; appId/env survive redaction and are the same
357
+ // non-sensitive handle app.install/uninstall/suspend use.
358
+ actors: ["merchant", "admin"], source: "server", sinks: LIFECYCLE,
359
+ argsAllow: ["appId", "env", "rotatedAt"], renderedAllow: [],
360
+ desc: "A private app's client secret was rotated.",
361
+ },
362
+ "app.suspend": {
363
+ actors: ["merchant", "admin"], source: "server", sinks: LIFECYCLE,
364
+ argsAllow: ["appId", "env"], renderedAllow: [],
365
+ desc: "A private app install was suspended.",
366
+ },
367
+ "app.resume": {
368
+ actors: ["merchant", "admin"], source: "server", sinks: LIFECYCLE,
369
+ argsAllow: ["appId", "env"], renderedAllow: [],
370
+ desc: "A private app install was resumed to active.",
371
+ },
372
+ "app.update": {
373
+ actors: ["merchant", "admin"], source: "server", sinks: LIFECYCLE,
374
+ argsAllow: ["appId", "env"], renderedAllow: [],
375
+ desc: "A private app install was updated (version/scopes/manifest/webhook).",
376
+ },
377
+
378
+ // ── Agent cohort (source: server/cli) — later ──────────────────────────────
379
+ // Placeholder spine so the agent cohort (a later card) has stable keys to emit
380
+ // against without reopening D0. Args are deliberately empty until that card
381
+ // defines what an agent action is allowed to record.
382
+ "agent.action.invoked": {
383
+ actors: ["agent"], source: "server", sinks: HIGH_VOLUME,
384
+ argsAllow: ["tool"], renderedAllow: [],
385
+ desc: "An automated agent invoked an action.",
386
+ },
387
+ "agent.action.result": {
388
+ actors: ["agent"], source: "server", sinks: HIGH_VOLUME,
389
+ argsAllow: ["tool", "durationMs"], renderedAllow: [],
390
+ desc: "An automated agent action finished.",
391
+ },
392
+
393
+ // ── D11: remaining grant/staff-ops gaps + machine/agent-driven mutations ─────
394
+ // Follow-on to D0–D8: wires the invite/grant paths, staff-impersonation, cache,
395
+ // and machine-authed (signature/idempotency/app-JWT) mutations that D6 left out.
396
+ // Grant/staff actions are low-volume lifecycle → timelined; the machine cohort's
397
+ // high-volume streams (order-forward, attribution) are analytics-only.
398
+ "grant.issue": {
399
+ // grants/index.ts POST — the owner's team grant fan-out (one grantRepoAccess per
400
+ // member×repo). DISTINCT from `grant.invite` (which emails a developer to establish
401
+ // eligibility): this issues repo access to EXISTING principals. `resolveOwnerSession`
402
+ // admits owner (merchant), staff (admin), and the headless Bearer operator (agent).
403
+ // Principals are emails/usernames (PII) → NEVER stored; only counts + the owner.
404
+ actors: ["merchant", "admin", "agent"], source: "ui", sinks: LIFECYCLE,
405
+ argsAllow: ["resource", "granted", "failed"], renderedAllow: [],
406
+ desc: "A team was granted repo access (grant fan-out).",
407
+ },
408
+ "grant.provisioned": {
409
+ // grants/provisioned.ts — bulk re-grant of the owner's EXISTING team onto a newly
410
+ // provisioned repo. DISTINCT from `grant.approve` (a CLI sign-in approval): this is
411
+ // a merchant-driven re-grant onto a new resource. Same admission ladder as the
412
+ // grants surface (resolveOwnerSession → merchant primary, staff/headless too).
413
+ actors: ["merchant", "admin", "agent"], source: "ui", sinks: LIFECYCLE,
414
+ argsAllow: ["repo", "regranted", "failed"], renderedAllow: [],
415
+ desc: "An existing team was re-granted onto a newly provisioned repo.",
416
+ },
417
+ "dashboard.vendor_entered": {
418
+ // dashboard/enter-vendor.ts — a ToT-STAFF viewer enters/impersonates a vendor tenant.
419
+ // A classic audit event on the staff-impersonation boundary → timelined. Emitted
420
+ // ONLY after the staff gate passes, so the actor is always `admin` (staff); the
421
+ // staff email is PII and never stored (actor.id is its salted hash).
422
+ actors: ["admin"], source: "ui", sinks: LIFECYCLE,
423
+ argsAllow: ["resource", "capability"], renderedAllow: [],
424
+ desc: "A ToT-staff operator entered a vendor tenant (impersonation boundary).",
425
+ },
426
+ "cache.purged": {
427
+ // cache-purge.ts — bumps a tenant's edge page-cache version (the ETL load step calls
428
+ // it per tenant post-migration). Bearer-secret service call (no session) → emitted
429
+ // server-side as the machine caller (agent); an operator manual purge is the admin.
430
+ actors: ["admin", "agent"], source: "server", sinks: LIFECYCLE,
431
+ argsAllow: ["version"], renderedAllow: [],
432
+ desc: "A tenant's edge page-cache version was bumped (purge).",
433
+ },
434
+ "store.connect.completed": {
435
+ // domain/callback.ts — the CI completion callback for the www-domain job, the
436
+ // machine-side terminal PARTNER of D6's `store.connect`. Signature-authed, one per
437
+ // connect run (rare) → low-volume lifecycle terminal, timelined like reconcile.terminal.
438
+ actors: ["system"], source: "server", sinks: LIFECYCLE,
439
+ argsAllow: ["status", "state", "action"], renderedAllow: [],
440
+ desc: "A store apex-domain connect/rollback CI job reported completion.",
441
+ },
442
+ "app.order.forwarded": {
443
+ // apps/internal/order-forward.ts — an order forwarded to a private app (the
444
+ // tot-foxycart reconciliation unit, HMAC-signature-authed). HIGH-VOLUME (per order)
445
+ // → analytics only, NEVER timelined. Customer/order ids are excluded from the
446
+ // allowlist; only the coarse status/env/currency dimensions survive.
447
+ actors: ["agent", "system"], source: "server", sinks: HIGH_VOLUME,
448
+ argsAllow: ["status", "env", "currency"], renderedAllow: [],
449
+ desc: "An order was forwarded to a private app.",
450
+ },
451
+ "app.attribution.recorded": {
452
+ // apps/v1/attribution.ts — an installed private app (external bot, app-JWT authed,
453
+ // idempotency-keyed) records an attribution. HIGH-VOLUME → analytics only. The
454
+ // referralCode/customerHash are excluded; only the campaign dimension survives.
455
+ actors: ["agent"], source: "server", sinks: HIGH_VOLUME,
456
+ argsAllow: ["campaign"], renderedAllow: [],
457
+ desc: "A private app recorded an order attribution.",
458
+ },
459
+ "webhook.delivery.replayed": {
460
+ // apps/{admin,v1}/webhooks/deliveries/[deliveryId]/replay.ts — a webhook delivery was
461
+ // re-enqueued. TWO call sites, ONE key: the admin/ops console (session-authed →
462
+ // admin/merchant per resolveOperatorSession) and the app-facing v1 route (app-JWT →
463
+ // agent). A deliberate remediation action, low-volume → timelined for audit. The
464
+ // deliveryId is the opaque CloudEvents wire id (non-PII); `surface` disambiguates.
465
+ actors: ["admin", "merchant", "agent"], source: "server", sinks: LIFECYCLE,
466
+ argsAllow: ["deliveryId", "surface"], renderedAllow: [],
467
+ desc: "A webhook delivery was replayed (re-enqueued).",
468
+ },
469
+
470
+ // ── Meta (source: server) ──────────────────────────────────────────────────
471
+ "system.ingest.rejected": {
472
+ // The ingest endpoint (D4) rejected an event (unknown action, actor
473
+ // mismatch, canary tripped). Self-telemetry so a leak attempt is visible.
474
+ actors: ["system"], source: "server", sinks: HIGH_VOLUME,
475
+ argsAllow: ["reason", "attemptedAction"], renderedAllow: [],
476
+ desc: "An inbound activity event was rejected at ingest.",
477
+ },
478
+ } as const satisfies Record<string, ActionSpec>;
479
+
480
+ /** The allowlisted set of action keys. `action` on an ActivityEvent must be one. */
481
+ export type ActionKey = keyof typeof ACTION_CATALOG;
482
+
483
+ /** All action keys as a runtime array (for ingest validation, tests, docs). */
484
+ export const ACTION_KEYS = Object.keys(ACTION_CATALOG) as ActionKey[];
485
+
486
+ /** Type guard: is this string an allowlisted action key? Deny-by-default. */
487
+ export function isActionKey(v: unknown): v is ActionKey {
488
+ return typeof v === "string" && Object.prototype.hasOwnProperty.call(ACTION_CATALOG, v);
489
+ }
490
+
491
+ /** The spec for an action key. Returns undefined for an unknown key (caller
492
+ * treats undefined as "reject" — never as "allow everything"). */
493
+ export function getActionSpec(action: string): ActionSpec | undefined {
494
+ return isActionKey(action) ? ACTION_CATALOG[action] : undefined;
495
+ }
496
+
497
+ /** May this actor kind emit this action? Ingest (D4) uses this to reject spoofed
498
+ * cohorts (e.g. a `dev`-sourced token claiming an `admin` lifecycle action). */
499
+ export function isActorAllowed(action: ActionKey, actor: ActorKind): boolean {
500
+ return (ACTION_CATALOG[action].actors as readonly ActorKind[]).includes(actor);
501
+ }