@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
@@ -1,22 +1,20 @@
1
1
  /**
2
- * REAL orders view model (card 8585 / U3) replaces the deterministic mock as
3
- * the Orders tab's data source. Fetches the tenant's checkout orders via U2's
4
- * order client and maps the frozen DTO (`@tot/public-runtime` checkout-orders)
5
- * into the EXISTING `OrdersViewModel` shape, so `OrdersWorkspace.astro` renders
6
- * unchanged. Read-only (scope-locks) — no write paths.
2
+ * REAL orders view model the Orders tab's data source. Fetches the tenant's
3
+ * checkout orders via the order client and maps the frozen DTO
4
+ * (`@tot/public-runtime` checkout-orders) into the `OrdersViewModel` shape that
5
+ * `OrdersWorkspace.astro` renders. Read-only (scope-locks) no write paths.
7
6
  *
8
7
  * The mapping is a pure function (`mapOrdersToViewModel`) for testability; the
9
8
  * orchestration (`getOrdersViewModel`) lists orders, eagerly fetches each order's
10
9
  * detail (OrdersWorkspace renders `detailsById[row.id]` for every row), and maps.
11
- * On ANY upstream failure it returns a valid EMPTY view model so the tab renders
12
- * (rich loading/empty/error UX is U5). NOTE: the live "real qa data" path is
13
- * gated on ToT-core exposing `GET /api/exciseTax/orders` (see U1/U2 notes); until
14
- * then this yields the empty model on qa.
10
+ * On ANY upstream failure it returns a valid EMPTY view model so the tab renders.
11
+ * The live "real qa data" path is gated on ToT-core exposing
12
+ * `GET /api/exciseTax/orders`; until that exists this yields the empty model on qa.
15
13
  *
16
14
  * Identity/age fields (riskLevel, readiness "identity_and_age", evidence) are
17
- * derived provisionally from commerce facts here; U4 (the tot20 verification
18
- * join) fills them authoritatively via the DTO's null `verification` /
19
- * `verificationDetail` slots — no shape change needed.
15
+ * derived provisionally from commerce facts here; the tot20 verification join
16
+ * fills them authoritatively via the DTO's null `verification` /
17
+ * `verificationDetail` slots.
20
18
  */
21
19
  import type {
22
20
  CheckoutOrderDetail,
@@ -45,7 +43,7 @@ function deriveNextStep(fulfillment: OrderShipState, blocker: string | null): st
45
43
  return "Create label";
46
44
  }
47
45
 
48
- /** Provisional risk until U4 fills `verification`. Prefer a real riskLevel when present. */
46
+ /** Provisional risk until the verification join fills `verification`. Prefer a real riskLevel when present. */
49
47
  function deriveRisk(
50
48
  verification: CheckoutOrderVerification | null,
51
49
  blocker: string | null,
@@ -75,7 +73,7 @@ export function mapSummaryToRow(s: CheckoutOrderSummary): OrderRow {
75
73
  };
76
74
  }
77
75
 
78
- /** Commerce-derived readiness checks (U4 enriches the identity dimension). */
76
+ /** Commerce-derived readiness checks (the verification join enriches the identity dimension). */
79
77
  function deriveReadinessChecks(d: CheckoutOrderDetail): OrderReadinessCheck[] {
80
78
  const ageHold = d.blocker === "Age verification hold";
81
79
  return [
@@ -118,7 +116,7 @@ function deriveReadinessChecks(d: CheckoutOrderDetail): OrderReadinessCheck[] {
118
116
  ];
119
117
  }
120
118
 
121
- /** Base commerce evidence events (U4 replaces with the real verification timeline). */
119
+ /** Base commerce evidence events (the verification join replaces with the real verification timeline). */
122
120
  function deriveEvidenceTimeline(d: CheckoutOrderDetail): OrderEvidenceEvent[] {
123
121
  const events: OrderEvidenceEvent[] = [
124
122
  { title: "Order imported", detail: "Storefront order became available in the admin queue.", at: d.placedAt },
@@ -226,7 +224,7 @@ export interface GetOrdersViewModelOptions {
226
224
  * the Orders tab always renders.
227
225
  */
228
226
  /**
229
- * Load outcome for the Orders tab (U5) — lets the UI pick the right state:
227
+ * Load outcome for the Orders tab — lets the UI pick the right state:
230
228
  * `loaded` (rows), `empty` (fetch OK, 0 orders), `error` (upstream failed), and
231
229
  * `not_configured` (produced by the caller when no bearer/client is available —
232
230
  * the service itself never returns it). `page` carries the pagination cursor.
@@ -240,7 +238,7 @@ export interface OrdersLoadResult {
240
238
  }
241
239
 
242
240
  /**
243
- * Parse server-backed list filters from a request's query string (U5). `queue`
241
+ * Parse server-backed list filters from a request's query string. `queue`
244
242
  * of `"all"` (or absent) means no server filter. Non-numeric `limit` is ignored.
245
243
  * `payment`/search are NOT here — they are client-side refinements (the DTO list
246
244
  * API has no payment filter, per contract §9).
@@ -263,7 +261,7 @@ export function parseOrderFilters(params: URLSearchParams): CheckoutOrderListFil
263
261
  }
264
262
 
265
263
  /**
266
- * Fetch + map the orders view model AND report the load outcome (U5). Lists,
264
+ * Fetch + map the orders view model AND report the load outcome. Lists,
267
265
  * eagerly fetches per-order detail, maps. Distinguishes empty from error so the
268
266
  * tab can degrade gracefully. Never throws.
269
267
  */
@@ -292,7 +290,7 @@ export async function getOrdersViewModelResult(
292
290
  }
293
291
  }
294
292
 
295
- /** Back-compat convenience: just the view model (see getOrdersViewModelResult). */
293
+ /** Convenience wrapper: just the view model (see getOrdersViewModelResult). */
296
294
  export async function getOrdersViewModel(
297
295
  opts: GetOrdersViewModelOptions,
298
296
  ): Promise<OrdersViewModel> {
@@ -1,10 +1,8 @@
1
1
  /**
2
- * AI Workflows view model (baseline §AI Workflows) — the 8 named recurring
3
- * jobs as a control-plane record. No execution: this unit only defines the
4
- * read-oriented record shape (ai-workflows-scaffold/operational build the tab
5
- * + execution on top of it). `ApprovalMode`/`AuditEvent`/`AiRecommendation`
6
- * now come from the ai-evidence-substrate (`lib/admin/ai`), which formalizes
7
- * the approval/evidence models this view model originally stubbed.
2
+ * AI Workflows view model — the 8 named recurring jobs as a control-plane
3
+ * record. No execution: this defines only the read-oriented record shape; the
4
+ * tab + execution build on top of it. `ApprovalMode`/`AuditEvent`/`AiRecommendation`
5
+ * come from `lib/admin/ai`, which formalizes the approval/evidence models.
8
6
  */
9
7
 
10
8
  import type { ApprovalMode } from "../ai/approvalMode";
@@ -54,7 +54,7 @@ export interface SettingsViewModel {
54
54
  auditTrail: SettingsAuditEvent[];
55
55
  /**
56
56
  * The target tenant's current checkout styling — the editable descriptor the
57
- * "Checkout styling" section (U12) renders in its editor and saves back through
57
+ * "Checkout styling" section renders in its editor and saves back through
58
58
  * the versioned publish spine (`POST /api/admin/checkout-style` →
59
59
  * `publishToTest` on `tenants/<id>/checkout-style.json`). Always a complete
60
60
  * `CheckoutStyle` (the resolved default merged with any published override), so
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Composition root + shared helpers for the `/api/apps/admin/*` routes
3
- * (PrivateApps epic, D2 Chunk C) — builds an `AppRegistryService` over the
2
+ * Composition root + shared helpers for the `/api/apps/admin/*` routes
3
+ * builds an `AppRegistryService` over the
4
4
  * `STOREFRONT_APPS_DB` `Queryable` binding and a `D1CredentialStore`, the same
5
- * construction `api/apps/oauth/token.ts` (Chunk B) uses, and mirrors
5
+ * construction `api/apps/oauth/token.ts` uses, and mirrors
6
6
  * `../grants/route.ts`'s `getService()` + no-store `json()` + 503-when-
7
7
  * unconfigured pattern. Kept out of the pure service module so that stays
8
8
  * transport-free and unit-testable against a fake `Queryable`.
@@ -72,7 +72,7 @@ export function parseInstallId(body: unknown): string | null {
72
72
  * Resolve `installId` and verify it belongs to `tenantId` — every mutating
73
73
  * admin route (update/suspend/resume/uninstall/rotate) MUST call this BEFORE
74
74
  * invoking a service method, since those methods take a bare `installId` with
75
- * no tenant check of their own (Chunk A assumption #1). A foreign-tenant
75
+ * no tenant check of their own. A foreign-tenant
76
76
  * `installId` is reported IDENTICALLY to a truly-missing one — 404, same
77
77
  * body shape as `RegistryError("not_found")` — so there is no cross-tenant
78
78
  * existence oracle.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Operator-session resolution for the Private Apps admin API
3
- * (`/api/apps/admin/*`, PrivateApps epic, D2 Chunk C). Adapted from
3
+ * (`/api/apps/admin/*`). Mirrors
4
4
  * `../grants/session.ts`'s owner-session resolution — same two paths, same
5
5
  * fail-closed shape — but resolves `{ tenantId, actor }` instead of `{ owner }`:
6
6
  *
@@ -38,7 +38,7 @@
38
38
  * Routes depend only on the `{ ok, tenantId, actor }` result. `authorizeAdmin`
39
39
  * is the pure core of path 2 (unit-testable, no I/O).
40
40
  *
41
- * D8 ADDITION — console admin session. The two paths above are host-bound:
41
+ * Console admin session. The two paths above are host-bound:
42
42
  * they resolve exactly one tenant (`context.locals.tenant`). The
43
43
  * `/api/apps/admin/*` surface is also reachable from the multi-tenant vendor
44
44
  * console, where a signed-in viewer may hold memberships on several vendors —
@@ -61,6 +61,11 @@ import { sha256Hex } from "@tot/public-runtime";
61
61
  import { readEnv } from "@/lib/env";
62
62
  import { readViewerSession } from "@/lib/auth/route.js";
63
63
  import { viewerCanAccess, membershipFor } from "@/lib/dashboard/tenantSelection.js";
64
+ import {
65
+ authorizeBearerOperator,
66
+ resolveHostBoundSession,
67
+ type SessionFailure,
68
+ } from "@/lib/auth/operatorSession.js";
64
69
 
65
70
  /** Capabilities that make a signed-in viewer an app-registry operator for their tenant. */
66
71
  const ADMIN_CAPABILITIES: ReadonlySet<string> = new Set(["owner", "admin"]);
@@ -88,33 +93,30 @@ export interface AdminAuthInput {
88
93
 
89
94
  export type AdminAuthResult =
90
95
  | { ok: true; tenantId: string; actor: string; role?: string }
91
- | { ok: false; status: number; message: string };
96
+ | SessionFailure;
97
+
98
+ /** The one authorized-success shape both host-bound and Bearer admin paths return. */
99
+ type AdminAuthOk = Extract<AdminAuthResult, { ok: true }>;
92
100
 
93
101
  /**
94
102
  * Decide the authenticated tenant from env + request, with NO I/O —
95
103
  * unit-testable. Fail-closed: unconfigured gate → 503, bad/absent Bearer →
96
- * 401, no resolvable tenant → 400.
104
+ * 401, no resolvable tenant → 400. A thin adapter over the shared
105
+ * Bearer-operator core that maps its resolved `principal` to this surface's
106
+ * `tenantId` and stamps the fixed `BEARER_ACTOR` (no per-caller identity here).
97
107
  */
98
108
  export function authorizeAdmin(input: AdminAuthInput): AdminAuthResult {
99
- if (!input.gateSecret) {
100
- return {
101
- ok: false,
102
- status: 503,
103
- message: "apps admin operator secret not configured",
104
- };
105
- }
106
- if (input.authHeader !== `Bearer ${input.gateSecret}`) {
107
- return { ok: false, status: 401, message: "Unauthorized" };
108
- }
109
- const tenantId = (input.configuredTenant ?? input.requestedTenant ?? "").trim();
110
- if (!tenantId) {
111
- return {
112
- ok: false,
113
- status: 400,
114
- message: "tenant unresolved (set APPS_ADMIN_TENANT or X-Tot-Tenant)",
115
- };
116
- }
117
- return { ok: true, tenantId, actor: BEARER_ACTOR };
109
+ const res = authorizeBearerOperator({
110
+ authHeader: input.authHeader,
111
+ gateSecret: input.gateSecret,
112
+ configuredPrincipal: input.configuredTenant,
113
+ requestedPrincipal: input.requestedTenant,
114
+ unconfiguredMessage: "apps admin operator secret not configured",
115
+ unresolvedMessage: "tenant unresolved (set APPS_ADMIN_TENANT or X-Tot-Tenant)",
116
+ });
117
+ return res.ok
118
+ ? { ok: true, tenantId: res.principal, actor: BEARER_ACTOR }
119
+ : res;
118
120
  }
119
121
 
120
122
  /** One-way, stable pseudonym for an audit `actor` — never the email itself. */
@@ -128,30 +130,37 @@ async function actorFromEmail(email: string): Promise<string> {
128
130
  * from the body.
129
131
  */
130
132
  export async function resolveAdminSession(context: APIContext): Promise<AdminAuthResult> {
131
- // Path 1: a signed-in tenant-operator session. The tenant IS the resolved
132
- // host's appDomain — never from the request body.
133
- const viewer = context.locals?.viewer;
134
- const tenantId = context.locals?.tenant?.appDomain;
135
- if (viewer && tenantId) {
136
- if (!ADMIN_CAPABILITIES.has(viewer.capability)) {
137
- return {
133
+ return resolveHostBoundSession<AdminAuthOk>(context, {
134
+ capabilities: ADMIN_CAPABILITIES,
135
+ // A resolved-but-insufficient viewer fails closed with 403 and does NOT
136
+ // fall through to the Bearer gate — an authenticated-but-under-privileged
137
+ // caller must not be silently admitted by a secret it also presents.
138
+ onInsufficientCapability: {
139
+ mode: "failClosed",
140
+ failure: {
138
141
  ok: false,
139
142
  status: 403,
140
143
  message: "not authorized to manage apps for this tenant",
141
- };
142
- }
143
- return { ok: true, tenantId, actor: await actorFromEmail(viewer.email) };
144
- }
145
-
146
- // Path 2 (fallback): the headless-console Bearer operator secret. Dedicated
147
- // secret only — no fallback to another deployment's secret.
148
- const gateSecret = await readEnv("APPS_ADMIN_SECRET");
149
- const configuredTenant = await readEnv("APPS_ADMIN_TENANT");
150
- return authorizeAdmin({
151
- authHeader: context.request.headers.get("authorization"),
152
- gateSecret,
153
- configuredTenant,
154
- requestedTenant: context.request.headers.get("x-tot-tenant"),
144
+ },
145
+ },
146
+ // Path 1: the tenant IS the resolved host's appDomain — never the body.
147
+ resolveViewer: async (viewer, tenantId) => ({
148
+ ok: true,
149
+ tenantId,
150
+ actor: await actorFromEmail(viewer.email),
151
+ }),
152
+ // Path 2 (fallback): the headless-console Bearer operator secret. Dedicated
153
+ // secret only — no fallback to another deployment's secret.
154
+ resolveBearer: async (ctx) => {
155
+ const gateSecret = await readEnv("APPS_ADMIN_SECRET");
156
+ const configuredTenant = await readEnv("APPS_ADMIN_TENANT");
157
+ return authorizeAdmin({
158
+ authHeader: ctx.request.headers.get("authorization"),
159
+ gateSecret,
160
+ configuredTenant,
161
+ requestedTenant: ctx.request.headers.get("x-tot-tenant"),
162
+ });
163
+ },
155
164
  });
156
165
  }
157
166
 
@@ -1,7 +1,7 @@
1
1
  /**
2
- * The D3 scoped-API authorization boundary (PrivateApps epic) — every
2
+ * The scoped-API authorization boundary — every
3
3
  * `/api/apps/v1/*` route calls `authorizeAppRequest` FIRST. Verifies the
4
- * app-gateway bearer JWT (D2's `AppTokenVerifier`), resolves the calling
4
+ * app-gateway bearer JWT (`AppTokenVerifier`), resolves the calling
5
5
  * install from the registry, and enforces the single scope the route
6
6
  * requires per `docs/private-apps/contract/openapi.yaml`'s `x-requiredScope`.
7
7
  *
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Response helpers shared by every `/api/apps/v1/*` route (PrivateApps epic,
3
- * D3) — a no-store JSON responder (mirrors `../grants/route.ts` /
2
+ * Response helpers shared by every `/api/apps/v1/*` route a no-store JSON
3
+ * responder (mirrors `../grants/route.ts` /
4
4
  * `./adminService.ts`'s pattern) plus the `tot-request-id` / `traceparent`
5
5
  * response headers `docs/private-apps/contract/openapi.yaml` documents on
6
6
  * every operation.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Maps the internal `CatalogProduct` read model to the D3 contract's
2
+ * Maps the internal `CatalogProduct` read model to the scoped-API contract's
3
3
  * `Product` shape (`docs/private-apps/contract/openapi.yaml`, schema
4
4
  * `Product` — `additionalProperties: false`). Drops every internal field
5
5
  * the contract doesn't declare (variants, images, metafields, source, …) so
@@ -1,6 +1,6 @@
1
1
  /**
2
- * D1-backed `CredentialPort` implementation (PrivateApps epic, D2 Chunk B) —
3
- * issues/rotates/clears the OAuth2 client-credentials for an app install, and
2
+ * D1-backed `CredentialPort` implementation issues/rotates/clears the
3
+ * OAuth2 client-credentials for an app install, and
4
4
  * verifies a presented `(clientId, secret)` pair for the token endpoint
5
5
  * (`tokenIssuer.ts`).
6
6
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
- * RS256 signing-key loader for the Storefront App Gateway (PrivateApps epic,
3
- * D2 Chunk B) — the key that mints and verifies the OAuth2 client-credentials
4
- * JWTs a private app presents to the D3 scoped APIs.
2
+ * RS256 signing-key loader for the Storefront App Gateway the key that mints
3
+ * and verifies the OAuth2 client-credentials
4
+ * JWTs a private app presents to the scoped APIs.
5
5
  *
6
6
  * Mirrors `../auth/brokerAssertion.ts`'s `loadSigningKey` caching + `readEnv`
7
7
  * usage EXACTLY, but over a SEPARATE key (`JWT_APP_GATEWAY_PRIVATE_KEY_B64`):
@@ -34,7 +34,7 @@ export const DEFAULT_APP_GATEWAY_ISSUER = "storefront-app-gateway";
34
34
  export const APP_TOKEN_TTL_SECONDS = 300;
35
35
 
36
36
  /**
37
- * Widget launch-token lifetime (PrivateApps epic, D5 Chunk B) — shorter than
37
+ * Widget launch-token lifetime — shorter than
38
38
  * {@link APP_TOKEN_TTL_SECONDS}: this token only needs to survive the SSR
39
39
  * render → iframe-load round trip, never a standing API session.
40
40
  */
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Admin Health view aggregation — PrivateApps epic, D8 (Chunk A + D). Folds
2
+ * Admin Health view aggregation. Folds
3
3
  * one `AppInstall` (`registryService.ts`) and its `InstallHealth` rollup
4
- * (`../webhooks/deliveryStore.ts`'s `getInstallHealth`, now including
4
+ * (`../webhooks/deliveryStore.ts`'s `getInstallHealth`, including
5
5
  * `p95DeliveryMs`) into the single `AppInstallHealthView` shape the admin
6
6
  * console renders. Deliberately transport-free — no D1/fetch here, so it's
7
7
  * unit-testable with fakes; the caller (the admin health route) owns
@@ -11,9 +11,9 @@
11
11
  * a raw URL here, only as `hasWebhookEndpoint` presence — the console shows
12
12
  * "configured or not", not the app's receiver address.
13
13
  *
14
- * `runtimeMode` is hardcoded to `"external"` for V1: there is no per-install
15
- * runtime-mode column yet (every install today IS an externally-hosted app;
16
- * an in-platform runtime mode is a D9+ concern).
14
+ * `runtimeMode` is hardcoded to `"external"`: there is no per-install
15
+ * runtime-mode column (every install IS an externally-hosted app;
16
+ * an in-platform runtime mode is out of scope).
17
17
  */
18
18
  import { validateWebhookEndpoint } from "../webhooks/endpointGuard.js";
19
19
  import type { InstallHealth } from "../webhooks/deliveryStore.js";
@@ -1,6 +1,6 @@
1
1
  /**
2
- * App-owned attribution/commission write model PrivateApps epic, D6
3
- * (Chunk C). Owns the `app_attributions` rows `schema.ts` declares: the
2
+ * App-owned attribution/commission write model. Owns the
3
+ * `app_attributions` rows `schema.ts` declares: the
4
4
  * ledger `POST /attribution` (`docs/private-apps/contract/openapi.yaml`'s
5
5
  * `writeAttribution`) writes into. Writes only into the app's own
6
6
  * attribution namespace — this service never mutates `orders`, it only
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Opaque customer hash — PrivateApps epic, D6 (Chunk A). The `customerHash`
2
+ * Opaque customer hash — the `customerHash`
3
3
  * every order/attribution surface exposes instead of a raw customer id (see
4
4
  * `docs/private-apps/api-event-reference.md`'s PII-minimization matrix and
5
5
  * `contract/openapi.yaml`'s `Order.customerHash`).
@@ -1,18 +1,18 @@
1
1
  /**
2
- * FoxyOrderClient — the boundary to tot-foxycart's order-read surface
3
- * (PrivateApps epic, D6 Chunk A/B). `ordersStore.ts` and the read
4
- * routes / event receiver (later chunks) never call `fetch` directly; they go
5
- * through this seam so a fake can stand in for tot-foxycart in tests.
2
+ * FoxyOrderClient — the boundary to tot-foxycart's order-read surface.
3
+ * `ordersStore.ts` and the read routes / event receiver never call `fetch`
4
+ * directly; they go through this seam so a fake can stand in for tot-foxycart
5
+ * in tests.
6
6
  *
7
- * RECONCILED (Chunk B) against the real service —
8
- * `tot-foxycart/routes/foxyOrderApi.js` + `modules/integrations/foxyOrderMapper.node.js`:
7
+ * The service is `tot-foxycart/routes/foxyOrderApi.js` +
8
+ * `modules/integrations/foxyOrderMapper.node.js`:
9
9
  * a `GET {baseUrl}/commerce/orders?appDomain=&limit=&offset=&since=&until=&status=`
10
10
  * list endpoint returning `{ orders, limit, offset, total, hasMore }` (limit/offset,
11
- * NOT page/pageSize — chunk A guessed page/pageSize), and a
11
+ * NOT page/pageSize), and a
12
12
  * `GET {baseUrl}/commerce/orders/{externalOrderId}?appDomain=` detail endpoint
13
- * returning `{ order }` (an envelope, not the bare order — chunk A guessed bare),
13
+ * returning `{ order }` (an envelope, not the bare order),
14
14
  * both Bearer-authenticated with the tenant's Foxy partner API key. `FoxyOrder`
15
- * below now mirrors `buildOrderPayload()`'s actual normalized DTO — NOT the
15
+ * below mirrors `buildOrderPayload()`'s actual normalized DTO — NOT the
16
16
  * contract's minimized `Order` shape (`contract/openapi.yaml`); mapping one to
17
17
  * the other is `orderMapper.ts`'s job.
18
18
  *
@@ -20,8 +20,8 @@
20
20
  * factory shape and `../../webhooks/deliveryEngine.ts`'s bounded-timeout
21
21
  * `fetch` (AbortController, injectable `fetchImpl`) — but adds a settable
22
22
  * test seam (`setFoxyOrderClientForTesting`) since this client, unlike
23
- * `messagesClient.ts`, is a shared singleton multiple later chunks (read
24
- * routes, event receiver) will each independently resolve.
23
+ * `messagesClient.ts`, is a shared singleton that multiple callers (read
24
+ * routes, event receiver) each independently resolve.
25
25
  */
26
26
  import { readEnv } from "@/lib/env";
27
27
 
@@ -174,7 +174,7 @@ export class HttpFoxyOrderClient implements FoxyOrderClient {
174
174
  }
175
175
 
176
176
  // --- Test seam ---------------------------------------------------------------
177
- // Later chunks (read routes, event receiver) each resolve their own client
177
+ // Callers (read routes, event receiver) each resolve their own client
178
178
  // instance via `getFoxyOrderClient()`; a module-level override lets a single
179
179
  // `setFoxyOrderClientForTesting` call stand in for tot-foxycart across all of
180
180
  // them in one test, without threading a fake through every call site.
@@ -1,17 +1,17 @@
1
1
  /**
2
- * Idempotency-Key ledger — PrivateApps epic, D6 (Chunk C). Backs the
3
- * `idempotency_keys` table `schema.ts` declares: the FIRST real (not
2
+ * Idempotency-Key ledger. Backs the
3
+ * `idempotency_keys` table `schema.ts` declares: the one real (not
4
4
  * presence-only) idempotency ledger in this codebase, and the one
5
5
  * `POST /attribution` (`docs/private-apps/contract/openapi.yaml`'s
6
6
  * `writeAttribution`) requires — a reused key with a body hash that doesn't
7
7
  * match the one first stored is a 409, not a silent overwrite or a second
8
- * row. Contrast D4's replay endpoint (`webhooks/deliveries/{id}/replay.ts`),
8
+ * row. Contrast the webhook replay endpoint (`webhooks/deliveries/{id}/replay.ts`),
9
9
  * which only checks the header's presence because it has no request body to
10
- * hash and no table backing it yet.
10
+ * hash and no table backing it.
11
11
  *
12
- * Pure over the `Queryable` seam, same shape every other D6 store uses.
13
- * Route-agnostic by design: `attribution.ts` is the first caller, but any
14
- * future idempotent write route can reuse this unchanged.
12
+ * Pure over the `Queryable` seam, same shape every other orders store uses.
13
+ * Route-agnostic by design: `attribution.ts` is one caller, but any
14
+ * idempotent write route can reuse this.
15
15
  */
16
16
  import { sha256Hex } from "@tot/public-runtime";
17
17
  import type { Queryable } from "../../d1/catalog.js";
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Inbound order-forward receiver — PrivateApps epic, D6 (Chunk D). The
2
+ * Inbound order-forward receiver. The
3
3
  * transport-free core `pages/api/apps/internal/order-forward.ts` calls once
4
4
  * it has verified the HMAC signature: given one reconciled order from
5
5
  * tot-foxycart, upsert it into `ordersStore.ts` and decide whether that write
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Pure mapper: `FoxyOrderClient`'s `FoxyOrder` (tot-foxycart's normalized
3
- * transaction DTO) -> the contract's minimized `Order` shape (PrivateApps
4
- * epic, D6 Chunk B; `docs/private-apps/contract/openapi.yaml`'s `Order`).
3
+ * transaction DTO) -> the contract's minimized `Order` shape
4
+ * (`docs/private-apps/contract/openapi.yaml`'s `Order`).
5
5
  *
6
6
  * Drops everything the contract's `additionalProperties: false` Order
7
7
  * doesn't declare — no raw checkout payload, no signed-cart material, no tax
@@ -1,9 +1,9 @@
1
1
  /**
2
- * `orders` read-model store — PrivateApps epic, D6 (Chunk A). Owns the
3
- * `orders` rows `schema.ts` declares: the diffing surface a later chunk's
4
- * event emission reads (`upsertOrder`'s returned `previousStatus` is how it
2
+ * `orders` read-model store. Owns the
3
+ * `orders` rows `schema.ts` declares: the diffing surface the event emission
4
+ * reads (`upsertOrder`'s returned `previousStatus` is how it
5
5
  * detects a created→fulfilled transition without a second query) and the
6
- * lookup surface the attribution route (also later) uses for its cross-tenant
6
+ * lookup surface the attribution route uses for its cross-tenant
7
7
  * ownership check.
8
8
  *
9
9
  * Pure over the `Queryable` seam (see `../../d1/catalog.ts`) — same shape
@@ -54,8 +54,8 @@ export interface UpsertOrderInput {
54
54
 
55
55
  /**
56
56
  * `upsertOrder`'s return: the row's status BEFORE this write (`null` for a
57
- * brand-new order) alongside the row as it now stands — the diffing signal a
58
- * later chunk's event emission needs to tell "just created" apart from
57
+ * brand-new order) alongside the row as it now stands — the diffing signal the
58
+ * event emission needs to tell "just created" apart from
59
59
  * "created → fulfilled" apart from "no-op re-delivery of the same status",
60
60
  * without a second round trip.
61
61
  */
@@ -1,8 +1,8 @@
1
1
  /**
2
- * App registry service — PrivateApps epic, D2 (Chunk A). Owns the
2
+ * App registry service. Owns the
3
3
  * `app_installs` / `app_audit_log` rows described in `schema.ts` and
4
4
  * orchestrates credential lifecycle through the {@link CredentialPort} seam
5
- * (Chunk B implements it; Chunk A only depends on the interface, mirroring
5
+ * (the credential store implements it; this service depends only on the interface, mirroring
6
6
  * how `D1VersionStore` implements `@tot/private-controlplane`'s `VersionStore`
7
7
  * contract without either side importing the other's internals).
8
8
  *
@@ -131,7 +131,7 @@ export interface IssuedCredential {
131
131
 
132
132
  /**
133
133
  * `IssuedCredential` plus the one-time plaintext secret. `issue`/`rotate` mint
134
- * the secret, hash it for storage (Chunk B: `sha256Hex`), and hand the
134
+ * the secret, hash it for storage (`sha256Hex`), and hand the
135
135
  * plaintext back exactly once here — there is no "show secret again" path by
136
136
  * design, so a caller that discards this return has lost it for good (see
137
137
  * `AppRegistryService.install`'s `InstallResult`, which is why this seam
@@ -140,10 +140,10 @@ export interface IssuedCredential {
140
140
  export type IssuedCredentialSecret = IssuedCredential & { secret: string };
141
141
 
142
142
  /**
143
- * Minimal seam for the credential lifecycle a later chunk (D2 Chunk B)
143
+ * Minimal seam for the credential lifecycle the credential store
144
144
  * implements against `app_credentials`. `AppRegistryService` orchestrates
145
145
  * through this interface only — it never touches credential storage directly,
146
- * so Chunk A can ship and be tested (with a fake) without Chunk B existing.
146
+ * so it can be tested with a fake.
147
147
  */
148
148
  export interface CredentialPort {
149
149
  /** Issue a fresh client-credential set for a newly installed app. */
@@ -155,7 +155,7 @@ export interface CredentialPort {
155
155
  }
156
156
 
157
157
  /**
158
- * The 3 platform-owned widget slots (D5 Chunk A). Mirrors the `widgets[].placement`
158
+ * The 3 platform-owned widget slots. Mirrors the `widgets[].placement`
159
159
  * enum in `docs/private-apps/contract/tot-app.schema.json` — widgets render in a
160
160
  * platform-managed sandboxed iframe at one of these fixed slots, never as
161
161
  * merchant-pasted script at an app-chosen location.
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Scope validation — the compliance-floor gate for Storefront Private Apps
3
- * (PrivateApps epic, D2). See `docs/private-apps/feature-brief.md` §"The
2
+ * Scope validation — the compliance-floor gate for Storefront Private Apps.
3
+ * See `docs/private-apps/feature-brief.md` §"The
4
4
  * compliance floor" and the machine-readable contract:
5
5
  * - `docs/private-apps/contract/scopes.json` — the 8-scope V1 catalog.
6
6
  * - `docs/private-apps/contract/forbidden-scopes.json` — the 8 ids that map
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * OAuth2 client-credentials token issuer — the Storefront App Gateway's
3
- * `POST /oauth/token` grant (PrivateApps epic, D2 Chunk B). Mints the
4
- * short-lived RS256 JWT a private app presents as a bearer token to the D3
3
+ * `POST /oauth/token` grant. Mints the
4
+ * short-lived RS256 JWT a private app presents as a bearer token to the
5
5
  * scoped APIs.
6
6
  *
7
7
  * `tenant` and `env` are supplied by the CALLER (the route, from
@@ -97,8 +97,8 @@ export class AppTokenIssuer {
97
97
  tenant: install.tenantId,
98
98
  env: install.env,
99
99
  scopes: install.scopes,
100
- // Discriminates this bearer token from a widget launch token (D5 Chunk
101
- // B) so the two can never be replayed for each other even though both
100
+ // Discriminates this bearer token from a widget launch token so the two
101
+ // can never be replayed for each other even though both
102
102
  // are RS256 JWTs signed by the same gateway key — see tokenVerifier.ts.
103
103
  token_use: "access",
104
104
  })
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Offline verification of the Storefront App Gateway's own OAuth2
3
- * client-credentials JWT (PrivateApps epic, D2 Chunk B) — this is what D3
4
- * imports at the scoped-API boundary to authorize an app's request.
3
+ * client-credentials JWT — this is what the scoped-API boundary
4
+ * imports to authorize an app's request.
5
5
  *
6
6
  * Modeled directly on `../auth/identityToken.ts`'s `IdentityAssertionVerifier`
7
7
  * (same `jwtVerify` + JWKS-by-`kid` shape, `aud`/`iss` pinning, alg allowlist),
@@ -25,7 +25,7 @@ import {
25
25
  } from "./gatewayKeys.js";
26
26
  import { readEnv } from "@/lib/env";
27
27
 
28
- /** The verified app-token claims (D3's authorization input). */
28
+ /** The verified app-token claims (the scoped API's authorization input). */
29
29
  export interface AppTokenClaims {
30
30
  iss: string;
31
31
  sub: string; // install id
@@ -40,8 +40,8 @@ export interface AppTokenClaims {
40
40
 
41
41
  /** Normalize a jose JWT payload into typed {@link AppTokenClaims}, throwing on any missing claim. */
42
42
  function toAppTokenClaims(payload: Record<string, unknown>): AppTokenClaims {
43
- // The security-critical discriminator: a widget launch token (D5 Chunk B,
44
- // `token_use: "widget_launch"`) is signed by this SAME gateway key, so
43
+ // The security-critical discriminator: a widget launch token
44
+ // (`token_use: "widget_launch"`) is signed by this SAME gateway key, so
45
45
  // signature/aud/iss/exp checks alone can't tell it apart from a bearer
46
46
  // token. Requiring `token_use === "access"` here is what stops a launch
47
47
  // token — minted for a sandboxed iframe, short-TTL, no scopes checked —
@@ -128,7 +128,7 @@ export class AppTokenVerifier {
128
128
  /**
129
129
  * Get (or build) the verifier for one tenant, from the gateway's own
130
130
  * locally-derived JWKS. Returns `null` when the signing key isn't configured
131
- * — callers (D3 routes) fail closed with 503, mirroring `getIdentityVerifier`.
131
+ * — callers (the scoped-API routes) fail closed with 503, mirroring `getIdentityVerifier`.
132
132
  * Not cached across calls (unlike `identityToken.ts`'s remote-fetch verifier):
133
133
  * deriving the local JWKS is cheap (`gatewayKeys.ts` caches the export itself).
134
134
  */