@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,119 +0,0 @@
1
- ---
2
- /**
3
- * /dashboard/<appDomain>/apps — the console's app-health list (PrivateApps
4
- * epic, D8 Chunk C). Guarded the same way as `../team.astro`: signed-in
5
- * session + a membership for this appDomain, else bounce.
6
- *
7
- * Thin shell over one session-authed endpoint:
8
- * GET /api/apps/admin/health?tenant=<appDomain> → AppInstallHealthView[]
9
- * `canManage` is resolved server-side from the SAME `MANAGE_ROLES` set the
10
- * mutating admin routes gate on, and passed to the client via `data-can-
11
- * manage` so a `developer` viewer never even sees the suspend/uninstall/
12
- * rotate buttons rendered (the routes still enforce it independently —
13
- * this is UX, not the security boundary).
14
- */
15
- export const prerender = false;
16
-
17
- import { readViewerSession } from "@/lib/auth/route";
18
- import { membershipFor } from "@/lib/dashboard/tenantSelection";
19
- import { MANAGE_ROLES } from "@/lib/apps/adminSession.js";
20
-
21
- const cspNonce = Astro.locals.cspNonce;
22
- const appDomain = decodeURIComponent(Astro.params.appDomain ?? "");
23
-
24
- const session = await readViewerSession(Astro);
25
- if (!session) {
26
- return Astro.redirect(
27
- `/auth/login?next=${encodeURIComponent(`/dashboard/${appDomain}/apps`)}`,
28
- 302,
29
- );
30
- }
31
- const membership = membershipFor(session.tenants, appDomain, {
32
- selection: session.staffSelection,
33
- nowSeconds: Math.floor(Date.now() / 1000),
34
- });
35
- if (!membership) {
36
- return Astro.redirect("/dashboard", 302);
37
- }
38
- const canManage = MANAGE_ROLES.has(membership.role);
39
- const backHref = `/dashboard/${encodeURIComponent(appDomain)}`;
40
- ---
41
-
42
- <html lang="en">
43
- <head>
44
- <meta charset="utf-8" />
45
- <meta name="viewport" content="width=device-width, initial-scale=1" />
46
- <meta name="robots" content="noindex, nofollow" />
47
- <title>Apps — {appDomain}</title>
48
- <style nonce={cspNonce}>
49
- :root { color-scheme: light dark; }
50
- * { box-sizing: border-box; }
51
- body { font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
52
- margin: 0; min-height: 100vh; padding: 2.5rem 1.25rem; background: #f5f4f0; color: #1c1c1c; }
53
- @media (prefers-color-scheme: dark) { body { background: #14141a; color: #ececf1; } }
54
- main { max-width: 48rem; margin-inline: auto; }
55
- .eyebrow { font: 600 .68rem/1 ui-monospace, monospace; letter-spacing: .12em; text-transform: uppercase; color: #8a7f66; margin: 0 0 .5rem; }
56
- h1 { font-size: 1.5rem; letter-spacing: -.01em; margin: 0 0 .3rem; }
57
- .sub { color: #6a6a6a; margin: 0 0 1.5rem; }
58
- @media (prefers-color-scheme: dark) { .sub { color: #a2a2ad; } }
59
- #apps { display: flex; flex-direction: column; gap: 1rem; }
60
- .card { background: #fff; border: 1px solid #e6e4de; border-radius: 12px; padding: 1.15rem 1.25rem; }
61
- @media (prefers-color-scheme: dark) { .card { background: #1e1e28; border-color: #33333f; } }
62
- .card .top { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
63
- .card .name { font-weight: 600; font-size: 1.02rem; }
64
- .badge { font: 600 .68rem/1 ui-monospace, monospace; text-transform: uppercase; letter-spacing: .06em;
65
- border: 1px solid currentColor; border-radius: 999px; padding: .2rem .55rem; }
66
- .badge.status-active, .badge.health-healthy { color: #2e8b45; }
67
- .badge.status-suspended, .badge.health-degraded { color: #b5860b; }
68
- .badge.status-uninstalled, .badge.health-unhealthy, .badge.egress-blocked { color: #b13a3a; }
69
- .badge.egress-ok { color: #2e8b45; }
70
- .badge.egress-no_endpoint { color: #8a8a8a; }
71
- dl.meta { display: grid; grid-template-columns: max-content 1fr; gap: .25rem 1rem; margin: .8rem 0 0; font-size: .85rem; }
72
- dl.meta dt { color: #8a8a8a; }
73
- dl.meta dd { margin: 0; }
74
- .traces { font: .78rem/1.4 ui-monospace, monospace; color: #8a8a8a; word-break: break-all; }
75
- .actions { margin-top: 1rem; display: flex; gap: .5rem; flex-wrap: wrap; }
76
- .rowbtn { padding: .4rem .8rem; font-size: .82rem; background: transparent; color: inherit;
77
- border: 1px solid #cfccc4; border-radius: 7px; font-weight: 600; cursor: pointer; }
78
- @media (prefers-color-scheme: dark) { .rowbtn { border-color: #40404d; } }
79
- .rowbtn:hover:not(:disabled) { border-color: #b7d0ff; }
80
- .rowbtn:disabled { opacity: .5; cursor: default; }
81
- .rowbtn.danger { color: #b13a3a; border-color: #e3b3b3; }
82
- .msg { margin-top: .8rem; font-size: .85rem; border-radius: 9px; padding: .5rem .7rem; display: none; }
83
- .msg.ok { display: block; background: #eaf7ee; border: 1px solid #bfe4c9; color: #1b6b34; }
84
- .msg.err { display: block; background: #fdecec; border: 1px solid #f5c2c2; color: #9a1c1c; }
85
- @media (prefers-color-scheme: dark) {
86
- .msg.ok { background: #16311f; border-color: #2f6b41; color: #a7e0ba; }
87
- .msg.err { background: #3a1f22; border-color: #6b2f34; color: #f6b9b9; }
88
- }
89
- .muted { color: #9a9a9a; font-size: .9rem; }
90
- .who { margin-top: 2rem; font-size: .82rem; color: #8a8a8a; }
91
- .who a, a.back { color: inherit; }
92
- .linkbtn { background: none; border: 0; color: inherit; font: inherit; text-decoration: underline; cursor: pointer; padding: 0; margin: 0; }
93
- .logout { display: inline; }
94
- </style>
95
- </head>
96
- <body>
97
- <main>
98
- <p class="eyebrow"><a class="back" href={backHref}>← {appDomain}</a></p>
99
- <h1>Apps</h1>
100
- <p class="sub">Installed apps and their webhook health for <strong>{appDomain}</strong>.{!canManage &&
101
- " Viewing read-only — ask an admin or owner to suspend, uninstall, or rotate credentials."}</p>
102
-
103
- <div id="apps" data-tenant={appDomain} data-can-manage={String(canManage)}>
104
- <p class="muted">Loading…</p>
105
- </div>
106
-
107
- <p class="who">Signed in as {session.email} ·
108
- <a href="/dashboard">Switch vendor</a> ·
109
- <form class="logout" method="post" action="/api/auth/logout"><button type="submit" class="linkbtn">Sign out</button></form></p>
110
- </main>
111
-
112
- <!--
113
- Client behavior is a STATIC same-origin file (public/js/dashboard-apps.js),
114
- referenced via src so the strict CSP allows it through `script-src 'self'` — no
115
- nonce, no per-build content-hash. Mirrors team.astro's script tag exactly.
116
- -->
117
- <script src="/js/dashboard-apps.js" is:inline defer></script>
118
- </body>
119
- </html>
@@ -1,105 +0,0 @@
1
- ---
2
- /**
3
- * /dashboard/<appDomain> — a single vendor's console (#8425 Stage 0).
4
- *
5
- * Guarded by the signed-in session: the viewer must hold a membership for this
6
- * appDomain (near-side authZ, deny-by-default), else bounce to the picker. The
7
- * page is a thin hub — today it links to the Team page (invite developers) and
8
- * the existing preview/grants surfaces for this tenant.
9
- */
10
- export const prerender = false;
11
-
12
- import { readViewerSession } from "@/lib/auth/route";
13
- import { membershipFor } from "@/lib/dashboard/tenantSelection";
14
-
15
- const cspNonce = Astro.locals.cspNonce;
16
- const appDomain = decodeURIComponent(Astro.params.appDomain ?? "");
17
-
18
- const session = await readViewerSession(Astro);
19
- if (!session) {
20
- return Astro.redirect(
21
- `/auth/login?next=${encodeURIComponent(`/dashboard/${appDomain}`)}`,
22
- 302,
23
- );
24
- }
25
- // Member OR ToT-staff admitted via an explicit vendor selection (distinct path —
26
- // the staff marker is not a tenants[] membership). membershipFor returns a
27
- // staff-derived stand-in (role = the staffRoles capability) when the selection is live.
28
- const membership = membershipFor(session.tenants, appDomain, {
29
- selection: session.staffSelection,
30
- nowSeconds: Math.floor(Date.now() / 1000),
31
- });
32
- if (!membership) {
33
- // Signed in but no access to THIS vendor — back to the picker/search.
34
- return Astro.redirect("/dashboard", 302);
35
- }
36
- const teamHref = `/dashboard/${encodeURIComponent(appDomain)}/team`;
37
- const appsHref = `/dashboard/${encodeURIComponent(appDomain)}/apps`;
38
- ---
39
-
40
- <html lang="en">
41
- <head>
42
- <meta charset="utf-8" />
43
- <meta name="viewport" content="width=device-width, initial-scale=1" />
44
- <meta name="robots" content="noindex, nofollow" />
45
- <title>{appDomain} — vendor dashboard</title>
46
- <style nonce={cspNonce}>
47
- :root { color-scheme: light dark; }
48
- * { box-sizing: border-box; }
49
- body { font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
50
- margin: 0; min-height: 100vh; padding: 2.5rem 1.25rem; background: #f5f4f0; color: #1c1c1c; }
51
- @media (prefers-color-scheme: dark) { body { background: #14141a; color: #ececf1; } }
52
- main { max-width: 44rem; margin-inline: auto; }
53
- .top { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
54
- .eyebrow { font: 600 .68rem/1 ui-monospace, monospace; letter-spacing: .12em; text-transform: uppercase; color: #8a7f66; margin: 0 0 .5rem; }
55
- h1 { font-size: 1.5rem; letter-spacing: -.01em; margin: 0 0 .3rem; }
56
- .role { font: 600 .68rem/1 ui-monospace, monospace; text-transform: uppercase; letter-spacing: .06em;
57
- color: #4a6; border: 1px solid currentColor; border-radius: 999px; padding: .2rem .55rem; }
58
- .sub { color: #6a6a6a; margin: .2rem 0 1.75rem; }
59
- @media (prefers-color-scheme: dark) { .sub { color: #a2a2ad; } }
60
- .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
61
- @media (max-width: 32rem) { .grid { grid-template-columns: 1fr; } }
62
- a.card { display: block; text-decoration: none; color: inherit; background: #fff;
63
- border: 1px solid #e6e4de; border-radius: 12px; padding: 1.15rem 1.25rem; }
64
- @media (prefers-color-scheme: dark) { a.card { background: #1e1e28; border-color: #33333f; } }
65
- a.card:hover { border-color: #b7d0ff; }
66
- .card .h { font-weight: 600; margin: 0 0 .25rem; }
67
- .card .d { color: #8a8a8a; font-size: .88rem; margin: 0; }
68
- .who { margin-top: 2rem; font-size: .82rem; color: #8a8a8a; }
69
- .who a { color: inherit; }
70
- .linkbtn { background: none; border: 0; color: inherit; font: inherit; text-decoration: underline; cursor: pointer; padding: 0; }
71
- .logout { display: inline; }
72
- </style>
73
- </head>
74
- <body>
75
- <main>
76
- <div class="top">
77
- <div>
78
- <p class="eyebrow">Vendor dashboard</p>
79
- <h1>{appDomain}</h1>
80
- </div>
81
- <span class="role">{membership.role}</span>
82
- </div>
83
- <p class="sub">Manage this vendor's developer team and preview.</p>
84
-
85
- <div class="grid">
86
- <a class="card" href={teamHref}>
87
- <p class="h">Team &amp; invites →</p>
88
- <p class="d">See who has developer access and invite new developers by email.</p>
89
- </a>
90
- <a class="card" href={`/preview/dashboard?tenant=${encodeURIComponent(appDomain)}`}>
91
- <p class="h">Preview &amp; deploy →</p>
92
- <p class="d">The tenant's shared preview: target vs actual, reconcile, preview URL.</p>
93
- </a>
94
- <a class="card" href={appsHref}>
95
- <p class="h">Apps →</p>
96
- <p class="d">Installed apps, webhook health, and manage suspend/uninstall/rotate.</p>
97
- </a>
98
- </div>
99
-
100
- <p class="who">Signed in as {session.email} ·
101
- <a href="/dashboard">Switch vendor</a> ·
102
- <form class="logout" method="post" action="/api/auth/logout"><button type="submit" class="linkbtn">Sign out</button></form></p>
103
- </main>
104
- </body>
105
- </html>
@@ -1,177 +0,0 @@
1
- ---
2
- /**
3
- * /dashboard/<appDomain>/team — a vendor's developer team + invite-by-email
4
- * (#8425 Stage 0).
5
- *
6
- * Guarded by the signed-in session (membership for this appDomain). The page is a
7
- * thin client over two session-authed endpoints:
8
- * GET /api/grants/team-members?tenant=<appDomain> → current developer invites
9
- * POST /api/grants/team-invite {tenant, email} → invite a developer
10
- * Both re-check membership server-side; the appDomain here is carried from the
11
- * guarded route, and every call is authorized by the `tot_session` cookie.
12
- */
13
- export const prerender = false;
14
-
15
- import { readViewerSession } from "@/lib/auth/route";
16
- import { membershipFor } from "@/lib/dashboard/tenantSelection";
17
- import { readEnv } from "@/lib/env";
18
-
19
- const cspNonce = Astro.locals.cspNonce;
20
- const appDomain = decodeURIComponent(Astro.params.appDomain ?? "");
21
-
22
- // qa-tier warn: derived from the SAME MCP origin the invite preflight uses
23
- // (MCP_APP_BINDING_URL). We don't want people onboarding developers via qa now that
24
- // prod+qa share data — so warn (non-blocking; qa is legitimate for testing).
25
- let isQaTier = false;
26
- try {
27
- const bindingUrl = await readEnv("MCP_APP_BINDING_URL");
28
- isQaTier = !!bindingUrl && new URL(bindingUrl).hostname.startsWith("mcp.qa.");
29
- } catch {
30
- isQaTier = false;
31
- }
32
-
33
- const session = await readViewerSession(Astro);
34
- if (!session) {
35
- return Astro.redirect(
36
- `/auth/login?next=${encodeURIComponent(`/dashboard/${appDomain}/team`)}`,
37
- 302,
38
- );
39
- }
40
- // Member OR ToT-staff admitted via an explicit vendor selection (distinct path).
41
- const membership = membershipFor(session.tenants, appDomain, {
42
- selection: session.staffSelection,
43
- nowSeconds: Math.floor(Date.now() / 1000),
44
- });
45
- if (!membership) {
46
- return Astro.redirect("/dashboard", 302);
47
- }
48
- // #8453 (`staff-can-invite-governed`, REVERSING the earlier members-only block):
49
- // a ToT-staff viewer admitted via an explicit vendor selection may now invite too —
50
- // the write endpoint mints a signed broker assertion from their session email and
51
- // tot20 verifies it to permit the invite. So the form shows for member OR staff
52
- // (everyone admitted to this page). `isRealMember` is kept only to tailor the copy.
53
- const isRealMember = !!membershipFor(session.tenants, appDomain);
54
- const backHref = `/dashboard/${encodeURIComponent(appDomain)}`;
55
- ---
56
-
57
- <html lang="en">
58
- <head>
59
- <meta charset="utf-8" />
60
- <meta name="viewport" content="width=device-width, initial-scale=1" />
61
- <meta name="robots" content="noindex, nofollow" />
62
- <title>Team — {appDomain}</title>
63
- <style nonce={cspNonce}>
64
- :root { color-scheme: light dark; }
65
- * { box-sizing: border-box; }
66
- body { font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
67
- margin: 0; min-height: 100vh; padding: 2.5rem 1.25rem; background: #f5f4f0; color: #1c1c1c; }
68
- @media (prefers-color-scheme: dark) { body { background: #14141a; color: #ececf1; } }
69
- main { max-width: 44rem; margin-inline: auto; }
70
- .eyebrow { font: 600 .68rem/1 ui-monospace, monospace; letter-spacing: .12em; text-transform: uppercase; color: #8a7f66; margin: 0 0 .5rem; }
71
- h1 { font-size: 1.5rem; letter-spacing: -.01em; margin: 0 0 .3rem; }
72
- h2 { font-size: .95rem; text-transform: uppercase; letter-spacing: .05em; color: #777; margin: 2rem 0 .6rem; }
73
- .sub { color: #6a6a6a; margin: 0 0 1rem; }
74
- @media (prefers-color-scheme: dark) { .sub { color: #a2a2ad; } }
75
- .card { background: #fff; border: 1px solid #e6e4de; border-radius: 12px; padding: 1.25rem; }
76
- @media (prefers-color-scheme: dark) { .card { background: #1e1e28; border-color: #33333f; } }
77
- label { display: block; font-weight: 600; font-size: .82rem; margin: 0 0 .4rem; }
78
- label[for=role], label[for=note] { margin-top: .9rem; }
79
- input[type=email], select, textarea { width: 100%; padding: .7rem .8rem; font-size: 1rem; border-radius: 9px;
80
- border: 1px solid #cfccc4; background: #fff; color: inherit; font-family: inherit; }
81
- select { margin-bottom: .2rem; }
82
- textarea { min-height: 4.5rem; resize: vertical; line-height: 1.4; }
83
- @media (prefers-color-scheme: dark) { input[type=email], select, textarea { background: #14141a; border-color: #40404d; } }
84
- button { margin-top: 1rem; padding: .7rem 1.1rem; border: 0; border-radius: 9px; background: #1a1a1a;
85
- color: #fff; font-weight: 600; font-size: .95rem; cursor: pointer; }
86
- @media (prefers-color-scheme: dark) { button { background: #ececf1; color: #14141a; } }
87
- button:disabled { opacity: .5; cursor: default; }
88
- .rowbtn { margin: 0; padding: .3rem .6rem; font-size: .78rem; background: transparent; color: inherit;
89
- border: 1px solid #cfccc4; border-radius: 7px; font-weight: 600; }
90
- @media (prefers-color-scheme: dark) { .rowbtn { border-color: #40404d; } }
91
- .rowbtn:hover:not(:disabled) { border-color: #b7d0ff; }
92
- td.act { text-align: right; white-space: nowrap; }
93
- table { width: 100%; border-collapse: collapse; }
94
- th, td { text-align: left; padding: .5rem .4rem; border-bottom: 1px solid #e6e4de; font-size: .9rem; }
95
- @media (prefers-color-scheme: dark) { th, td { border-color: #33333f; } }
96
- th { font: 600 .7rem/1 ui-monospace, monospace; text-transform: uppercase; letter-spacing: .05em; color: #999; }
97
- .msg { margin-top: 1rem; font-size: .9rem; border-radius: 9px; padding: .6rem .8rem; display: none; }
98
- .msg.ok { display: block; background: #eaf7ee; border: 1px solid #bfe4c9; color: #1b6b34; }
99
- .msg.err { display: block; background: #fdecec; border: 1px solid #f5c2c2; color: #9a1c1c; }
100
- @media (prefers-color-scheme: dark) {
101
- .msg.ok { background: #16311f; border-color: #2f6b41; color: #a7e0ba; }
102
- .msg.err { background: #3a1f22; border-color: #6b2f34; color: #f6b9b9; }
103
- }
104
- .warn { margin: 0 0 1rem; font-size: .88rem; border-radius: 9px; padding: .6rem .8rem;
105
- background: #fff7e6; border: 1px solid #f0d999; color: #7a5b12; }
106
- @media (prefers-color-scheme: dark) { .warn { background: #332a12; border-color: #6b5a2f; color: #e8d39a; } }
107
- .muted { color: #9a9a9a; font-size: .85rem; }
108
- .who { margin-top: 2rem; font-size: .82rem; color: #8a8a8a; }
109
- .who a, a.back { color: inherit; }
110
- .linkbtn { background: none; border: 0; color: inherit; font: inherit; text-decoration: underline; cursor: pointer; padding: 0; margin: 0; }
111
- .logout { display: inline; }
112
- </style>
113
- </head>
114
- <body>
115
- <main>
116
- <p class="eyebrow"><a class="back" href={backHref}>← {appDomain}</a></p>
117
- <h1>Team</h1>
118
- <p class="sub">Developers with access to <strong>{appDomain}</strong>.{isRealMember
119
- ? " Invite by email — they'll get a branded sign-in link and become eligible on this vendor."
120
- : " Viewing as Token of Trust staff — invite by email to grant a developer access."}</p>
121
-
122
- <h2>Invite a developer</h2>
123
- {isQaTier && (
124
- <p class="warn">⚠ This invite connects the developer to the <strong>QA</strong> environment
125
- — likely not what you want.</p>
126
- )}
127
- <div class="card">
128
- <label for="email">Developer email</label>
129
- <input id="email" type="email" placeholder="dev@agency.com" autocomplete="off" />
130
- {/* tot20's role vocabulary: developer | appAdmin | appOwner | contributor.
131
- `contributor` is a strictly-weaker self-downgrade a plain member may
132
- request directly (tot20 honors it for a non-staff operator — handoff
133
- 2026-08-22-tot20-contributor-role); appAdmin/appOwner still require ToT
134
- staff (#8453) and tot20 ignores them from a plain member (pins to
135
- developer), so only staff see those two options. */}
136
- <label for="role">Role</label>
137
- <select id="role">
138
- <option value="developer" selected>Developer</option>
139
- <option value="contributor">Contributor (blog &amp; content only)</option>
140
- {!isRealMember && (
141
- <>
142
- <option value="appAdmin">Admin</option>
143
- <option value="appOwner">Owner</option>
144
- </>
145
- )}
146
- </select>
147
- <label for="note">Note to the developer (optional)</label>
148
- <textarea id="note" rows="3" maxlength="1000"
149
- placeholder="Add a personal message — they'll see it in the invite email."></textarea>
150
- <button id="inviteBtn" type="button">Send invite</button>
151
- <div id="msg" class="msg"></div>
152
- </div>
153
-
154
- <h2>Current developers</h2>
155
- <div class="card">
156
- <table>
157
- <thead><tr><th>Developer</th><th>Roles</th><th></th></tr></thead>
158
- <tbody id="members" data-tenant={appDomain}><tr><td colspan="3" class="muted">Loading…</td></tr></tbody>
159
- </table>
160
- </div>
161
-
162
- <p class="who">Signed in as {session.email} ·
163
- <a href="/dashboard">Switch vendor</a> ·
164
- <form class="logout" method="post" action="/api/auth/logout"><button type="submit" class="linkbtn">Sign out</button></form></p>
165
- </main>
166
-
167
- <!--
168
- Client behavior is a STATIC same-origin file (public/js/dashboard-team.js),
169
- referenced via src so the strict CSP allows it through `script-src 'self'` — no
170
- nonce, no per-build content-hash. An inline <script> here is blocked: Astro's
171
- `is:inline` drops the templated nonce, and a processed inline script would need
172
- its hash pinned in cspStaticHashes on every edit. `is:inline` keeps Astro from
173
- trying to bundle the external-src tag; `defer` runs it after the DOM parses.
174
- -->
175
- <script src="/js/dashboard-team.js" is:inline defer></script>
176
- </body>
177
- </html>
@@ -1,160 +0,0 @@
1
- ---
2
- /**
3
- * /dashboard — the vendor console entry (#8425 Stage 0).
4
- *
5
- * Federated: the viewer signed in via ToT (email OTP → offline-verified id_token →
6
- * `tot_session`). This page holds NO identity of its own — it reads the session's
7
- * tenant memberships and routes per the L3 contract
8
- * (docs/architecture/identity-and-authorization.md):
9
- * 0 memberships → "nothing to manage"; 1 → straight into that vendor; ≥2 → picker.
10
- *
11
- * STAGE-0: tot20's id_token currently carries only the one resource signed against,
12
- * so this resolves to a single vendor in practice; the picker activates unchanged
13
- * once tot20 populates all memberships (flagged to the workstream lead).
14
- */
15
- export const prerender = false;
16
-
17
- import { readViewerSession } from "@/lib/auth/route";
18
- import { selectTenant } from "@/lib/dashboard/tenantSelection";
19
- import { isStaffSession } from "@/lib/dashboard/staffAdmission";
20
-
21
- const cspNonce = Astro.locals.cspNonce;
22
- const session = await readViewerSession(Astro);
23
- if (!session) {
24
- return Astro.redirect("/auth/login?next=%2Fdashboard", 302);
25
- }
26
-
27
- // ToT-STAFF viewers reach ANY vendor by explicit SEARCH-and-enter (a distinct
28
- // path from tenants[] membership). When staff, we always render the hub (their
29
- // memberships, if any, PLUS a vendor search) and skip the single-membership
30
- // auto-redirect so they can still search for a vendor they don't belong to.
31
- const isStaff = isStaffSession(session);
32
- const selection = selectTenant(session.tenants);
33
- if (!isStaff && selection.mode === "single") {
34
- return Astro.redirect(`/dashboard/${encodeURIComponent(selection.tenant.resource)}`, 302);
35
- }
36
- const vendors =
37
- selection.mode === "picker"
38
- ? selection.tenants
39
- : selection.mode === "single"
40
- ? [selection.tenant]
41
- : [];
42
- ---
43
-
44
- <html lang="en">
45
- <head>
46
- <meta charset="utf-8" />
47
- <meta name="viewport" content="width=device-width, initial-scale=1" />
48
- <meta name="robots" content="noindex, nofollow" />
49
- <title>Your stores — Token of Trust</title>
50
- <style nonce={cspNonce}>
51
- :root { color-scheme: light dark; }
52
- * { box-sizing: border-box; }
53
- body { font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
54
- margin: 0; min-height: 100vh; padding: 2.5rem 1.25rem; background: #f5f4f0; color: #1c1c1c; }
55
- @media (prefers-color-scheme: dark) { body { background: #14141a; color: #ececf1; } }
56
- main { max-width: 40rem; margin-inline: auto; }
57
- .eyebrow { font: 600 .68rem/1 ui-monospace, monospace; letter-spacing: .12em; text-transform: uppercase; color: #8a7f66; margin: 0 0 .5rem; }
58
- h1 { font-size: 1.5rem; letter-spacing: -.01em; margin: 0 0 .3rem; }
59
- .sub { color: #6a6a6a; margin: 0 0 1.75rem; }
60
- @media (prefers-color-scheme: dark) { .sub { color: #a2a2ad; } }
61
- ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .75rem; }
62
- a.vendor { display: flex; align-items: center; justify-content: space-between; text-decoration: none;
63
- background: #fff; border: 1px solid #e6e4de; border-radius: 12px; padding: 1rem 1.25rem; color: inherit; }
64
- @media (prefers-color-scheme: dark) { a.vendor { background: #1e1e28; border-color: #33333f; } }
65
- a.vendor:hover { border-color: #b7d0ff; }
66
- .name { font-weight: 600; }
67
- .role { font: 600 .68rem/1 ui-monospace, monospace; text-transform: uppercase; letter-spacing: .06em;
68
- color: #4a6; border: 1px solid currentColor; border-radius: 999px; padding: .2rem .55rem; }
69
- .who { margin-top: 2rem; font-size: .82rem; color: #8a8a8a; }
70
- .who a { color: inherit; }
71
- .linkbtn { background: none; border: 0; color: inherit; font: inherit; text-decoration: underline; cursor: pointer; padding: 0; }
72
- .logout { display: inline; }
73
- .empty { background: #fff; border: 1px solid #e6e4de; border-radius: 12px; padding: 1.5rem; }
74
- @media (prefers-color-scheme: dark) { .empty { background: #1e1e28; border-color: #33333f; } }
75
- /* Staff vendor search */
76
- .staff { margin-top: 2.25rem; }
77
- h2 { font-size: .95rem; text-transform: uppercase; letter-spacing: .05em; color: #777; margin: 0 0 .3rem; }
78
- .staff .hint { color: #8a8a8a; font-size: .85rem; margin: 0 0 .7rem; }
79
- .staff .hint code { font: .8em ui-monospace, monospace; background: #ece9e1; color: #4a4a4a;
80
- padding: .1em .35em; border-radius: 5px; }
81
- @media (prefers-color-scheme: dark) { .staff .hint code { background: #2a2a33; color: #cfcfda; } }
82
- .search { position: relative; }
83
- #vendorSearch { width: 100%; padding: .75rem .85rem; font-size: 1rem; border-radius: 10px;
84
- border: 1px solid #cfccc4; background: #fff; color: inherit; }
85
- @media (prefers-color-scheme: dark) { #vendorSearch { background: #14141a; border-color: #40404d; } }
86
- #vendorResults { list-style: none; margin: .6rem 0 0; padding: 0; display: grid; gap: .5rem; }
87
- #vendorResults button { display: flex; width: 100%; align-items: center; justify-content: space-between; gap: 1rem;
88
- text-align: left; background: #fff; border: 1px solid #e6e4de; border-radius: 10px; padding: .8rem 1rem;
89
- color: inherit; font: inherit; cursor: pointer; }
90
- @media (prefers-color-scheme: dark) { #vendorResults button { background: #1e1e28; border-color: #33333f; } }
91
- #vendorResults button:hover, #vendorResults button:focus-visible { border-color: #b7d0ff; }
92
- #vendorResults button:disabled { opacity: .5; cursor: default; }
93
- #vendorResults .dn { font-weight: 600; }
94
- #vendorResults .ad { font: 600 .68rem/1 ui-monospace, monospace; color: #8a8a8a; }
95
- #vendorSearchMsg { margin-top: .6rem; font-size: .88rem; color: #8a8a8a; min-height: 1.1em; }
96
- #vendorSearchMsg.err { color: #9a1c1c; }
97
- @media (prefers-color-scheme: dark) { #vendorSearchMsg.err { color: #f6b9b9; } }
98
- .badge { display: inline-block; font: 600 .6rem/1 ui-monospace, monospace; letter-spacing: .08em;
99
- text-transform: uppercase; color: #8a7f66; border: 1px solid currentColor; border-radius: 999px;
100
- padding: .18rem .5rem; margin-left: .5rem; vertical-align: middle; }
101
- </style>
102
- </head>
103
- <body>
104
- <main>
105
- <p class="eyebrow">Token of Trust</p>
106
- <h1>Your stores{isStaff && <span class="badge">ToT staff</span>}</h1>
107
- {vendors.length ? (
108
- <>
109
- <p class="sub">Choose a store to manage its team and preview.</p>
110
- <ul>
111
- {vendors.map((v) => (
112
- <li>
113
- <a class="vendor" href={`/dashboard/${encodeURIComponent(v.resource)}`}>
114
- <span class="name">{v.resource}</span>
115
- <span class="role">{v.role}</span>
116
- </a>
117
- </li>
118
- ))}
119
- </ul>
120
- </>
121
- ) : (
122
- !isStaff && (
123
- <div class="empty">
124
- <p style="margin:0">You're signed in as <strong>{session.email}</strong>, but
125
- you don't have access to any store yet. Ask a store's merchant to invite you.</p>
126
- </div>
127
- )
128
- )}
129
-
130
- {isStaff && (
131
- <section class="staff">
132
- <h2>Find a store</h2>
133
- <p class="hint">As Token of Trust staff you can enter any store's console. Search by
134
- domain, then select one to enter — every admission is logged.</p>
135
- <p class="hint">Tip: <code>^</code> matches the start, <code>$</code> the end — e.g.
136
- <code>^acme.com$</code> for an exact match, <code>acme$</code> to end with “acme”.</p>
137
- <div class="search">
138
- <input id="vendorSearch" type="text" placeholder="acme.com"
139
- autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false"
140
- aria-label="Search stores by domain" />
141
- <ul id="vendorResults"></ul>
142
- <p id="vendorSearchMsg" aria-live="polite"></p>
143
- </div>
144
- </section>
145
- )}
146
-
147
- <p class="who">Signed in as {session.email} ·
148
- <form class="logout" method="post" action="/api/auth/logout"><button type="submit" class="linkbtn">Sign out</button></form></p>
149
- </main>
150
-
151
- <!--
152
- Staff vendor-search behavior is a STATIC same-origin file
153
- (public/js/dashboard-vendor-search.js), referenced via src so the strict CSP
154
- allows it through `script-src 'self'` — no nonce, no per-build content-hash
155
- (see public/js/dashboard-team.js for the proven pattern). The file no-ops when
156
- #vendorSearch is absent (non-staff), so it's safe to always include.
157
- -->
158
- <script src="/js/dashboard-vendor-search.js" is:inline defer></script>
159
- </body>
160
- </html>