@tokenoftrust/storefront-runner 2.0.1 → 2.0.3

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 (120) hide show
  1. package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +19 -1
  2. package/apps/storefront/dev-plugins/tenant-validate-on-save.mjs +78 -0
  3. package/apps/storefront/src/middleware/index.ts +17 -1
  4. package/package.json +1 -1
  5. package/scripts/lib/tenant-dirs.mjs +80 -0
  6. package/scripts/tenant/lint-embeds.mjs +180 -0
  7. package/scripts/tenant/materialize-tenant-content.mjs +533 -0
  8. package/scripts/tenant/validate.mjs +837 -0
  9. package/apps/storefront/scripts/build-v84-clean-clone-standalone.mjs +0 -68
  10. package/apps/storefront/scripts/deny-v84-clean-clone-standalone-capabilities.mjs +0 -111
  11. package/apps/storefront/scripts/deny-v84-dogfood-capabilities.mjs +0 -2
  12. package/apps/storefront/scripts/deny-v84-lifecycle-capabilities.mjs +0 -247
  13. package/apps/storefront/scripts/dogfood-v84-nonportable-routes.ts +0 -73
  14. package/apps/storefront/scripts/fixed-v84-native-install-producer.ts +0 -79
  15. package/apps/storefront/scripts/generated/v84-clean-clone-replay.bundle.mjs +0 -10982
  16. package/apps/storefront/scripts/import-v84-through-ws2.ts +0 -38
  17. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling-dependency.ts +0 -2
  18. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling.ts +0 -4
  19. package/apps/storefront/scripts/install-v84-native-through-ws1.ts +0 -185
  20. package/apps/storefront/scripts/prove-clone-isolation.ts +0 -506
  21. package/apps/storefront/scripts/replay-v84-clean-clone-pass.ts +0 -148
  22. package/apps/storefront/scripts/replay-v84-clean-clone-standalone.ts +0 -35
  23. package/apps/storefront/scripts/replay-v84-clean-clone.ts +0 -214
  24. package/apps/storefront/scripts/run-fixed-v84-native-install-producer.mjs +0 -18
  25. package/apps/storefront/scripts/run-v84-clone-isolation-proof.mjs +0 -18
  26. package/apps/storefront/scripts/run-v84-clone-lifecycle-proof.mjs +0 -18
  27. package/apps/storefront/scripts/run-v84-import-through-ws2-proof.mjs +0 -17
  28. package/apps/storefront/scripts/run-v84-nonportable-route-dogfood.mjs +0 -15
  29. package/apps/storefront/scripts/sanitize-v84-proof-environment.mjs +0 -21
  30. package/apps/storefront/scripts/v84CleanCloneGraphPin.ts +0 -42
  31. package/apps/storefront/scripts/v84CleanCloneSourceGraph.ts +0 -145
  32. package/apps/storefront/scripts/validate-v84-clone-lifecycle.ts +0 -18
  33. package/apps/storefront/src/components/admin/AdminAIWorkflowsTab.astro +0 -531
  34. package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +0 -878
  35. package/apps/storefront/src/components/admin/AdminBlogTab.astro +0 -1435
  36. package/apps/storefront/src/components/admin/AdminCheckoutSettingsPanel.astro +0 -226
  37. package/apps/storefront/src/components/admin/AdminCustomersTab.astro +0 -593
  38. package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +0 -465
  39. package/apps/storefront/src/components/admin/AdminIntegrationsTab.astro +0 -105
  40. package/apps/storefront/src/components/admin/AdminProductsTab.astro +0 -901
  41. package/apps/storefront/src/components/admin/AdminPublishTab.astro +0 -2978
  42. package/apps/storefront/src/components/admin/AdminReportingTab.astro +0 -435
  43. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +0 -782
  44. package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +0 -611
  45. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +0 -1223
  46. package/apps/storefront/src/lib/dev/cockpitStore.ts +0 -57
  47. package/apps/storefront/src/lib/publish/adminPublishTab.ts +0 -4625
  48. package/apps/storefront/src/lib/publish/apex-readiness.ts +0 -360
  49. package/apps/storefront/src/lib/publish/buildStream.ts +0 -269
  50. package/apps/storefront/src/lib/publish/collapsePersistence.ts +0 -72
  51. package/apps/storefront/src/lib/publish/dispatchHealth.ts +0 -267
  52. package/apps/storefront/src/lib/publish/domainState.ts +0 -360
  53. package/apps/storefront/src/lib/publish/lifecycleState.ts +0 -369
  54. package/apps/storefront/src/lib/publish/liveReleaseSummary.ts +0 -29
  55. package/apps/storefront/src/lib/publish/mergeDoctorOnFailure.ts +0 -78
  56. package/apps/storefront/src/lib/publish/mergeHealthBanner.ts +0 -62
  57. package/apps/storefront/src/lib/publish/mergeRowFindings.ts +0 -124
  58. package/apps/storefront/src/lib/publish/mergeSupportEscalation.ts +0 -75
  59. package/apps/storefront/src/lib/publish/panelPolling.ts +0 -78
  60. package/apps/storefront/src/lib/publish/publish-guard.ts +0 -88
  61. package/apps/storefront/src/lib/publish/publishFormat.ts +0 -165
  62. package/apps/storefront/src/lib/publish/publishHero.ts +0 -177
  63. package/apps/storefront/src/lib/publish/reviewRelease/changeState.ts +0 -66
  64. package/apps/storefront/src/lib/publish/reviewRelease/copy.ts +0 -141
  65. package/apps/storefront/src/lib/publish/reviewRelease/index.ts +0 -41
  66. package/apps/storefront/src/lib/publish/reviewRelease/permissions.ts +0 -47
  67. package/apps/storefront/src/lib/publish/reviewRelease/releaseState.ts +0 -42
  68. package/apps/storefront/src/lib/publish/reviewRelease/types.ts +0 -59
  69. package/apps/storefront/src/lib/publish/reviewRelease/viewModel.ts +0 -440
  70. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/actions.ts +0 -161
  71. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/controller.ts +0 -325
  72. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/render.ts +0 -241
  73. package/apps/storefront/src/lib/publish/shipWorkspace.ts +0 -439
  74. package/apps/storefront/src/lib/publish/sseRoute.ts +0 -119
  75. package/apps/storefront/src/lib/publish/stageBundleUpload.ts +0 -215
  76. package/apps/storefront/src/lib/publish/streamLogConsole.ts +0 -142
  77. package/apps/storefront/src/lib/publish/versionNumber.ts +0 -52
  78. package/apps/storefront/src/pages/admin/ops-timeline.astro +0 -420
  79. package/apps/storefront/src/pages/admin/review-release.astro +0 -265
  80. package/apps/storefront/src/pages/admin/select.astro +0 -96
  81. package/apps/storefront/src/pages/admin.astro +0 -568
  82. package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +0 -86
  83. package/apps/storefront/src/pages/api/apps/admin/gtm-migration-install.ts +0 -174
  84. package/apps/storefront/src/pages/api/apps/admin/gtm-migration.ts +0 -96
  85. package/apps/storefront/src/pages/api/apps/admin/health.ts +0 -44
  86. package/apps/storefront/src/pages/api/apps/admin/install.ts +0 -125
  87. package/apps/storefront/src/pages/api/apps/admin/integration-lifecycle.ts +0 -118
  88. package/apps/storefront/src/pages/api/apps/admin/integrations.ts +0 -89
  89. package/apps/storefront/src/pages/api/apps/admin/list.ts +0 -26
  90. package/apps/storefront/src/pages/api/apps/admin/platform-adapters.ts +0 -85
  91. package/apps/storefront/src/pages/api/apps/admin/resume.ts +0 -79
  92. package/apps/storefront/src/pages/api/apps/admin/suspend.ts +0 -80
  93. package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +0 -98
  94. package/apps/storefront/src/pages/api/apps/admin/update.ts +0 -126
  95. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +0 -70
  96. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +0 -51
  97. package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +0 -255
  98. package/apps/storefront/src/pages/api/apps/oauth/token.ts +0 -87
  99. package/apps/storefront/src/pages/api/apps/v1/attribution.ts +0 -162
  100. package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +0 -39
  101. package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +0 -49
  102. package/apps/storefront/src/pages/api/apps/v1/health.ts +0 -32
  103. package/apps/storefront/src/pages/api/apps/v1/inventory.ts +0 -59
  104. package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +0 -48
  105. package/apps/storefront/src/pages/api/apps/v1/orders.ts +0 -73
  106. package/apps/storefront/src/pages/api/apps/v1/reports.ts +0 -21
  107. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +0 -74
  108. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +0 -47
  109. package/apps/storefront/src/pages/api/cache-purge.ts +0 -55
  110. package/apps/storefront/src/pages/api/compliance/tamper.ts +0 -88
  111. package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +0 -154
  112. package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +0 -120
  113. package/apps/storefront/src/pages/api/integrations/v1/[operation].ts +0 -108
  114. package/apps/storefront/src/pages/api/publish/stage-bundle.ts +0 -70
  115. package/apps/storefront/src/pages/api/test/dev-session.ts +0 -133
  116. package/apps/storefront/src/pages/cockpit.astro +0 -1548
  117. package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +0 -119
  118. package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +0 -105
  119. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +0 -177
  120. package/apps/storefront/src/pages/dashboard/index.astro +0 -160
@@ -1,32 +0,0 @@
1
- /**
2
- * `GET /api/apps/v1/health` — the D4 install delivery/telemetry health read
3
- * (PrivateApps epic; `docs/private-apps/contract/openapi.yaml`'s
4
- * `getInstallHealth`). Requires no scope beyond a valid token — only ever the
5
- * caller's OWN install health (the `sub` claim `authorizeAppRequest` already
6
- * resolved), never another install's.
7
- *
8
- * `WebhookDeliveryStore.getInstallHealth`'s `InstallHealth` return already IS
9
- * the contract's `InstallHealth` shape 1:1, incl. `p95DeliveryMs` (a
10
- * never-attempted install reads as healthy/zeroed) — no mapper needed, unlike
11
- * `Delivery` below.
12
- */
13
- import type { APIContext } from "astro";
14
- import { authorizeAppRequest } from "@/lib/apps/apiAuth.js";
15
- import { json, withRequestId } from "@/lib/apps/apiRoute.js";
16
- import { WebhookDeliveryStore } from "@/lib/webhooks/deliveryStore.js";
17
- import { fromD1 } from "@/lib/d1/catalog.js";
18
- import { readD1 } from "@/lib/env";
19
-
20
- export const prerender = false;
21
-
22
- export async function GET(context: APIContext): Promise<Response> {
23
- const auth = await authorizeAppRequest(context, null);
24
- if (!auth.ok) return withRequestId(auth.response, context);
25
-
26
- const d1 = await readD1("STOREFRONT_APPS_DB");
27
- if (!d1) return withRequestId(json({ error: "webhook store not configured", code: "unconfigured" }, 503), context);
28
- const store = new WebhookDeliveryStore(fromD1(d1));
29
-
30
- const health = await store.getInstallHealth(auth.install.installId);
31
- return withRequestId(json(health), context);
32
- }
@@ -1,59 +0,0 @@
1
- /**
2
- * `GET /api/apps/v1/inventory` — the D3 inventory-availability read
3
- * (PrivateApps epic; `docs/private-apps/contract/openapi.yaml`'s
4
- * `listInventory`). Requires scope `inventory:read`.
5
- *
6
- * No new table: this projects `{sku, available, quantity?, policy?}` off the
7
- * variants embedded in the existing catalog read model
8
- * (`../../../lib/d1/catalog.js`'s `listAllActiveProducts`) — active products
9
- * only. A variant with no `sku` is skipped; the contract's `InventoryList`
10
- * item requires one, and there's nothing to key it by otherwise.
11
- */
12
- import type { APIContext } from "astro";
13
- import { authorizeAppRequest } from "@/lib/apps/apiAuth.js";
14
- import { json, withRequestId } from "@/lib/apps/apiRoute.js";
15
- import { listAllActiveProducts, fromD1 } from "@/lib/d1/catalog.js";
16
- import { readD1 } from "@/lib/env";
17
-
18
- export const prerender = false;
19
-
20
- interface InventoryItem {
21
- sku: string;
22
- available: boolean;
23
- quantity?: number;
24
- policy?: "deny" | "continue";
25
- }
26
-
27
- function toInventoryItem(policy: string | undefined, quantity: number | undefined, sku: string, available: boolean): InventoryItem {
28
- return {
29
- sku,
30
- available,
31
- quantity,
32
- policy: policy === "deny" || policy === "continue" ? policy : undefined,
33
- };
34
- }
35
-
36
- export async function GET(context: APIContext): Promise<Response> {
37
- const auth = await authorizeAppRequest(context, "inventory:read");
38
- if (!auth.ok) return withRequestId(auth.response, context);
39
-
40
- const d1 = await readD1("CATALOG_DB");
41
- if (!d1) return withRequestId(json({ error: "catalog not configured", code: "unconfigured" }, 503), context);
42
- const db = fromD1(d1);
43
-
44
- const skuFilter = new URL(context.request.url).searchParams.get("sku");
45
- const products = await listAllActiveProducts(db);
46
-
47
- const items: InventoryItem[] = [];
48
- for (const product of products) {
49
- for (const variant of product.variants) {
50
- if (!variant.sku) continue;
51
- if (skuFilter && variant.sku !== skuFilter) continue;
52
- items.push(
53
- toInventoryItem(variant.inventory.policy, variant.inventory.quantity, variant.sku, variant.inventory.available),
54
- );
55
- }
56
- }
57
-
58
- return withRequestId(json({ items }), context);
59
- }
@@ -1,48 +0,0 @@
1
- /**
2
- * `GET /api/apps/v1/orders/{id}` — the D6 minimal single-order read
3
- * (PrivateApps epic; `docs/private-apps/contract/openapi.yaml`'s `getOrder`).
4
- * Requires scope `orders:read:minimal`.
5
- *
6
- * `{id}` is tot-foxycart's `externalId` — the same id `listOrders` echoes
7
- * back as `Order.id`. There's no cross-tenant oracle here: the Foxy partner
8
- * API key is itself tenant-scoped, so a call can only ever resolve THIS
9
- * tenant's own orders — an unknown id and a different tenant's id both come
10
- * back `null` from `FoxyOrderClient.getOrder` and are reported identically
11
- * as 404.
12
- */
13
- import type { APIContext } from "astro";
14
- import { authorizeAppRequest } from "@/lib/apps/apiAuth.js";
15
- import { json, withRequestId } from "@/lib/apps/apiRoute.js";
16
- import { toContractOrder } from "@/lib/apps/orders/orderMapper.js";
17
- import { getFoxyOrderClient } from "@/lib/apps/orders/foxyOrderClient.js";
18
-
19
- export const prerender = false;
20
-
21
- export async function GET(context: APIContext): Promise<Response> {
22
- const auth = await authorizeAppRequest(context, "orders:read:minimal");
23
- if (!auth.ok) return withRequestId(auth.response, context);
24
-
25
- const id = context.params.id;
26
- if (!id) return withRequestId(json({ error: "missing order id", code: "bad_request" }, 400), context);
27
-
28
- let client;
29
- try {
30
- client = await getFoxyOrderClient();
31
- } catch {
32
- return withRequestId(json({ error: "orders not configured", code: "unconfigured" }, 503), context);
33
- }
34
-
35
- let foxyOrder;
36
- try {
37
- foxyOrder = await client.getOrder(auth.install.tenantId, id);
38
- } catch {
39
- return withRequestId(json({ error: "failed to read order", code: "upstream_error" }, 502), context);
40
- }
41
- if (!foxyOrder) return withRequestId(json({ error: "order not found", code: "not_found" }, 404), context);
42
-
43
- const order = await toContractOrder(foxyOrder, {
44
- tenantId: auth.install.tenantId,
45
- installId: auth.install.installId,
46
- });
47
- return withRequestId(json(order), context);
48
- }
@@ -1,73 +0,0 @@
1
- /**
2
- * `GET /api/apps/v1/orders` — the D6 minimal order list (PrivateApps epic;
3
- * `docs/private-apps/contract/openapi.yaml`'s `listOrders`). Requires scope
4
- * `orders:read:minimal`.
5
- *
6
- * Reads live from tot-foxycart via `FoxyOrderClient` (never a raw fetch) and
7
- * maps every row through `orderMapper.ts`'s `toContractOrder` — nothing
8
- * beyond the contract's minimized `Order` shape (customer as opaque hash, no
9
- * checkout/tax/signed-cart internals) ever leaves this route. Fail-closed:
10
- * an unconfigured Foxy client is a 503, any upstream read failure is a clean
11
- * 502 — neither leaks Foxy's own error detail.
12
- */
13
- import type { APIContext } from "astro";
14
- import { authorizeAppRequest } from "@/lib/apps/apiAuth.js";
15
- import { json, withRequestId } from "@/lib/apps/apiRoute.js";
16
- import { toContractOrder, type ContractOrderStatus } from "@/lib/apps/orders/orderMapper.js";
17
- import { getFoxyOrderClient } from "@/lib/apps/orders/foxyOrderClient.js";
18
-
19
- export const prerender = false;
20
-
21
- const DEFAULT_PAGE_SIZE = 24;
22
- const MAX_PAGE_SIZE = 100;
23
- const ORDER_STATUSES: readonly ContractOrderStatus[] = ["created", "fulfilled", "cancelled"];
24
-
25
- function parsePage(raw: string | null, fallback: number): number {
26
- const n = raw != null ? Number(raw) : NaN;
27
- return Number.isFinite(n) && n >= 1 ? Math.floor(n) : fallback;
28
- }
29
-
30
- function parseStatus(raw: string | null): ContractOrderStatus | undefined {
31
- return raw != null && (ORDER_STATUSES as readonly string[]).includes(raw) ? (raw as ContractOrderStatus) : undefined;
32
- }
33
-
34
- export async function GET(context: APIContext): Promise<Response> {
35
- const auth = await authorizeAppRequest(context, "orders:read:minimal");
36
- if (!auth.ok) return withRequestId(auth.response, context);
37
-
38
- const url = new URL(context.request.url);
39
- const page = parsePage(url.searchParams.get("page"), 1);
40
- const pageSize = Math.min(
41
- MAX_PAGE_SIZE,
42
- Math.max(1, parsePage(url.searchParams.get("pageSize"), DEFAULT_PAGE_SIZE)),
43
- );
44
- const statusFilter = parseStatus(url.searchParams.get("status"));
45
-
46
- let client;
47
- try {
48
- client = await getFoxyOrderClient();
49
- } catch {
50
- return withRequestId(json({ error: "orders not configured", code: "unconfigured" }, 503), context);
51
- }
52
-
53
- let foxyPage;
54
- try {
55
- foxyPage = await client.listOrders(auth.install.tenantId, {
56
- limit: pageSize,
57
- offset: (page - 1) * pageSize,
58
- });
59
- } catch {
60
- return withRequestId(json({ error: "failed to read orders", code: "upstream_error" }, 502), context);
61
- }
62
-
63
- const mapCtx = { tenantId: auth.install.tenantId, installId: auth.install.installId };
64
- let orders = await Promise.all(foxyPage.orders.map((order) => toContractOrder(order, mapCtx)));
65
- // tot-foxycart's own `status` query param speaks Foxy's status vocabulary, not
66
- // the contract's created|fulfilled|cancelled enum — there's no confirmed
67
- // mapping to push this filter upstream, so it's applied post-map here. This
68
- // can leave a page short of `pageSize`, and `total` still reflects Foxy's
69
- // UNFILTERED count — a known gap, flagged rather than built around.
70
- if (statusFilter) orders = orders.filter((order) => order.status === statusFilter);
71
-
72
- return withRequestId(json({ orders, page, pageSize, total: foxyPage.total }), context);
73
- }
@@ -1,21 +0,0 @@
1
- /**
2
- * `GET /api/apps/v1/reports` — the D3 report-snapshot list (PrivateApps
3
- * epic; `docs/private-apps/contract/openapi.yaml`'s `listReports`). Requires
4
- * scope `reports:read`.
5
- *
6
- * Real auth + scope enforcement, empty data: this milestone ships the
7
- * endpoint's contract shape (a valid `ReportList`) with no report snapshots
8
- * behind it yet — there is no report-generation pipeline to read from.
9
- */
10
- import type { APIContext } from "astro";
11
- import { authorizeAppRequest } from "@/lib/apps/apiAuth.js";
12
- import { json, withRequestId } from "@/lib/apps/apiRoute.js";
13
-
14
- export const prerender = false;
15
-
16
- export async function GET(context: APIContext): Promise<Response> {
17
- const auth = await authorizeAppRequest(context, "reports:read");
18
- if (!auth.ok) return withRequestId(auth.response, context);
19
-
20
- return withRequestId(json({ reports: [] }), context);
21
- }
@@ -1,74 +0,0 @@
1
- /**
2
- * `POST /api/apps/v1/webhooks/deliveries/{deliveryId}/replay` — the D4
3
- * install-owned replay (PrivateApps epic; `docs/private-apps/contract/
4
- * openapi.yaml`'s `replayWebhookDelivery`). Authorized by install ownership,
5
- * not a scope (`x-requiredScope: null`, `x-authorization: install-owns-
6
- * delivery`): an app may replay only a delivery it owns.
7
- *
8
- * Preserves the SAME `deliveryId` (the CloudEvents wire id) throughout — a
9
- * receiver that already acked a `delivered` delivery still dedupes a
10
- * replayed resend of it — via `deliveryStore.ts`'s `requeueForReplay`, which
11
- * resets the row (incl. out of the `delivered`/`dead-lettered` terminal
12
- * states) to `pending` with a fresh attempt budget so the dispatcher's
13
- * `attemptDelivery` makes a real HTTP attempt instead of hitting its
14
- * terminal-state no-op branch. See `requeueForReplay`'s own doc comment for
15
- * the full rationale.
16
- *
17
- * No `Idempotency-Key` ledger exists yet (no table backs it), so this only
18
- * validates the header's presence, per the contract's `required: true` — it
19
- * does not yet detect/409 a reused key with a mismatched body (there is no
20
- * request body on this operation to hash in the first place).
21
- */
22
- import type { APIContext } from "astro";
23
- import { authorizeAppRequest } from "@/lib/apps/apiAuth.js";
24
- import { json, withRequestId } from "@/lib/apps/apiRoute.js";
25
- import { WebhookDeliveryStore } from "@/lib/webhooks/deliveryStore.js";
26
- import { toContractDelivery } from "@/lib/webhooks/deliveryMapper.js";
27
- import { getDispatcher } from "@/lib/webhooks/getDispatcher.js";
28
- import { fromD1 } from "@/lib/d1/catalog.js";
29
- import { readD1 } from "@/lib/env";
30
- import { recordActivity } from "@/lib/activity/recordActivity";
31
-
32
- export const prerender = false;
33
-
34
- export async function POST(context: APIContext): Promise<Response> {
35
- const auth = await authorizeAppRequest(context, null);
36
- if (!auth.ok) return withRequestId(auth.response, context);
37
-
38
- if (!context.request.headers.get("idempotency-key")) {
39
- return withRequestId(json({ error: "Idempotency-Key header is required" }, 400), context);
40
- }
41
-
42
- const deliveryId = context.params.deliveryId;
43
- if (!deliveryId) {
44
- return withRequestId(json({ error: "deliveryId is required", code: "not_found" }, 404), context);
45
- }
46
-
47
- const d1 = await readD1("STOREFRONT_APPS_DB");
48
- if (!d1) return withRequestId(json({ error: "webhook store not configured", code: "unconfigured" }, 503), context);
49
- const store = new WebhookDeliveryStore(fromD1(d1));
50
-
51
- const delivery = await store.getDelivery(deliveryId);
52
- if (!delivery || delivery.installId !== auth.install.installId) {
53
- return withRequestId(json({ error: "Delivery not found.", code: "not_found" }, 404), context);
54
- }
55
-
56
- const dispatcher = await getDispatcher();
57
- if (dispatcher instanceof Response) return withRequestId(dispatcher, context);
58
-
59
- const requeued = await store.requeueForReplay(deliveryId);
60
- await dispatcher.enqueue({ deliveryId });
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
- });
73
- return withRequestId(json(toContractDelivery(requeued), 202), context);
74
- }
@@ -1,47 +0,0 @@
1
- /**
2
- * `GET /api/apps/v1/webhooks/deliveries` — the D4 install-owned delivery
3
- * list (PrivateApps epic; `docs/private-apps/contract/openapi.yaml`'s
4
- * `listWebhookDeliveries`). Authorized by install ownership, not a scope
5
- * (`x-requiredScope: null`, `x-authorization: install-owns-delivery`): any
6
- * installed app may list ITS OWN delivery records, never another install's —
7
- * `listDeliveriesByInstall` is scoped to `auth.install.installId` from the
8
- * JWT, never a caller-supplied id.
9
- *
10
- * Delivery rows never carry a payload body (see `deliveryStore.ts`'s header),
11
- * so there is nothing sensitive to redact in `toContractDelivery`'s output.
12
- */
13
- import type { APIContext } from "astro";
14
- import { authorizeAppRequest } from "@/lib/apps/apiAuth.js";
15
- import { json, withRequestId } from "@/lib/apps/apiRoute.js";
16
- import { WebhookDeliveryStore, type DeliveryState } from "@/lib/webhooks/deliveryStore.js";
17
- import { toContractDelivery } from "@/lib/webhooks/deliveryMapper.js";
18
- import { fromD1 } from "@/lib/d1/catalog.js";
19
- import { readD1 } from "@/lib/env";
20
-
21
- export const prerender = false;
22
-
23
- const DELIVERY_STATES: readonly DeliveryState[] = ["pending", "retrying", "delivered", "dead-lettered"];
24
-
25
- /** Absent = no filter (`undefined`); an unrecognized value is a 400, not a silent no-op. */
26
- function parseStateFilter(raw: string | null): DeliveryState | undefined | "invalid" {
27
- if (raw === null) return undefined;
28
- return (DELIVERY_STATES as readonly string[]).includes(raw) ? (raw as DeliveryState) : "invalid";
29
- }
30
-
31
- export async function GET(context: APIContext): Promise<Response> {
32
- const auth = await authorizeAppRequest(context, null);
33
- if (!auth.ok) return withRequestId(auth.response, context);
34
-
35
- const state = parseStateFilter(new URL(context.request.url).searchParams.get("state"));
36
- if (state === "invalid") {
37
- return withRequestId(json({ error: "invalid state filter", code: "invalid_state_filter" }, 400), context);
38
- }
39
-
40
- const d1 = await readD1("STOREFRONT_APPS_DB");
41
- if (!d1) return withRequestId(json({ error: "webhook store not configured", code: "unconfigured" }, 503), context);
42
- const store = new WebhookDeliveryStore(fromD1(d1));
43
-
44
- const deliveries = await store.listDeliveriesByInstall(auth.install.installId, { state });
45
-
46
- return withRequestId(json({ deliveries: deliveries.map(toContractDelivery) }), context);
47
- }
@@ -1,55 +0,0 @@
1
- /**
2
- * POST /api/cache-purge — bump a tenant's edge page-cache version.
3
- *
4
- * The ETL load step calls this per tenant after a (re)migration so freshly
5
- * migrated content is visible immediately, rather than waiting out the page
6
- * TTL. This closes the loop that HttpToTClient.purgeTenant() (catalog API cache)
7
- * only half-covered: that purges the data cache; this purges the rendered HTML.
8
- *
9
- * Auth: `Authorization: Bearer <CACHE_PURGE_SECRET>`. JSON bodies are exempt from
10
- * Astro's CSRF origin check, so the server-to-server ETL caller works.
11
- */
12
- import type { APIContext } from "astro";
13
- import { readEnv, readKv } from "@/lib/env";
14
- import { bumpCacheVersion } from "@/lib/edgeCache";
15
- import { recordActivity } from "@/lib/activity/recordActivity";
16
-
17
- export const prerender = false;
18
-
19
- export async function POST(context: APIContext): Promise<Response> {
20
- const secret = await readEnv("CACHE_PURGE_SECRET");
21
- const auth = context.request.headers.get("authorization");
22
- if (!secret || auth !== `Bearer ${secret}`) {
23
- return new Response("Unauthorized", { status: 401 });
24
- }
25
-
26
- const kv = await readKv("TENANT_CACHE");
27
- if (!kv) {
28
- return new Response("No cache store bound", { status: 503 });
29
- }
30
-
31
- let tenantId: string | undefined;
32
- try {
33
- tenantId = ((await context.request.json()) as { tenantId?: string })?.tenantId;
34
- } catch {
35
- /* fall through to 400 */
36
- }
37
- if (!tenantId) {
38
- return new Response("tenantId required", { status: 400 });
39
- }
40
-
41
- const version = await bumpCacheVersion(kv, tenantId);
42
- // D11: the caller is the Bearer-secret ETL load step (a machine, no session) — emit as
43
- // the `agent` cohort with an opaque handle. Best-effort; never affects the purge.
44
- recordActivity({
45
- action: "cache.purged",
46
- actor: { kind: "agent", id: "storefront" },
47
- source: "server",
48
- outcome: { status: "succeeded" },
49
- scope: { tenantId },
50
- payload: { args: { version } },
51
- });
52
- return new Response(JSON.stringify({ tenantId, version }), {
53
- headers: { "content-type": "application/json" },
54
- });
55
- }
@@ -1,88 +0,0 @@
1
- /**
2
- * POST /api/compliance/tamper — the server receiver for protected-surface tamper
3
- * beacons (D-R7, `integration-protected-tamper-recovery`).
4
- *
5
- * The browser sentinel restores safe state IN the document and posts a
6
- * `TamperEvidenceBeacon` here. This route validates it fail-closed, records the
7
- * durable evidence + operator alert, and — when the beacon is attributable to a
8
- * single active install — SUSPENDS that install. It is a DETECTIVE control: the
9
- * suspension is after-the-fact recovery, never prevention.
10
- *
11
- * TENANT SCOPE is server-owned: the install lookup and suspend are ALWAYS keyed on
12
- * `locals.tenant` (middleware-resolved from the request host), never on the
13
- * body-supplied `tenant` — a page cannot ask to suspend another tenant's app. The
14
- * beacon body carries no secret (the contract rejects one), and the response is a
15
- * write-path ack the emitter ignores, so a failed post never breaks the page.
16
- */
17
- import type { APIContext } from "astro";
18
- import { getRegistryService, json } from "@/lib/apps/adminService.js";
19
- import type { AppInstall } from "@/lib/apps/registryService.js";
20
- import { RegistryError } from "@/lib/apps/registryService.js";
21
- import { planTamperRecovery, type AttributableInstall } from "@tot/public-runtime";
22
-
23
- export const prerender = false;
24
-
25
- const SYSTEM_ACTOR = "system:tamper-sentinel";
26
-
27
- function toAttributable(install: AppInstall): AttributableInstall {
28
- return {
29
- installId: install.installId,
30
- appId: install.appId,
31
- appVersion: install.appVersion,
32
- status: install.status,
33
- };
34
- }
35
-
36
- export async function POST(context: APIContext): Promise<Response> {
37
- const tenantId = context.locals.tenant?.tenant_id;
38
- // No resolved tenant ⇒ nothing third-party could be scoped to; accept-and-drop so
39
- // the emitter never errors or retries.
40
- if (!tenantId) return json({ ok: true, accepted: false }, 202);
41
-
42
- let body: unknown;
43
- try {
44
- body = await context.request.json();
45
- } catch {
46
- return json({ ok: false, error: "invalid JSON body" }, 400);
47
- }
48
-
49
- // Resolve the tenant's installs (empty when the registry binding isn't configured
50
- // in this runtime — the beacon is still validated + recorded, just not actioned).
51
- const registry = await getRegistryService();
52
- const installs: AttributableInstall[] =
53
- registry instanceof Response
54
- ? []
55
- : (await registry.listByTenant(tenantId)).map(toAttributable);
56
-
57
- const plan = planTamperRecovery(body, installs, {
58
- recordId: crypto.randomUUID(),
59
- receivedAt: new Date().toISOString(),
60
- });
61
- if (!plan.ok) return json({ ok: false, error: "invalid tamper beacon" }, 400);
62
-
63
- const { record, suspend } = plan.plan;
64
- // Durable, secret-free evidence line for operations. The beacon itself is the record.
65
- // eslint-disable-next-line no-console
66
- console.warn(
67
- `[tamper] tenant=${tenantId} record=${record.recordId} surface=${record.beacon.surface} kind=${record.beacon.kind} recovered=${record.beacon.recovered} app=${record.beacon.attribution?.appId ?? "unattributable"}`,
68
- );
69
-
70
- let suspended = false;
71
- if (suspend && !(registry instanceof Response)) {
72
- try {
73
- await registry.suspend(suspend.installId, SYSTEM_ACTOR);
74
- suspended = true;
75
- } catch (err) {
76
- // An already-suspended/uninstalled install (invalid_state) is a benign
77
- // no-op under rapid re-mutation; anything else is swallowed so the write
78
- // path never errors the emitter. The audit row from a successful suspend is
79
- // the durable trail.
80
- if (!(err instanceof RegistryError)) {
81
- // eslint-disable-next-line no-console
82
- console.error(`[tamper] suspend failed for ${suspend.installId}`, err);
83
- }
84
- }
85
- }
86
-
87
- return json({ ok: true, accepted: true, recovered: record.beacon.recovered, suspended }, 202);
88
- }
@@ -1,154 +0,0 @@
1
- /**
2
- * POST /api/dashboard/enter-vendor { vendorAppDomain } — the ToT-STAFF admission
3
- * endpoint (#8425 staff access). Phase 2 of two-phase admission: on the staff
4
- * viewer explicitly selecting a searched vendor, tot20 writes the server-side
5
- * AUDIT row and returns the staffRoles-scoped capability; we then stamp a
6
- * short-lived {@link StaffSelection} on the session and admit the viewer to
7
- * `/dashboard/<appDomain>` scoped to that capability.
8
- *
9
- * AUTHORIZATION — two independent axes:
10
- * NEAR (this endpoint): a valid `tot_session` carrying ToT-STAFF scope
11
- * (`staff[]` non-empty). No session → 401; no staff scope → 403.
12
- * FAR (tot20): `/clients/staffVendorAccess`, operator-authed as storefront,
13
- * presenting a storefront-signed staff broker assertion in the
14
- * `X-ToT-Broker-Assertion` header (minted from the verified session email).
15
- * tot20 verifies it, re-checks staff scope from its `sub`, scopes the capability,
16
- * and writes the audit row — it is the authority. No plaintext email on the wire.
17
- *
18
- * The staff selection is a DISTINCT access path from `tenants[]`: we NEVER
19
- * synthesize a membership. The dashboard guard + login gate consult the marker
20
- * separately (see tenantSelection.staffAdmits / loginGate.sessionAdmitsHost).
21
- */
22
- import type { APIContext } from "astro";
23
- import { readViewerSession, getSessionMutationHandle } from "@/lib/auth/route";
24
- import { updateSession } from "@/lib/auth/session";
25
- import { isStaffSession, buildStaffSelection } from "@/lib/dashboard/staffAdmission";
26
- import {
27
- createStaffVendorAccessClient,
28
- staffAccessHttpStatus,
29
- StaffVendorAccessClientError,
30
- } from "@/lib/dashboard/staffVendorAccess";
31
- import { mintBrokerAssertion, BrokerAssertionError } from "@/lib/auth/brokerAssertion";
32
- import { resolveUiActor, recordUiActivity } from "@/lib/activity/uiActor";
33
-
34
- export const prerender = false;
35
-
36
- /** JSON response with no-store (staff access state must never be edge-cached). */
37
- function json(body: unknown, status = 200): Response {
38
- return new Response(JSON.stringify(body), {
39
- status,
40
- headers: { "content-type": "application/json", "cache-control": "no-store" },
41
- });
42
- }
43
-
44
- export async function POST(context: APIContext): Promise<Response> {
45
- const session = await readViewerSession(context);
46
- if (!session) return json({ error: "not signed in" }, 401);
47
- if (!isStaffSession(session)) {
48
- return json({ error: "staff access required", reason: "notStaff" }, 403);
49
- }
50
-
51
- let body: { vendorAppDomain?: unknown };
52
- try {
53
- body = (await context.request.json()) as typeof body;
54
- } catch {
55
- return json({ error: "invalid JSON body" }, 400);
56
- }
57
- const vendorAppDomain =
58
- typeof body.vendorAppDomain === "string" ? body.vendorAppDomain.trim() : "";
59
- if (!vendorAppDomain) return json({ error: "vendorAppDomain is required" }, 400);
60
-
61
- // D11: staff-impersonation audit event. Resolve the actor AFTER the staff gate above,
62
- // so the cohort is always `admin` (a ToT-staff session); the staff email is PII and is
63
- // never stored (actor.id is its salted hash). Reused at each emit exit point below.
64
- const activityActor = await resolveUiActor(context, {});
65
-
66
- let client;
67
- try {
68
- client = await createStaffVendorAccessClient();
69
- } catch (err) {
70
- if (err instanceof StaffVendorAccessClientError) {
71
- return json({ error: "staff-access integration not configured" }, 503);
72
- }
73
- throw err;
74
- }
75
-
76
- // #8453 `staff-invite-proof-via-broker`: prove the staff actor with a fresh
77
- // storefront-signed assertion minted from the VERIFIED session email. Fail closed
78
- // (503) if the signing key is unconfigured.
79
- let brokerAssertion: string;
80
- try {
81
- brokerAssertion = await mintBrokerAssertion(session.email);
82
- } catch (err) {
83
- if (err instanceof BrokerAssertionError) {
84
- return json({ error: "staff-proof signing not configured" }, 503);
85
- }
86
- throw err;
87
- }
88
-
89
- // FAR authority: tot20 verifies the assertion, derives the staff actor, scopes the
90
- // capability, and writes the audit row. We admit ONLY on its ok.
91
- const result = await client.grantStaffAccess(brokerAssertion, vendorAppDomain);
92
- if (!result.ok) {
93
- recordUiActivity(activityActor, {
94
- action: "dashboard.vendor_entered",
95
- outcome: { status: "refused", errorClass: result.reason ?? `http_${result.status}` },
96
- scope: { tenantId: vendorAppDomain },
97
- payload: { args: { resource: vendorAppDomain } },
98
- });
99
- return json(
100
- { error: result.message, reason: result.reason, code: result.code },
101
- staffAccessHttpStatus(result.reason, result.status),
102
- );
103
- }
104
-
105
- // Stamp the distinct staff-selection marker on the session (short-lived, scoped
106
- // to the returned capability). Never a tenants[] entry.
107
- const nowSeconds = Math.floor(Date.now() / 1000);
108
- const selection = buildStaffSelection({
109
- resource: vendorAppDomain,
110
- capability: result.capability,
111
- nowSeconds,
112
- });
113
- const handle = await getSessionMutationHandle(context);
114
- if (!handle) {
115
- // No SESSION KV / no cookie (e.g. plain `astro dev`) — the audit row is
116
- // written but we can't persist the marker. Fail closed so the viewer isn't
117
- // told they're admitted when the guard can't see it.
118
- recordUiActivity(activityActor, {
119
- action: "dashboard.vendor_entered",
120
- outcome: { status: "failed", errorClass: "session_store_unavailable" },
121
- scope: { tenantId: vendorAppDomain },
122
- payload: { args: { resource: vendorAppDomain, capability: result.capability } },
123
- });
124
- return json({ error: "session store unavailable; cannot persist admission" }, 503);
125
- }
126
- const updated = await updateSession(
127
- handle.store,
128
- handle.id,
129
- (rec) => ({ ...rec, staffSelection: selection }),
130
- nowSeconds,
131
- );
132
- if (!updated) {
133
- recordUiActivity(activityActor, {
134
- action: "dashboard.vendor_entered",
135
- outcome: { status: "failed", errorClass: "session_expired" },
136
- scope: { tenantId: vendorAppDomain },
137
- payload: { args: { resource: vendorAppDomain, capability: result.capability } },
138
- });
139
- return json({ error: "session expired; sign in again" }, 401);
140
- }
141
-
142
- recordUiActivity(activityActor, {
143
- action: "dashboard.vendor_entered",
144
- outcome: { status: "succeeded" },
145
- scope: { tenantId: vendorAppDomain },
146
- payload: { args: { resource: vendorAppDomain, capability: result.capability } },
147
- });
148
- return json({
149
- ok: true,
150
- appDomain: vendorAppDomain,
151
- capability: result.capability,
152
- redirect: `/dashboard/${encodeURIComponent(vendorAppDomain)}`,
153
- });
154
- }