@tokenoftrust/storefront-runner 1.4.0 → 1.4.2-rc.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 (193) hide show
  1. package/apps/storefront/astro.config.mjs +17 -1
  2. package/apps/storefront/integrations/materialize-guard.mjs +56 -0
  3. package/apps/storefront/migrations-apps/0001_woozy_lyja.sql +22 -0
  4. package/apps/storefront/migrations-apps/0002_good_switch.sql +26 -0
  5. package/apps/storefront/migrations-apps/0003_mute_marauders.sql +12 -0
  6. package/apps/storefront/migrations-apps/meta/0001_snapshot.json +990 -0
  7. package/apps/storefront/migrations-apps/meta/0002_snapshot.json +1160 -0
  8. package/apps/storefront/migrations-apps/meta/0003_snapshot.json +1235 -0
  9. package/apps/storefront/migrations-apps/meta/_journal.json +21 -0
  10. package/apps/storefront/package.json +0 -1
  11. package/apps/storefront/public/js/dashboard-team.js +38 -2
  12. package/apps/storefront/src/components/Seo.astro +65 -1
  13. package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +878 -0
  14. package/apps/storefront/src/components/admin/AdminBlogTab.astro +1432 -0
  15. package/apps/storefront/src/components/admin/AdminCustomersTab.astro +3 -7
  16. package/apps/storefront/src/components/admin/AdminPublishTab.astro +1756 -1382
  17. package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +1 -3
  18. package/apps/storefront/src/components/blog/BlogPaywall.astro +87 -0
  19. package/apps/storefront/src/components/blog/PostAccessBadge.astro +43 -0
  20. package/apps/storefront/src/components/blog/PostGrid.astro +57 -0
  21. package/apps/storefront/src/components/blog/ReactionBar.astro +214 -0
  22. package/apps/storefront/src/layouts/Layout.astro +48 -3
  23. package/apps/storefront/src/lib/activity/alerts.ts +426 -0
  24. package/apps/storefront/src/lib/activity/changeActorAttribution.ts +85 -0
  25. package/apps/storefront/src/lib/activity/deployVersion.ts +120 -0
  26. package/apps/storefront/src/lib/activity/ingest.ts +188 -0
  27. package/apps/storefront/src/lib/activity/ingestAuth.ts +105 -0
  28. package/apps/storefront/src/lib/activity/killSwitch.ts +80 -0
  29. package/apps/storefront/src/lib/activity/query.ts +403 -0
  30. package/apps/storefront/src/lib/activity/recordActivity.ts +101 -0
  31. package/apps/storefront/src/lib/activity/store.ts +120 -0
  32. package/apps/storefront/src/lib/activity/uiActor.ts +150 -0
  33. package/apps/storefront/src/lib/activity/workerCommit.ts +70 -0
  34. package/apps/storefront/src/lib/admin/adminShell.ts +290 -0
  35. package/apps/storefront/src/lib/admin/ai/approvalMode.ts +2 -3
  36. package/apps/storefront/src/lib/admin/ai/audit.ts +3 -5
  37. package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +7 -7
  38. package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +3 -3
  39. package/apps/storefront/src/lib/admin/orderClient.ts +3 -5
  40. package/apps/storefront/src/lib/admin/services/orders.ts +17 -19
  41. package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +4 -6
  42. package/apps/storefront/src/lib/admin/viewmodels/settings.ts +1 -1
  43. package/apps/storefront/src/lib/apps/adminService.ts +4 -4
  44. package/apps/storefront/src/lib/apps/adminSession.ts +53 -44
  45. package/apps/storefront/src/lib/apps/apiAuth.ts +2 -2
  46. package/apps/storefront/src/lib/apps/apiRoute.ts +2 -2
  47. package/apps/storefront/src/lib/apps/catalogMapper.ts +1 -1
  48. package/apps/storefront/src/lib/apps/credentials.ts +2 -2
  49. package/apps/storefront/src/lib/apps/gatewayKeys.ts +4 -4
  50. package/apps/storefront/src/lib/apps/healthAggregate.ts +5 -5
  51. package/apps/storefront/src/lib/apps/orders/attributionService.ts +2 -2
  52. package/apps/storefront/src/lib/apps/orders/customerHash.ts +1 -1
  53. package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +12 -12
  54. package/apps/storefront/src/lib/apps/orders/idempotency.ts +7 -7
  55. package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +1 -1
  56. package/apps/storefront/src/lib/apps/orders/orderMapper.ts +2 -2
  57. package/apps/storefront/src/lib/apps/orders/ordersStore.ts +6 -6
  58. package/apps/storefront/src/lib/apps/registryService.ts +6 -6
  59. package/apps/storefront/src/lib/apps/scopes.ts +2 -2
  60. package/apps/storefront/src/lib/apps/tokenIssuer.ts +4 -4
  61. package/apps/storefront/src/lib/apps/tokenVerifier.ts +6 -6
  62. package/apps/storefront/src/lib/apps/widgets/eligibility.ts +2 -2
  63. package/apps/storefront/src/lib/apps/widgets/frameProps.ts +3 -3
  64. package/apps/storefront/src/lib/apps/widgets/launchToken.ts +4 -4
  65. package/apps/storefront/src/lib/apps/widgets/placements.ts +2 -2
  66. package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +1 -1
  67. package/apps/storefront/src/lib/auth/adminEntry.ts +9 -9
  68. package/apps/storefront/src/lib/auth/identityToken.ts +6 -14
  69. package/apps/storefront/src/lib/auth/loginGate.ts +75 -13
  70. package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +6 -4
  71. package/apps/storefront/src/lib/auth/operatorSession.ts +113 -0
  72. package/apps/storefront/src/lib/auth/route.ts +44 -4
  73. package/apps/storefront/src/lib/auth/session.ts +7 -8
  74. package/apps/storefront/src/lib/auth/totAccessClient.ts +8 -18
  75. package/apps/storefront/src/lib/basePath.ts +9 -2
  76. package/apps/storefront/src/lib/blog/access.ts +29 -0
  77. package/apps/storefront/src/lib/blog/collection.ts +313 -0
  78. package/apps/storefront/src/lib/blog/markdown.ts +174 -0
  79. package/apps/storefront/src/lib/blog/provider.ts +5 -0
  80. package/apps/storefront/src/lib/blog/reactions.ts +87 -0
  81. package/apps/storefront/src/lib/blog/rss.ts +92 -0
  82. package/apps/storefront/src/lib/blog/teaser.ts +83 -0
  83. package/apps/storefront/src/lib/blog/types.ts +34 -0
  84. package/apps/storefront/src/lib/blog/viewCounts.ts +243 -0
  85. package/apps/storefront/src/lib/d1/catalog.ts +2 -2
  86. package/apps/storefront/src/lib/d1/schema-apps.ts +147 -14
  87. package/apps/storefront/src/lib/dev/activityIngestToken.ts +2 -4
  88. package/apps/storefront/src/lib/dev/activityStore.ts +3 -5
  89. package/apps/storefront/src/lib/dev/apiBase.ts +1 -4
  90. package/apps/storefront/src/lib/dev/cockpitStore.ts +1 -9
  91. package/apps/storefront/src/lib/dev/devDraft.ts +1 -1
  92. package/apps/storefront/src/lib/dev/previewStatus.ts +9 -21
  93. package/apps/storefront/src/lib/dev/rendezvousBroker.ts +9 -24
  94. package/apps/storefront/src/lib/edgeCache.ts +17 -0
  95. package/apps/storefront/src/lib/env.ts +51 -0
  96. package/apps/storefront/src/lib/http/fetchTransport.ts +63 -0
  97. package/apps/storefront/src/lib/i18n.ts +25 -0
  98. package/apps/storefront/src/lib/jsonld.ts +118 -0
  99. package/apps/storefront/src/lib/monitoring/manifest.ts +11 -11
  100. package/apps/storefront/src/lib/newsletter/confirmationEmail.ts +30 -0
  101. package/apps/storefront/src/lib/newsletter/consent.ts +23 -0
  102. package/apps/storefront/src/lib/newsletter/digest.ts +117 -0
  103. package/apps/storefront/src/lib/newsletter/resendClient.ts +79 -0
  104. package/apps/storefront/src/lib/newsletter/send.ts +265 -0
  105. package/apps/storefront/src/lib/newsletter/subscribers.ts +156 -0
  106. package/apps/storefront/src/lib/publish/adminPublishTab.ts +3519 -0
  107. package/apps/storefront/src/lib/publish/apex-readiness.ts +41 -18
  108. package/apps/storefront/src/lib/publish/dispatchHealth.ts +5 -7
  109. package/apps/storefront/src/lib/publish/domainState.ts +22 -13
  110. package/apps/storefront/src/lib/publish/panelPolling.ts +78 -0
  111. package/apps/storefront/src/lib/publish/shipWorkspace.ts +100 -42
  112. package/apps/storefront/src/lib/publish/stageBundleUpload.ts +215 -0
  113. package/apps/storefront/src/lib/rate/kvWindow.ts +53 -0
  114. package/apps/storefront/src/lib/search/index.ts +2 -2
  115. package/apps/storefront/src/lib/seo/alternates.ts +42 -0
  116. package/apps/storefront/src/lib/seo/meta.ts +65 -0
  117. package/apps/storefront/src/lib/social/golive.ts +212 -0
  118. package/apps/storefront/src/lib/social/notify.ts +32 -0
  119. package/apps/storefront/src/lib/social/tokenStore.ts +171 -0
  120. package/apps/storefront/src/lib/the-build/provider.ts +27 -0
  121. package/apps/storefront/src/lib/the-build/readingTime.ts +35 -0
  122. package/apps/storefront/src/lib/the-build/toc.ts +95 -0
  123. package/apps/storefront/src/lib/tot/ToTClient.ts +1 -1
  124. package/apps/storefront/src/lib/tot/d1Client.ts +2 -2
  125. package/apps/storefront/src/lib/tot/totClientInterface.ts +3 -4
  126. package/apps/storefront/src/lib/webhooks/deliveryStore.ts +16 -17
  127. package/apps/storefront/src/middleware/index.ts +130 -2
  128. package/apps/storefront/src/pages/admin/ops-timeline.astro +420 -0
  129. package/apps/storefront/src/pages/admin.astro +52 -1206
  130. package/apps/storefront/src/pages/api/activity.ts +137 -0
  131. package/apps/storefront/src/pages/api/admin/activity-alerts.ts +73 -0
  132. package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +27 -1
  133. package/apps/storefront/src/pages/api/apps/admin/install.ts +26 -1
  134. package/apps/storefront/src/pages/api/apps/admin/resume.ts +24 -1
  135. package/apps/storefront/src/pages/api/apps/admin/suspend.ts +24 -1
  136. package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +25 -1
  137. package/apps/storefront/src/pages/api/apps/admin/update.ts +24 -1
  138. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +11 -0
  139. package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +11 -0
  140. package/apps/storefront/src/pages/api/apps/v1/attribution.ts +20 -0
  141. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +12 -0
  142. package/apps/storefront/src/pages/api/blog/[collection]/[slug]/react.ts +104 -0
  143. package/apps/storefront/src/pages/api/cache-purge.ts +11 -0
  144. package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +30 -0
  145. package/apps/storefront/src/pages/api/newsletter/[collection]/subscribe.ts +146 -0
  146. package/apps/storefront/src/pages/api/newsletter/confirm.ts +75 -0
  147. package/apps/storefront/src/pages/api/newsletter/unsubscribe.ts +90 -0
  148. package/apps/storefront/src/pages/api/publish/stage-bundle.ts +70 -0
  149. package/apps/storefront/src/pages/api/rum/vitals.ts +21 -2
  150. package/apps/storefront/src/pages/auth/login.astro +5 -4
  151. package/apps/storefront/src/pages/blog/[slug].astro +96 -16
  152. package/apps/storefront/src/pages/blog/author/[author].astro +140 -0
  153. package/apps/storefront/src/pages/blog/category/[category].astro +67 -0
  154. package/apps/storefront/src/pages/blog/index.astro +2 -0
  155. package/apps/storefront/src/pages/blog/tag/[tag].astro +65 -0
  156. package/apps/storefront/src/pages/cockpit.astro +74 -12
  157. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +15 -11
  158. package/apps/storefront/src/pages/search.astro +1 -1
  159. package/apps/storefront/src/pages/sitemap.xml.ts +20 -0
  160. package/apps/storefront/src/pages/the-build/[slug].astro +129 -0
  161. package/apps/storefront/src/pages/the-build/index.astro +146 -0
  162. package/apps/storefront/src/pages/the-build/rss.xml.ts +33 -0
  163. package/apps/storefront/src/styles/admin.css +1080 -0
  164. package/apps/storefront/tsconfig.json +5 -0
  165. package/package.json +1 -1
  166. package/packages/public-runtime/package.json +1 -0
  167. package/packages/public-runtime/src/activity/README.md +146 -0
  168. package/packages/public-runtime/src/activity/catalog.ts +553 -0
  169. package/packages/public-runtime/src/activity/event.ts +168 -0
  170. package/packages/public-runtime/src/activity/index.ts +16 -0
  171. package/packages/public-runtime/src/activity/redaction.ts +263 -0
  172. package/packages/public-runtime/src/candidate-index.ts +13 -0
  173. package/packages/public-runtime/src/catalog-d1.ts +1 -1
  174. package/packages/public-runtime/src/customization-preview.ts +25 -2
  175. package/packages/public-runtime/src/customization-runtime.ts +125 -3
  176. package/packages/public-runtime/src/customization-versioning.ts +10 -0
  177. package/packages/public-runtime/src/index.ts +7 -0
  178. package/packages/public-runtime/src/static-bundle.ts +357 -0
  179. package/packages/public-runtime/src/tenant.ts +36 -8
  180. package/scripts/build/copy-tenant-assets.mjs +14 -4
  181. package/scripts/dev/ai-edit.mjs +1 -1
  182. package/scripts/dev/checkout-watch.mjs +2 -2
  183. package/scripts/dev/file-browser.mjs +2 -4
  184. package/scripts/dev/file-writer.mjs +1 -1
  185. package/scripts/dev/git-status.mjs +2 -2
  186. package/scripts/dev/locate-handler.mjs +3 -3
  187. package/scripts/dev/port-check.mjs +3 -3
  188. package/scripts/dev/publish.mjs +5 -9
  189. package/scripts/dev/shot.mjs +1 -1
  190. package/scripts/dev/unified-diff.mjs +8 -3
  191. package/scripts/tot-dev.mjs +6 -9
  192. package/apps/storefront/src/lib/webhooks/signing.ts +0 -29
  193. package/apps/storefront/src/lib/webhooks/webhookSigningKey.ts +0 -146
@@ -0,0 +1,137 @@
1
+ /**
2
+ * POST /api/activity — the ingest endpoint of the operational activity telemetry
3
+ * spine (card D4). Accepts one `ActivityEvent` (or a batch array) as JSON from
4
+ * the emitter cohorts — the CLI (D3) and the UI (D6), both posting the wire shape
5
+ * of `packages/public-runtime/src/activity/event.ts` — authenticates the caller,
6
+ * re-redacts server-side, and fans each accepted event out to the sink(s) its
7
+ * catalog entry declares. See DECISIONS.md §8 for the store decision.
8
+ *
9
+ * AUTH — HMAC-SHA256 service-auth (`src/lib/activity/ingestAuth.ts`). The caller
10
+ * presents the authenticated tenant in `x-tot-activity-tenant` and a hex
11
+ * signature over `<tenant>\n<rawBody>` in `x-tot-activity-signature`, keyed by
12
+ * the `ACTIVITY_INGEST_SECRET` Worker secret. Binding the tenant into the signed
13
+ * material is what makes the tenant scope trustworthy: the route writes every
14
+ * row under THIS verified tenant and discards any body-supplied `scope.tenantId`
15
+ * (multi-tenant isolation). An unset secret is a 503 (seam not configured — the
16
+ * same convention as the MCP webhook receivers), not an auth failure.
17
+ *
18
+ * DEFENSE IN DEPTH — every event is rebuilt through `createActivityEvent()`
19
+ * (`ingestEvents`), re-applying the default-deny allowlist + canary scan even
20
+ * though emitters already redacted. A value-canary trip is self-telemetried as
21
+ * `system.ingest.rejected` so a leak attempt is visible (D6/D7 alert on it).
22
+ *
23
+ * OFF THE CRITICAL PATH — auth + parse + validate + redact are cheap synchronous
24
+ * CPU; the sink WRITES (D1 + Analytics Engine) run past the response via
25
+ * `runAfterResponse` (Workers `waitUntil`). The response is a 202 with per-event
26
+ * accepted/rejected counts the instant validation is done — a slow D1 write never
27
+ * holds the emitter's request open. JSON bodies are exempt from Astro's CSRF
28
+ * origin check, so the server-to-server callers work.
29
+ */
30
+ import type { APIContext } from "astro";
31
+ import { readD1, readEnv, readAnalytics, runAfterResponse, type CfExecutionContextLike } from "@/lib/env";
32
+ import { fromD1 } from "@/lib/d1/catalog";
33
+ import { verifyIngestAuth } from "@/lib/activity/ingestAuth";
34
+ import { ingestEvents, type IngestRejection } from "@/lib/activity/ingest";
35
+ import { fanOutActivity, type AnalyticsEngineLike, type ActivitySinks } from "@/lib/activity/store";
36
+ import { isActivityDisabled } from "@/lib/activity/killSwitch";
37
+ import { createActivityEvent } from "@tot/public-runtime";
38
+
39
+ export const prerender = false;
40
+
41
+ /** Cap the batch so a single POST can't fan out unbounded work past the response. */
42
+ const MAX_BATCH = 200;
43
+
44
+ function json(body: unknown, status: number): Response {
45
+ return new Response(JSON.stringify(body), {
46
+ status,
47
+ // Write-path ack — never cacheable. Mirrors the house convention.
48
+ headers: { "content-type": "application/json", "cache-control": "no-store" },
49
+ });
50
+ }
51
+
52
+ export async function POST(context: APIContext): Promise<Response> {
53
+ // Kill switch (D8): telemetry off ⇒ accept-and-drop. Fail-open toward the EMITTER —
54
+ // a 202 (never a 4xx/5xx) so a CLI/UI poster never errors, retries, or is delayed by
55
+ // the switch; nothing is validated, stored, or fanned out.
56
+ if (isActivityDisabled()) {
57
+ return json({ ok: true, disabled: true, accepted: 0, rejected: [] }, 202);
58
+ }
59
+
60
+ const secret = await readEnv("ACTIVITY_INGEST_SECRET");
61
+ if (!secret) {
62
+ // Seam not configured (emitters don't post without the shared ingest secret).
63
+ return new Response("activity ingest not configured", { status: 503 });
64
+ }
65
+
66
+ // Read the RAW body once: it is both the auth material and the JSON payload.
67
+ const rawBody = await context.request.text();
68
+
69
+ const auth = await verifyIngestAuth(context.request.headers, rawBody, secret);
70
+ if (!auth.ok) return new Response(auth.reason, { status: auth.status });
71
+
72
+ let parsed: unknown;
73
+ try {
74
+ parsed = JSON.parse(rawBody);
75
+ } catch {
76
+ return json({ ok: false, reason: "invalid JSON body" }, 400);
77
+ }
78
+ const events = Array.isArray(parsed) ? parsed : [parsed];
79
+ if (events.length === 0) return json({ ok: true, accepted: 0, rejected: [] }, 202);
80
+ if (events.length > MAX_BATCH) {
81
+ return json({ ok: false, reason: `batch too large (max ${MAX_BATCH})` }, 413);
82
+ }
83
+
84
+ // Validate + re-redact under the AUTHENTICATED tenant (cheap, synchronous).
85
+ const { accepted, rejected } = ingestEvents(events, auth.tenantId);
86
+
87
+ // Resolve the sink bindings (absent in dev/until provisioned ⇒ that sink is
88
+ // skipped, never an error).
89
+ const d1 = await readD1("STOREFRONT_APPS_DB");
90
+ const analytics = (await readAnalytics("ACTIVITY_ANALYTICS")) as AnalyticsEngineLike | undefined;
91
+ const sinks: ActivitySinks = {
92
+ ...(d1 ? { db: fromD1(d1) } : {}),
93
+ ...(analytics ? { analytics } : {}),
94
+ ingestedAt: new Date().toISOString(),
95
+ };
96
+
97
+ // Self-telemetry: a value-canary trip during server-side redaction means an
98
+ // emitter leaked (or tried to leak) a secret/PII into the payload. Emit a
99
+ // `system.ingest.rejected` event (itself routed to logs+analytics) so D6/D7 can
100
+ // alert — the fact of the trip, the action, never the offending value. Rejected
101
+ // events get the same self-telemetry so a spoof/unknown-action attempt is
102
+ // visible. Built here (synchronously) but fanned out off-path below.
103
+ const selfTelemetry = [
104
+ ...accepted
105
+ .filter((a) => a.canaryTripped)
106
+ .map((a) =>
107
+ createActivityEvent({
108
+ action: "system.ingest.rejected",
109
+ actor: { kind: "system", id: "activity-ingest" },
110
+ source: "server",
111
+ outcome: { status: "refused", errorClass: "canary_tripped" },
112
+ scope: { tenantId: auth.tenantId },
113
+ payload: { args: { reason: "canary_tripped", attemptedAction: a.event.action } },
114
+ }).event,
115
+ ),
116
+ ...rejected.map((r: IngestRejection) =>
117
+ createActivityEvent({
118
+ action: "system.ingest.rejected",
119
+ actor: { kind: "system", id: "activity-ingest" },
120
+ source: "server",
121
+ outcome: { status: "refused", errorClass: r.reason },
122
+ scope: { tenantId: auth.tenantId },
123
+ payload: { args: { reason: r.reason, ...(r.attemptedAction ? { attemptedAction: r.attemptedAction } : {}) } },
124
+ }).event,
125
+ ),
126
+ ];
127
+
128
+ // Fan every accepted event + the self-telemetry out to its sinks, PAST the
129
+ // response. Best-effort per sink (never throws). The adapter's per-request
130
+ // ExecutionContext lives at `locals.runtime.ctx`; runAfterResponse floats the
131
+ // promise in dev/Node when it is absent.
132
+ const cfContext = (context.locals as { runtime?: { ctx?: CfExecutionContextLike } })?.runtime?.ctx ?? null;
133
+ const work = Promise.all([...accepted.map((a) => a.event), ...selfTelemetry].map((e) => fanOutActivity(e, sinks)));
134
+ runAfterResponse(work, cfContext);
135
+
136
+ return json({ ok: true, accepted: accepted.length, rejected }, 202);
137
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `GET /api/admin/activity-alerts` — the polled/on-demand surface for card D7's
3
+ * operational-activity alert conditions.
4
+ *
5
+ * On-demand by design (see `lib/activity/alerts.ts` header): D7 deliberately does
6
+ * NOT add a cron/scheduled runner (no established precedent for one in this repo).
7
+ * This route runs `checkActivityAlerts()` fresh on every call, so it doubles as:
8
+ * (a) the data source for the `/admin/ops-timeline` alerts banner (that page
9
+ * calls the same function directly, in-process — this route is for OTHER
10
+ * callers), and
11
+ * (b) a lightweight endpoint an external uptime/monitor tool can poll on an
12
+ * interval, turning "on-demand" into "effectively continuous" without this
13
+ * repo standing up its own scheduler.
14
+ *
15
+ * ACCESS CONTROL mirrors `/admin/ops-timeline` exactly (same owner/admin-of-tenant
16
+ * or ToT-staff gate via `resolveAdminEntry`/`resolveAdminTenantMode`) rather than a
17
+ * new service-secret seam — this repo's other machine-to-machine surfaces
18
+ * (candidate-reconcile, the activity ingest route) authenticate a SPECIFIC known
19
+ * caller (MCP, an emitter) via a shared secret; there is no such caller for
20
+ * "a monitor polls for alerts" yet, and inventing one is the "new notification
21
+ * infra" the card scopes OUT. An admin/staff session (the same one that already
22
+ * views ops-timeline) is the correct, already-available caller today.
23
+ *
24
+ * TENANT ISOLATION — scoped to `Astro.locals.tenant.tenant_id` exactly like
25
+ * ops-timeline; never a URL/query value.
26
+ */
27
+ import type { APIContext } from "astro";
28
+ import { readViewerSession } from "@/lib/auth/route";
29
+ import { resolveAdminEntry } from "@/lib/auth/adminEntry";
30
+ import { resolveAdminTenantMode } from "@/lib/adminTenantEnvelope";
31
+ import { readD1 } from "@/lib/env";
32
+ import { fromD1 } from "@/lib/d1/catalog";
33
+ import { checkActivityAlerts } from "@/lib/activity/alerts";
34
+
35
+ export const prerender = false;
36
+
37
+ const JSON_HEADERS = {
38
+ "content-type": "application/json; charset=utf-8",
39
+ "cache-control": "no-store",
40
+ "x-robots-tag": "noindex, nofollow",
41
+ };
42
+
43
+ function json(body: unknown, status = 200): Response {
44
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
45
+ }
46
+
47
+ export async function GET(context: APIContext): Promise<Response> {
48
+ const { tenant } = context.locals;
49
+ const nowSeconds = Math.floor(Date.now() / 1000);
50
+
51
+ const viewerSession = await readViewerSession(context);
52
+ if (!viewerSession) return json({ ok: false, reason: "sign_in_required" }, 401);
53
+
54
+ const adminEntry = resolveAdminEntry({
55
+ record: viewerSession,
56
+ hostResource: tenant.appDomain,
57
+ nowSeconds,
58
+ });
59
+ const { isStaff } = resolveAdminTenantMode({
60
+ session: viewerSession,
61
+ adminAppTenant: tenant.appDomain,
62
+ requestedTarget: null,
63
+ nowSeconds,
64
+ });
65
+ const authorized = adminEntry.isOwnerViewer || isStaff;
66
+ if (!authorized) return json({ ok: false, reason: "not_authorized" }, 403);
67
+
68
+ const d1 = await readD1("STOREFRONT_APPS_DB");
69
+ if (!d1) return json({ ok: true, dbAvailable: false, alerts: [] });
70
+
71
+ const alerts = await checkActivityAlerts(fromD1(d1), tenant.tenant_id);
72
+ return json({ ok: true, dbAvailable: true, alerts });
73
+ }
@@ -24,6 +24,8 @@ import {
24
24
  json,
25
25
  } from "@/lib/apps/adminService.js";
26
26
  import { RegistryError } from "@/lib/apps/registryService.js";
27
+ import { resolveUiActor } from "@/lib/activity/uiActor";
28
+ import { recordActivity } from "@/lib/activity/recordActivity";
27
29
 
28
30
  export const prerender = false;
29
31
 
@@ -50,11 +52,35 @@ export async function POST(context: APIContext): Promise<Response> {
50
52
  const owned = await requireOwnedInstall(registry, installId, auth.tenantId);
51
53
  if (owned instanceof Response) return owned;
52
54
 
55
+ // D10: acting cohort (merchant/admin), emitted source "server". SECURITY: the
56
+ // rotated secret (`issued.secret`) is NEVER placed in the payload — the allowlist
57
+ // names only the non-sensitive app handle (appId/env, from the owned install) +
58
+ // rotation time.
59
+ const activityActor = await resolveUiActor(context, { owner: auth.tenantId });
60
+
53
61
  try {
54
62
  const issued = await registry.rotateCredentials(installId, auth.actor);
63
+ recordActivity({
64
+ action: "app.credential.rotated",
65
+ actor: activityActor,
66
+ source: "server",
67
+ outcome: { status: "succeeded" },
68
+ scope: { tenantId: auth.tenantId },
69
+ payload: { args: { appId: owned.appId, env: owned.env, rotatedAt: new Date().toISOString() } },
70
+ });
55
71
  return json({ credential: { clientId: issued.clientId, secret: issued.secret } });
56
72
  } catch (err) {
57
- if (err instanceof RegistryError) return registryErrorResponse(err);
73
+ if (err instanceof RegistryError) {
74
+ recordActivity({
75
+ action: "app.credential.rotated",
76
+ actor: activityActor,
77
+ source: "server",
78
+ outcome: { status: "failed", errorClass: err.code },
79
+ scope: { tenantId: auth.tenantId },
80
+ payload: { args: { appId: owned.appId, env: owned.env } },
81
+ });
82
+ return registryErrorResponse(err);
83
+ }
58
84
  throw err;
59
85
  }
60
86
  }
@@ -13,6 +13,8 @@ import { resolveAdminSession } from "@/lib/apps/adminSession.js";
13
13
  import { getRegistryService, registryErrorResponse, json } from "@/lib/apps/adminService.js";
14
14
  import { RegistryError, type InstallInput } from "@/lib/apps/registryService.js";
15
15
  import { emitWebhookEvent } from "@/lib/webhooks/emit.js";
16
+ import { resolveUiActor } from "@/lib/activity/uiActor";
17
+ import { recordActivity } from "@/lib/activity/recordActivity";
16
18
 
17
19
  export const prerender = false;
18
20
 
@@ -75,6 +77,11 @@ export async function POST(context: APIContext): Promise<Response> {
75
77
  const registry = await getRegistryService();
76
78
  if (registry instanceof Response) return registry;
77
79
 
80
+ // D10: resolve the acting cohort ONCE (merchant/admin — a store owner managing
81
+ // their own app, or ToT staff operating-as). Emitted as source "server" (the
82
+ // app.* catalog source), best-effort, never alters control flow.
83
+ const activityActor = await resolveUiActor(context, { owner: auth.tenantId });
84
+
78
85
  try {
79
86
  const result = await registry.install(input, { tenantId: auth.tenantId, actor: auth.actor });
80
87
  try {
@@ -92,9 +99,27 @@ export async function POST(context: APIContext): Promise<Response> {
92
99
  }),
93
100
  );
94
101
  }
102
+ recordActivity({
103
+ action: "app.install",
104
+ actor: activityActor,
105
+ source: "server",
106
+ outcome: { status: "succeeded" },
107
+ scope: { tenantId: auth.tenantId },
108
+ payload: { args: { appId: input.appId, appVersion: input.appVersion, env: input.env } },
109
+ });
95
110
  return json(result, 201);
96
111
  } catch (err) {
97
- if (err instanceof RegistryError) return registryErrorResponse(err);
112
+ if (err instanceof RegistryError) {
113
+ recordActivity({
114
+ action: "app.install",
115
+ actor: activityActor,
116
+ source: "server",
117
+ outcome: { status: err.code === "scope_denied" ? "refused" : "failed", errorClass: err.code },
118
+ scope: { tenantId: auth.tenantId },
119
+ payload: { args: { appId: input.appId, appVersion: input.appVersion, env: input.env } },
120
+ });
121
+ return registryErrorResponse(err);
122
+ }
98
123
  throw err;
99
124
  }
100
125
  }
@@ -20,6 +20,8 @@ import {
20
20
  json,
21
21
  } from "@/lib/apps/adminService.js";
22
22
  import { RegistryError } from "@/lib/apps/registryService.js";
23
+ import { resolveUiActor } from "@/lib/activity/uiActor";
24
+ import { recordActivity } from "@/lib/activity/recordActivity";
23
25
 
24
26
  export const prerender = false;
25
27
 
@@ -46,11 +48,32 @@ export async function POST(context: APIContext): Promise<Response> {
46
48
  const owned = await requireOwnedInstall(registry, installId, auth.tenantId);
47
49
  if (owned instanceof Response) return owned;
48
50
 
51
+ // D10: acting cohort (merchant/admin), emitted source "server" per the app.* catalog.
52
+ const activityActor = await resolveUiActor(context, { owner: auth.tenantId });
53
+
49
54
  try {
50
55
  const install = await registry.resume(installId, auth.actor);
56
+ recordActivity({
57
+ action: "app.resume",
58
+ actor: activityActor,
59
+ source: "server",
60
+ outcome: { status: "succeeded" },
61
+ scope: { tenantId: auth.tenantId },
62
+ payload: { args: { appId: owned.appId, env: owned.env } },
63
+ });
51
64
  return json({ install });
52
65
  } catch (err) {
53
- if (err instanceof RegistryError) return registryErrorResponse(err);
66
+ if (err instanceof RegistryError) {
67
+ recordActivity({
68
+ action: "app.resume",
69
+ actor: activityActor,
70
+ source: "server",
71
+ outcome: { status: err.code === "invalid_state" ? "refused" : "failed", errorClass: err.code },
72
+ scope: { tenantId: auth.tenantId },
73
+ payload: { args: { appId: owned.appId, env: owned.env } },
74
+ });
75
+ return registryErrorResponse(err);
76
+ }
54
77
  throw err;
55
78
  }
56
79
  }
@@ -21,6 +21,8 @@ import {
21
21
  json,
22
22
  } from "@/lib/apps/adminService.js";
23
23
  import { RegistryError } from "@/lib/apps/registryService.js";
24
+ import { resolveUiActor } from "@/lib/activity/uiActor";
25
+ import { recordActivity } from "@/lib/activity/recordActivity";
24
26
 
25
27
  export const prerender = false;
26
28
 
@@ -47,11 +49,32 @@ export async function POST(context: APIContext): Promise<Response> {
47
49
  const owned = await requireOwnedInstall(registry, installId, auth.tenantId);
48
50
  if (owned instanceof Response) return owned;
49
51
 
52
+ // D10: acting cohort (merchant/admin), emitted source "server" per the app.* catalog.
53
+ const activityActor = await resolveUiActor(context, { owner: auth.tenantId });
54
+
50
55
  try {
51
56
  const install = await registry.suspend(installId, auth.actor);
57
+ recordActivity({
58
+ action: "app.suspend",
59
+ actor: activityActor,
60
+ source: "server",
61
+ outcome: { status: "succeeded" },
62
+ scope: { tenantId: auth.tenantId },
63
+ payload: { args: { appId: owned.appId, env: owned.env } },
64
+ });
52
65
  return json({ install });
53
66
  } catch (err) {
54
- if (err instanceof RegistryError) return registryErrorResponse(err);
67
+ if (err instanceof RegistryError) {
68
+ recordActivity({
69
+ action: "app.suspend",
70
+ actor: activityActor,
71
+ source: "server",
72
+ outcome: { status: err.code === "invalid_state" ? "refused" : "failed", errorClass: err.code },
73
+ scope: { tenantId: auth.tenantId },
74
+ payload: { args: { appId: owned.appId, env: owned.env } },
75
+ });
76
+ return registryErrorResponse(err);
77
+ }
55
78
  throw err;
56
79
  }
57
80
  }
@@ -22,6 +22,8 @@ import {
22
22
  } from "@/lib/apps/adminService.js";
23
23
  import { RegistryError } from "@/lib/apps/registryService.js";
24
24
  import { emitWebhookEvent } from "@/lib/webhooks/emit.js";
25
+ import { resolveUiActor } from "@/lib/activity/uiActor";
26
+ import { recordActivity } from "@/lib/activity/recordActivity";
25
27
 
26
28
  export const prerender = false;
27
29
 
@@ -48,6 +50,10 @@ export async function POST(context: APIContext): Promise<Response> {
48
50
  const owned = await requireOwnedInstall(registry, installId, auth.tenantId);
49
51
  if (owned instanceof Response) return owned;
50
52
 
53
+ // D10: acting cohort (merchant/admin), emitted source "server" per the app.*
54
+ // catalog. Best-effort, never alters control flow.
55
+ const activityActor = await resolveUiActor(context, { owner: auth.tenantId });
56
+
51
57
  try {
52
58
  const install = await registry.uninstall(installId, auth.actor);
53
59
  try {
@@ -66,9 +72,27 @@ export async function POST(context: APIContext): Promise<Response> {
66
72
  }),
67
73
  );
68
74
  }
75
+ recordActivity({
76
+ action: "app.uninstall",
77
+ actor: activityActor,
78
+ source: "server",
79
+ outcome: { status: "succeeded" },
80
+ scope: { tenantId: auth.tenantId },
81
+ payload: { args: { appId: owned.appId, env: owned.env } },
82
+ });
69
83
  return json({ install });
70
84
  } catch (err) {
71
- if (err instanceof RegistryError) return registryErrorResponse(err);
85
+ if (err instanceof RegistryError) {
86
+ recordActivity({
87
+ action: "app.uninstall",
88
+ actor: activityActor,
89
+ source: "server",
90
+ outcome: { status: "failed", errorClass: err.code },
91
+ scope: { tenantId: auth.tenantId },
92
+ payload: { args: { appId: owned.appId, env: owned.env } },
93
+ });
94
+ return registryErrorResponse(err);
95
+ }
72
96
  throw err;
73
97
  }
74
98
  }
@@ -21,6 +21,8 @@ import {
21
21
  json,
22
22
  } from "@/lib/apps/adminService.js";
23
23
  import { RegistryError, type AppInstallPatch } from "@/lib/apps/registryService.js";
24
+ import { resolveUiActor } from "@/lib/activity/uiActor";
25
+ import { recordActivity } from "@/lib/activity/recordActivity";
24
26
 
25
27
  export const prerender = false;
26
28
 
@@ -93,11 +95,32 @@ export async function POST(context: APIContext): Promise<Response> {
93
95
  const owned = await requireOwnedInstall(registry, installId, auth.tenantId);
94
96
  if (owned instanceof Response) return owned;
95
97
 
98
+ // D10: acting cohort (merchant/admin), emitted source "server" per the app.* catalog.
99
+ const activityActor = await resolveUiActor(context, { owner: auth.tenantId });
100
+
96
101
  try {
97
102
  const install = await registry.update(installId, patch, auth.actor);
103
+ recordActivity({
104
+ action: "app.update",
105
+ actor: activityActor,
106
+ source: "server",
107
+ outcome: { status: "succeeded" },
108
+ scope: { tenantId: auth.tenantId },
109
+ payload: { args: { appId: owned.appId, env: owned.env } },
110
+ });
98
111
  return json({ install });
99
112
  } catch (err) {
100
- if (err instanceof RegistryError) return registryErrorResponse(err);
113
+ if (err instanceof RegistryError) {
114
+ recordActivity({
115
+ action: "app.update",
116
+ actor: activityActor,
117
+ source: "server",
118
+ outcome: { status: err.code === "invalid_state" ? "refused" : "failed", errorClass: err.code },
119
+ scope: { tenantId: auth.tenantId },
120
+ payload: { args: { appId: owned.appId, env: owned.env } },
121
+ });
122
+ return registryErrorResponse(err);
123
+ }
101
124
  throw err;
102
125
  }
103
126
  }
@@ -27,6 +27,7 @@ import { WebhookDeliveryStore } from "@/lib/webhooks/deliveryStore.js";
27
27
  import { getDispatcher } from "@/lib/webhooks/getDispatcher.js";
28
28
  import { fromD1 } from "@/lib/d1/catalog.js";
29
29
  import { readD1 } from "@/lib/env";
30
+ import { resolveUiActor, recordUiActivity } from "@/lib/activity/uiActor";
30
31
 
31
32
  export const prerender = false;
32
33
 
@@ -55,5 +56,15 @@ export async function POST(context: APIContext): Promise<Response> {
55
56
  const requeued = await store.requeueForReplay(deliveryId);
56
57
  await dispatcher.enqueue({ deliveryId });
57
58
 
59
+ // D11: ops/support console replay. Resolve the acting cohort (admin/merchant per
60
+ // resolveOperatorSession) from the session; `surface: "admin"` disambiguates from the
61
+ // app-facing v1 call site that shares this key. deliveryId is the opaque wire id.
62
+ const activityActor = await resolveUiActor(context, {});
63
+ recordUiActivity(activityActor, {
64
+ action: "webhook.delivery.replayed",
65
+ outcome: { status: "succeeded" },
66
+ scope: { tenantId: auth.tenantId },
67
+ payload: { args: { deliveryId, surface: "admin" } },
68
+ });
58
69
  return json({ delivery: requeued }, 202);
59
70
  }
@@ -24,6 +24,7 @@ import {
24
24
  type OrderForwardInput,
25
25
  } from "@/lib/apps/orders/orderForwardReceiver.js";
26
26
  import type { OrderLineItem, OrderStatus } from "@/lib/apps/orders/ordersStore.js";
27
+ import { recordActivity } from "@/lib/activity/recordActivity";
27
28
 
28
29
  export const prerender = false;
29
30
 
@@ -168,5 +169,15 @@ export async function POST(context: APIContext): Promise<Response> {
168
169
  if (!d1) return json({ error: "orders store not configured" }, 503);
169
170
 
170
171
  const result = await receiveForwardedOrder(fromD1(d1), input);
172
+ // D11: HIGH-VOLUME machine action (the tot-foxycart forwarder, HMAC-authed) → analytics
173
+ // only, never timelined. Coarse dimensions only; customer/order ids never stored.
174
+ recordActivity({
175
+ action: "app.order.forwarded",
176
+ actor: { kind: "agent", id: "order-forwarder" },
177
+ source: "server",
178
+ outcome: { status: "succeeded" },
179
+ scope: { tenantId: input.tenantId },
180
+ payload: { args: { status: result.order.status, env: input.env, currency: input.currency } },
181
+ });
171
182
  return json({ ok: true, status: result.order.status, emitted: result.emitted }, 200);
172
183
  }
@@ -18,6 +18,7 @@ import { checkIdempotency, computeBodyHash, recordIdempotency } from "@/lib/apps
18
18
  import { AppAttributionService, AttributionError } from "@/lib/apps/orders/attributionService.js";
19
19
  import { fromD1 } from "@/lib/d1/catalog.js";
20
20
  import { readD1 } from "@/lib/env";
21
+ import { recordActivity } from "@/lib/activity/recordActivity";
21
22
 
22
23
  export const prerender = false;
23
24
 
@@ -130,6 +131,14 @@ export async function POST(context: APIContext): Promise<Response> {
130
131
  });
131
132
  } catch (err) {
132
133
  if (err instanceof AttributionError) {
134
+ recordActivity({
135
+ action: "app.attribution.recorded",
136
+ actor: { kind: "agent", id: auth.install.installId },
137
+ source: "server",
138
+ outcome: { status: "failed", errorClass: err.code },
139
+ scope: { tenantId: auth.install.tenantId },
140
+ ...(parsed.campaign ? { payload: { args: { campaign: parsed.campaign } } } : {}),
141
+ });
133
142
  const status =
134
143
  err.code === "attribution_forbidden" ? 403 : err.code === "attribution_conflict" ? 409 : 400;
135
144
  return withRequestId(json({ error: err.message, code: err.code }, status), context);
@@ -138,5 +147,16 @@ export async function POST(context: APIContext): Promise<Response> {
138
147
  }
139
148
 
140
149
  await recordIdempotency(db, auth.install.installId, idempotencyKey, bodyHash, record);
150
+ // D11: HIGH-VOLUME machine action (an installed private app / external bot, app-JWT
151
+ // authed) → analytics only, never timelined. installId is an opaque, per-install handle
152
+ // (non-PII) — a good stable actor id. referralCode/customerHash are never stored.
153
+ recordActivity({
154
+ action: "app.attribution.recorded",
155
+ actor: { kind: "agent", id: auth.install.installId },
156
+ source: "server",
157
+ outcome: { status: "succeeded" },
158
+ scope: { tenantId: auth.install.tenantId },
159
+ ...(parsed.campaign ? { payload: { args: { campaign: parsed.campaign } } } : {}),
160
+ });
141
161
  return withRequestId(json(record, 201), context);
142
162
  }
@@ -27,6 +27,7 @@ import { toContractDelivery } from "@/lib/webhooks/deliveryMapper.js";
27
27
  import { getDispatcher } from "@/lib/webhooks/getDispatcher.js";
28
28
  import { fromD1 } from "@/lib/d1/catalog.js";
29
29
  import { readD1 } from "@/lib/env";
30
+ import { recordActivity } from "@/lib/activity/recordActivity";
30
31
 
31
32
  export const prerender = false;
32
33
 
@@ -58,5 +59,16 @@ export async function POST(context: APIContext): Promise<Response> {
58
59
  const requeued = await store.requeueForReplay(deliveryId);
59
60
  await dispatcher.enqueue({ deliveryId });
60
61
 
62
+ // D11: the app-facing (install-owned, app-JWT authed) replay — the `agent` cohort. Same
63
+ // catalog key as the admin console call site; `surface: "v1"` disambiguates. installId
64
+ // is an opaque per-install handle; deliveryId is the opaque CloudEvents wire id.
65
+ recordActivity({
66
+ action: "webhook.delivery.replayed",
67
+ actor: { kind: "agent", id: auth.install.installId },
68
+ source: "server",
69
+ outcome: { status: "succeeded" },
70
+ scope: { tenantId: auth.install.tenantId },
71
+ payload: { args: { deliveryId, surface: "v1" } },
72
+ });
61
73
  return withRequestId(json(toContractDelivery(requeued), 202), context);
62
74
  }