@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,89 +0,0 @@
1
- /**
2
- * GET /api/apps/admin/integrations — the effective integration admin read model
3
- * (ext-admin-02): the tenant-scoped data behind ext-admin-01's `/admin#integrations`
4
- * IA. Combines each install's desired config, resolved effective plan, compiled CSP,
5
- * consent, assurance, candidate, and rollback availability into ONE safe view.
6
- *
7
- * Auth mirrors `list.ts` exactly: the `?tenant=` query param is only ever a CANDIDATE
8
- * into `resolveOperatorSession` — the tenant actually used is `auth.tenantId` from the
9
- * resolved session, never the raw param — so another tenant, or a vendor with no
10
- * membership, is denied (401/403) before any read model is assembled. Read-only: any
11
- * membership role admits. The pure {@link assembleIntegrationReadModel} is the second,
12
- * defense-in-depth gate: it re-asserts every materialized input belongs to
13
- * `auth.tenantId` and redacts secrets, so a mis-plumbed control plane cannot leak.
14
- *
15
- * The control-plane materialization of effective plans, candidate evidence, and
16
- * assurance records is resolved per install where present; an install with none yet
17
- * surfaces honestly as `configuring` (no effective authority, no rollback), never a
18
- * fabricated live state.
19
- */
20
- import type { APIContext } from "astro";
21
- import { resolveOperatorSession } from "@/lib/apps/adminSession.js";
22
- import { getRegistryService, json } from "@/lib/apps/adminService.js";
23
- import { parseEnvironment, type IntegrationEnvironment } from "@tot/public-runtime";
24
- import {
25
- assembleIntegrationReadModel,
26
- type IntegrationReadModelInputItem,
27
- } from "@/lib/apps/integrationReadModel.js";
28
- import { tenantLocalReadModelItems } from "@/lib/apps/tenantIntegrationMaterialization.js";
29
- import { resolveAdminTenant } from "@/config/adminTenantLookup";
30
-
31
- export const prerender = false;
32
-
33
- /** A merchant-legible purpose line for an install, never an internal adapter name. */
34
- function purposeFor(appId: string): string {
35
- return `Integration: ${appId}`;
36
- }
37
-
38
- export async function GET(context: APIContext): Promise<Response> {
39
- const url = new URL(context.request.url);
40
- const requestedTenant = url.searchParams.get("tenant");
41
- const auth = await resolveOperatorSession(context, requestedTenant);
42
- if (!auth.ok) return json({ error: auth.message }, auth.status);
43
-
44
- const registry = await getRegistryService();
45
- if (registry instanceof Response) return registry;
46
-
47
- const stage: IntegrationEnvironment = parseEnvironment(url.searchParams.get("stage")) ?? "staged";
48
-
49
- const installs = await registry.listByTenant(auth.tenantId);
50
- const items: IntegrationReadModelInputItem[] = installs
51
- .filter((install) => install.status !== "uninstalled")
52
- .map((install) => ({
53
- install: {
54
- installId: install.installId,
55
- appId: install.appId,
56
- appVersion: install.appVersion,
57
- tenantId: install.tenantId,
58
- status: install.status,
59
- },
60
- purpose: purposeFor(install.appId),
61
- // The desired config is the tenant's own input; when the manifest snapshot is not
62
- // a structured object it contributes nothing — never a fabricated value.
63
- desiredConfig: {},
64
- }));
65
-
66
- // ext-kernel-09: a tenant's OWN (tenant-local) declarations are materialized through
67
- // the interim bridge and appended with their REAL effective plan + compiled CSP, so a
68
- // declared integration surfaces effective state — not `configuring`. Resolved off the
69
- // auth-bound tenant config only (the tenant id is `auth.tenantId`, never the param).
70
- // Best-effort + additive: a resolver hiccup degrades to the managed-app view rather
71
- // than failing the whole read model closed.
72
- try {
73
- const tenantConfig = await resolveAdminTenant(auth.tenantId);
74
- if (tenantConfig && tenantConfig.tenant_id === auth.tenantId) {
75
- items.push(...tenantLocalReadModelItems(tenantConfig, stage));
76
- }
77
- } catch (err) {
78
- console.error(`[admin-integrations] tenant-local materialization skipped for ${auth.tenantId}: ${String(err)}`);
79
- }
80
-
81
- const result = assembleIntegrationReadModel({ tenantPublicId: auth.tenantId, stage, items });
82
- if (!result.ok) {
83
- // A cross-tenant input or surviving secret is a server-side integrity failure, not
84
- // something to echo back field-by-field.
85
- return json({ error: "integration read model failed integrity checks" }, 500);
86
- }
87
-
88
- return json({ readModel: result.value });
89
- }
@@ -1,26 +0,0 @@
1
- /**
2
- * GET /api/apps/admin/list — list the app installs for the resolved operator
3
- * tenant (PrivateApps epic, D2 Chunk C; D8 console-session auth added). The
4
- * `?tenant=` query param (read by console callers with several memberships,
5
- * mirroring `../../grants/team-members.ts`) is only ever a CANDIDATE into
6
- * `resolveOperatorSession` — the tenant actually used below is `auth.tenantId`
7
- * from the resolved session, never the raw param, so a caller can only ever
8
- * see their own tenant's installs. Read-only: any membership role admits.
9
- */
10
- import type { APIContext } from "astro";
11
- import { resolveOperatorSession } from "@/lib/apps/adminSession.js";
12
- import { getRegistryService, json } from "@/lib/apps/adminService.js";
13
-
14
- export const prerender = false;
15
-
16
- export async function GET(context: APIContext): Promise<Response> {
17
- const requestedTenant = new URL(context.request.url).searchParams.get("tenant");
18
- const auth = await resolveOperatorSession(context, requestedTenant);
19
- if (!auth.ok) return json({ error: auth.message }, auth.status);
20
-
21
- const registry = await getRegistryService();
22
- if (registry instanceof Response) return registry;
23
-
24
- const installs = await registry.listByTenant(auth.tenantId);
25
- return json({ installs });
26
- }
@@ -1,85 +0,0 @@
1
- /**
2
- * POST /api/apps/admin/platform-adapters — the first-class native adapter
3
- * surface (ext-admin-04). Two actions over the pure, edge-safe adapter core
4
- * ({@link platformAdapterCatalog} / {@link buildPlatformAdapterDeclaration}):
5
- *
6
- * - `catalog` — the first-class adapter cards (consent purposes + performance
7
- * budget + CSP origins + per-stage destination behavior), so a merchant sees the
8
- * material effects before configuring anything.
9
- * - `build` — turn a merchant's public destination ids (per environment) into a
10
- * tenant-scoped declaration + resolved effective plan + read-model card, or
11
- * refuse. Fail-closed: an unknown adapter, a missing/invalid live id, or a
12
- * non-live stage that reuses the live id blocks with violations.
13
- *
14
- * Auth mirrors the other `/api/apps/admin/*` routes: the tenant is `auth.tenantId`
15
- * from the resolved operator session — NEVER the request body — so a build is only
16
- * ever scoped to the caller's own tenant. The build environment is derived server-side
17
- * as a `candidate` stage, so the browser can never forge a live-stage declaration.
18
- * Configuring is a manage action ({@link requireManageRole}). The route does NOT
19
- * persist — staging/publishing a produced candidate is ext-admin-07's lifecycle.
20
- */
21
- import type { APIContext } from "astro";
22
- import { resolveOperatorSession } from "@/lib/apps/adminSession.js";
23
- import { json, requireManageRole } from "@/lib/apps/adminService.js";
24
- import {
25
- platformAdapterCatalog,
26
- buildPlatformAdapterDeclaration,
27
- resolveBrowserEnvironment,
28
- INTEGRATION_SITE_SURFACES,
29
- type IntegrationSiteSurface,
30
- } from "@tot/public-runtime";
31
-
32
- export const prerender = false;
33
-
34
- export async function POST(context: APIContext): Promise<Response> {
35
- const url = new URL(context.request.url);
36
- const requestedTenant = url.searchParams.get("tenant");
37
- const auth = await resolveOperatorSession(context, requestedTenant);
38
- if (!auth.ok) return json({ error: auth.message }, auth.status);
39
- const roleDenied = requireManageRole(auth);
40
- if (roleDenied) return roleDenied;
41
-
42
- let body: unknown;
43
- try {
44
- body = await context.request.json();
45
- } catch {
46
- return json({ error: "request body must be JSON" }, 400);
47
- }
48
- if (typeof body !== "object" || body === null) {
49
- return json({ error: "request body must be a JSON object" }, 400);
50
- }
51
- const action = (body as { action?: unknown }).action;
52
-
53
- if (action === "catalog") {
54
- return json({ catalog: platformAdapterCatalog() });
55
- }
56
-
57
- if (action === "build") {
58
- const fields = (body as { fields?: unknown }).fields;
59
- const base = typeof fields === "object" && fields !== null ? (fields as Record<string, unknown>) : {};
60
- const surfaceParam = (base["siteSurface"] as IntegrationSiteSurface | undefined) ?? "storefront";
61
- const siteSurface = INTEGRATION_SITE_SURFACES.includes(surfaceParam) ? surfaceParam : "storefront";
62
- // A build always targets a candidate; the environment is server-derived, so a
63
- // browser can never forge a live-stage declaration.
64
- const environment = resolveBrowserEnvironment({
65
- siteSurface,
66
- canonicalHost: url.host,
67
- revision: typeof base["revision"] === "string" ? (base["revision"] as string) : "adapter-candidate",
68
- dev: false,
69
- localHost: false,
70
- candidate: true,
71
- previewHost: false,
72
- customizationEnv: "test",
73
- });
74
- const result = buildPlatformAdapterDeclaration({
75
- adapterId: base["adapterId"],
76
- ids: base["ids"],
77
- tenantPublicId: auth.tenantId,
78
- environment,
79
- });
80
- const status = result.ok ? 200 : result.reason === "unknown-adapter" ? 404 : 422;
81
- return json({ result }, status);
82
- }
83
-
84
- return json({ error: 'action must be "catalog" or "build"' }, 400);
85
- }
@@ -1,79 +0,0 @@
1
- /**
2
- * POST /api/apps/admin/resume — resume a suspended install back to active
3
- * (PrivateApps epic, D2 Chunk C; D8 console-session auth added).
4
- *
5
- * `registryService.resume()` takes a bare `installId` with no tenant check of
6
- * its own (Chunk A assumption #1) — so this route resolves the install via
7
- * `requireOwnedInstall` BEFORE calling it, rejecting a foreign-tenant
8
- * `installId` with the SAME 404 a truly-missing one gets (no cross-tenant
9
- * existence oracle). Mutating: `requireManageRole` gates a console
10
- * `developer` role out — the host-bound/Bearer path is exempt (no `role`).
11
- */
12
- import type { APIContext } from "astro";
13
- import { resolveOperatorSession } from "@/lib/apps/adminSession.js";
14
- import {
15
- getRegistryService,
16
- registryErrorResponse,
17
- requireOwnedInstall,
18
- requireManageRole,
19
- parseInstallId,
20
- json,
21
- } from "@/lib/apps/adminService.js";
22
- import { RegistryError } from "@/lib/apps/registryService.js";
23
- import { resolveUiActor } from "@/lib/activity/uiActor";
24
- import { recordActivity } from "@/lib/activity/recordActivity";
25
-
26
- export const prerender = false;
27
-
28
- export async function POST(context: APIContext): Promise<Response> {
29
- const requestedTenant = new URL(context.request.url).searchParams.get("tenant");
30
- const auth = await resolveOperatorSession(context, requestedTenant);
31
- if (!auth.ok) return json({ error: auth.message }, auth.status);
32
- const manageDenied = requireManageRole(auth);
33
- if (manageDenied) return manageDenied;
34
-
35
- let body: unknown;
36
- try {
37
- body = await context.request.json();
38
- } catch {
39
- return json({ error: "invalid JSON body" }, 400);
40
- }
41
-
42
- const installId = parseInstallId(body);
43
- if (!installId) return json({ error: "installId is required" }, 400);
44
-
45
- const registry = await getRegistryService();
46
- if (registry instanceof Response) return registry;
47
-
48
- const owned = await requireOwnedInstall(registry, installId, auth.tenantId);
49
- if (owned instanceof Response) return owned;
50
-
51
- // D10: acting cohort (merchant/admin), emitted source "server" per the app.* catalog.
52
- const activityActor = await resolveUiActor(context, { owner: auth.tenantId });
53
-
54
- try {
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
- });
64
- return json({ install });
65
- } catch (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
- }
77
- throw err;
78
- }
79
- }
@@ -1,80 +0,0 @@
1
- /**
2
- * POST /api/apps/admin/suspend — suspend an active install: immediately
3
- * disables its API tokens, webhook delivery, and widget placements
4
- * (PrivateApps epic, D2 Chunk C; D8 console-session auth added).
5
- *
6
- * `registryService.suspend()` takes a bare `installId` with no tenant check
7
- * of its own (Chunk A assumption #1) — so this route resolves the install via
8
- * `requireOwnedInstall` BEFORE calling it, rejecting a foreign-tenant
9
- * `installId` with the SAME 404 a truly-missing one gets (no cross-tenant
10
- * existence oracle). Mutating: `requireManageRole` gates a console
11
- * `developer` role out — the host-bound/Bearer path is exempt (no `role`).
12
- */
13
- import type { APIContext } from "astro";
14
- import { resolveOperatorSession } from "@/lib/apps/adminSession.js";
15
- import {
16
- getRegistryService,
17
- registryErrorResponse,
18
- requireOwnedInstall,
19
- requireManageRole,
20
- parseInstallId,
21
- json,
22
- } from "@/lib/apps/adminService.js";
23
- import { RegistryError } from "@/lib/apps/registryService.js";
24
- import { resolveUiActor } from "@/lib/activity/uiActor";
25
- import { recordActivity } from "@/lib/activity/recordActivity";
26
-
27
- export const prerender = false;
28
-
29
- export async function POST(context: APIContext): Promise<Response> {
30
- const requestedTenant = new URL(context.request.url).searchParams.get("tenant");
31
- const auth = await resolveOperatorSession(context, requestedTenant);
32
- if (!auth.ok) return json({ error: auth.message }, auth.status);
33
- const manageDenied = requireManageRole(auth);
34
- if (manageDenied) return manageDenied;
35
-
36
- let body: unknown;
37
- try {
38
- body = await context.request.json();
39
- } catch {
40
- return json({ error: "invalid JSON body" }, 400);
41
- }
42
-
43
- const installId = parseInstallId(body);
44
- if (!installId) return json({ error: "installId is required" }, 400);
45
-
46
- const registry = await getRegistryService();
47
- if (registry instanceof Response) return registry;
48
-
49
- const owned = await requireOwnedInstall(registry, installId, auth.tenantId);
50
- if (owned instanceof Response) return owned;
51
-
52
- // D10: acting cohort (merchant/admin), emitted source "server" per the app.* catalog.
53
- const activityActor = await resolveUiActor(context, { owner: auth.tenantId });
54
-
55
- try {
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
- });
65
- return json({ install });
66
- } catch (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
- }
78
- throw err;
79
- }
80
- }
@@ -1,98 +0,0 @@
1
- /**
2
- * POST /api/apps/admin/uninstall — uninstall an app: revokes credentials and
3
- * clears placements, but retains the install row + audit trail (PrivateApps
4
- * epic, D2 Chunk C; D8 console-session auth added).
5
- *
6
- * `registryService.uninstall()` takes a bare `installId` with no tenant check
7
- * of its own (Chunk A assumption #1) — so this route resolves the install via
8
- * `requireOwnedInstall` BEFORE calling it, rejecting a foreign-tenant
9
- * `installId` with the SAME 404 a truly-missing one gets (no cross-tenant
10
- * existence oracle). Mutating: `requireManageRole` gates a console
11
- * `developer` role out — the host-bound/Bearer path is exempt (no `role`).
12
- */
13
- import type { APIContext } from "astro";
14
- import { resolveOperatorSession } from "@/lib/apps/adminSession.js";
15
- import {
16
- getRegistryService,
17
- registryErrorResponse,
18
- requireOwnedInstall,
19
- requireManageRole,
20
- parseInstallId,
21
- json,
22
- } from "@/lib/apps/adminService.js";
23
- import { RegistryError } from "@/lib/apps/registryService.js";
24
- import { emitWebhookEvent } from "@/lib/webhooks/emit.js";
25
- import { resolveUiActor } from "@/lib/activity/uiActor";
26
- import { recordActivity } from "@/lib/activity/recordActivity";
27
-
28
- export const prerender = false;
29
-
30
- export async function POST(context: APIContext): Promise<Response> {
31
- const requestedTenant = new URL(context.request.url).searchParams.get("tenant");
32
- const auth = await resolveOperatorSession(context, requestedTenant);
33
- if (!auth.ok) return json({ error: auth.message }, auth.status);
34
- const manageDenied = requireManageRole(auth);
35
- if (manageDenied) return manageDenied;
36
-
37
- let body: unknown;
38
- try {
39
- body = await context.request.json();
40
- } catch {
41
- return json({ error: "invalid JSON body" }, 400);
42
- }
43
-
44
- const installId = parseInstallId(body);
45
- if (!installId) return json({ error: "installId is required" }, 400);
46
-
47
- const registry = await getRegistryService();
48
- if (registry instanceof Response) return registry;
49
-
50
- const owned = await requireOwnedInstall(registry, installId, auth.tenantId);
51
- if (owned instanceof Response) return owned;
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
-
57
- try {
58
- const install = await registry.uninstall(installId, auth.actor);
59
- try {
60
- // Best-effort, and deliberately AFTER uninstall has already flipped the
61
- // install to "uninstalled" — this is the final farewell delivery; see
62
- // emit.ts / deliveryEngine.ts's matching status exemption for it.
63
- await emitWebhookEvent("app.uninstalled", auth.tenantId, {}, { installId });
64
- } catch (cause) {
65
- console.warn(
66
- "[diag:webhooks:emit]",
67
- JSON.stringify({
68
- topic: "app.uninstalled",
69
- tenantId: auth.tenantId,
70
- installId,
71
- error: cause instanceof Error ? cause.message : String(cause),
72
- }),
73
- );
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
- });
83
- return json({ install });
84
- } catch (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
- }
96
- throw err;
97
- }
98
- }
@@ -1,126 +0,0 @@
1
- /**
2
- * POST /api/apps/admin/update — patch an existing install's version, scopes,
3
- * manifest snapshot, or webhook endpoint (PrivateApps epic, D2 Chunk C; D8
4
- * console-session auth added).
5
- *
6
- * `registryService.update()` takes a bare `installId` with no tenant check of
7
- * its own (Chunk A assumption #1) — so this route resolves the install via
8
- * `requireOwnedInstall` BEFORE calling it, rejecting a foreign-tenant
9
- * `installId` with the SAME 404 a truly-missing one gets (no cross-tenant
10
- * existence oracle). Mutating: `requireManageRole` gates a console
11
- * `developer` role out — the host-bound/Bearer path is exempt (no `role`).
12
- */
13
- import type { APIContext } from "astro";
14
- import { resolveOperatorSession } from "@/lib/apps/adminSession.js";
15
- import {
16
- getRegistryService,
17
- registryErrorResponse,
18
- requireOwnedInstall,
19
- requireManageRole,
20
- parseInstallId,
21
- json,
22
- } from "@/lib/apps/adminService.js";
23
- import { RegistryError, type AppInstallPatch } from "@/lib/apps/registryService.js";
24
- import { resolveUiActor } from "@/lib/activity/uiActor";
25
- import { recordActivity } from "@/lib/activity/recordActivity";
26
-
27
- export const prerender = false;
28
-
29
- interface UpdateBody {
30
- installId?: unknown;
31
- appVersion?: unknown;
32
- scopes?: unknown;
33
- manifest?: unknown;
34
- webhookEndpoint?: unknown;
35
- widgetPlacements?: unknown;
36
- }
37
-
38
- /** Validate + narrow the given fields into `AppInstallPatch`, or a 400 message. */
39
- function parsePatch(body: UpdateBody): AppInstallPatch | string {
40
- const patch: AppInstallPatch = {};
41
- if (body.appVersion !== undefined) {
42
- if (typeof body.appVersion !== "string" || !body.appVersion.trim()) {
43
- return "appVersion, when given, must be a non-empty string";
44
- }
45
- patch.appVersion = body.appVersion;
46
- }
47
- if (body.scopes !== undefined) {
48
- if (!Array.isArray(body.scopes) || body.scopes.some((s) => typeof s !== "string")) {
49
- return "scopes, when given, must be a string[]";
50
- }
51
- patch.scopes = body.scopes as string[];
52
- }
53
- if (body.manifest !== undefined) {
54
- if (body.manifest === null || typeof body.manifest !== "object" || Array.isArray(body.manifest)) {
55
- return "manifest, when given, must be an object";
56
- }
57
- patch.manifest = body.manifest;
58
- }
59
- if (body.webhookEndpoint !== undefined) {
60
- if (body.webhookEndpoint !== null && typeof body.webhookEndpoint !== "string") {
61
- return "webhookEndpoint, when given, must be a string or null";
62
- }
63
- patch.webhookEndpoint = body.webhookEndpoint as string | null;
64
- }
65
- if (body.widgetPlacements !== undefined) {
66
- if (!Array.isArray(body.widgetPlacements)) return "widgetPlacements, when given, must be an array";
67
- patch.widgetPlacements = body.widgetPlacements;
68
- }
69
- return patch;
70
- }
71
-
72
- export async function POST(context: APIContext): Promise<Response> {
73
- const requestedTenant = new URL(context.request.url).searchParams.get("tenant");
74
- const auth = await resolveOperatorSession(context, requestedTenant);
75
- if (!auth.ok) return json({ error: auth.message }, auth.status);
76
- const manageDenied = requireManageRole(auth);
77
- if (manageDenied) return manageDenied;
78
-
79
- let body: UpdateBody;
80
- try {
81
- body = (await context.request.json()) as UpdateBody;
82
- } catch {
83
- return json({ error: "invalid JSON body" }, 400);
84
- }
85
-
86
- const installId = parseInstallId(body);
87
- if (!installId) return json({ error: "installId is required" }, 400);
88
-
89
- const patch = parsePatch(body);
90
- if (typeof patch === "string") return json({ error: patch }, 400);
91
-
92
- const registry = await getRegistryService();
93
- if (registry instanceof Response) return registry;
94
-
95
- const owned = await requireOwnedInstall(registry, installId, auth.tenantId);
96
- if (owned instanceof Response) return owned;
97
-
98
- // D10: acting cohort (merchant/admin), emitted source "server" per the app.* catalog.
99
- const activityActor = await resolveUiActor(context, { owner: auth.tenantId });
100
-
101
- try {
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
- });
111
- return json({ install });
112
- } catch (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
- }
124
- throw err;
125
- }
126
- }
@@ -1,70 +0,0 @@
1
- /**
2
- * POST /api/apps/admin/webhooks/deliveries/{deliveryId}/replay — the
3
- * ops/support replay (PrivateApps epic, D4 Chunk E; D8 console-session auth
4
- * added). Operator session only; `tenantId` comes from `auth.tenantId`
5
- * (`resolveOperatorSession`), never the request. A `deliveryId` whose
6
- * `tenantId` doesn't match the operator's resolved tenant 404s identically to
7
- * a truly-missing one — same no-cross-tenant-oracle discipline
8
- * `requireOwnedInstall` keeps for install ids. `webhook_deliveries` already
9
- * carries its own `tenantId` (set at creation, mirroring the owning
10
- * install's), so this checks that column directly rather than round-tripping
11
- * through the registry. Mutating (re-enqueues a delivery): `requireManageRole`
12
- * gates a console `developer` role out — the host-bound/Bearer path is
13
- * exempt (no `role`).
14
- *
15
- * Not part of the app-facing `openapi.yaml` contract (same as the rest of
16
- * `admin/*`) — no `Idempotency-Key` requirement; an authenticated operator
17
- * retriggering a replay isn't the untrusted-client-retry case that header
18
- * guards against.
19
- *
20
- * Same `requeueForReplay` + same-`deliveryId` re-enqueue as the app-facing
21
- * replay — see that route's doc comment for the full rationale.
22
- */
23
- import type { APIContext } from "astro";
24
- import { resolveOperatorSession } from "@/lib/apps/adminSession.js";
25
- import { json, requireManageRole } from "@/lib/apps/adminService.js";
26
- import { WebhookDeliveryStore } from "@/lib/webhooks/deliveryStore.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 { resolveUiActor, recordUiActivity } from "@/lib/activity/uiActor";
31
-
32
- export const prerender = false;
33
-
34
- export async function POST(context: APIContext): Promise<Response> {
35
- const requestedTenant = new URL(context.request.url).searchParams.get("tenant");
36
- const auth = await resolveOperatorSession(context, requestedTenant);
37
- if (!auth.ok) return json({ error: auth.message }, auth.status);
38
- const manageDenied = requireManageRole(auth);
39
- if (manageDenied) return manageDenied;
40
-
41
- const deliveryId = context.params.deliveryId;
42
- if (!deliveryId) return json({ error: "Delivery not found.", code: "not_found" }, 404);
43
-
44
- const d1 = await readD1("STOREFRONT_APPS_DB");
45
- if (!d1) return json({ error: "webhook store not configured" }, 503);
46
- const store = new WebhookDeliveryStore(fromD1(d1));
47
-
48
- const delivery = await store.getDelivery(deliveryId);
49
- if (!delivery || delivery.tenantId !== auth.tenantId) {
50
- return json({ error: "Delivery not found.", code: "not_found" }, 404);
51
- }
52
-
53
- const dispatcher = await getDispatcher();
54
- if (dispatcher instanceof Response) return dispatcher;
55
-
56
- const requeued = await store.requeueForReplay(deliveryId);
57
- await dispatcher.enqueue({ deliveryId });
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
- });
69
- return json({ delivery: requeued }, 202);
70
- }