@tokenoftrust/storefront-runner 1.4.1 → 1.4.2-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/storefront/astro.config.mjs +17 -1
- package/apps/storefront/integrations/materialize-guard.mjs +56 -0
- package/apps/storefront/migrations-apps/0002_good_switch.sql +26 -0
- package/apps/storefront/migrations-apps/0003_mute_marauders.sql +12 -0
- package/apps/storefront/migrations-apps/meta/0002_snapshot.json +1160 -0
- package/apps/storefront/migrations-apps/meta/0003_snapshot.json +1235 -0
- package/apps/storefront/migrations-apps/meta/_journal.json +14 -0
- package/apps/storefront/public/js/dashboard-team.js +38 -2
- package/apps/storefront/src/components/Seo.astro +65 -1
- package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +878 -0
- package/apps/storefront/src/components/admin/AdminBlogTab.astro +1432 -0
- package/apps/storefront/src/components/admin/AdminCustomersTab.astro +3 -7
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +1105 -2334
- package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +1 -3
- package/apps/storefront/src/components/blog/BlogPaywall.astro +87 -0
- package/apps/storefront/src/components/blog/PostAccessBadge.astro +43 -0
- package/apps/storefront/src/components/blog/PostGrid.astro +57 -0
- package/apps/storefront/src/components/blog/ReactionBar.astro +214 -0
- package/apps/storefront/src/layouts/Layout.astro +48 -3
- package/apps/storefront/src/lib/activity/alerts.ts +17 -19
- package/apps/storefront/src/lib/activity/changeActorAttribution.ts +3 -3
- package/apps/storefront/src/lib/activity/deployVersion.ts +19 -26
- package/apps/storefront/src/lib/activity/ingest.ts +8 -8
- package/apps/storefront/src/lib/activity/ingestAuth.ts +1 -1
- package/apps/storefront/src/lib/activity/killSwitch.ts +6 -6
- package/apps/storefront/src/lib/activity/query.ts +9 -9
- package/apps/storefront/src/lib/activity/recordActivity.ts +11 -15
- package/apps/storefront/src/lib/activity/store.ts +4 -6
- package/apps/storefront/src/lib/activity/uiActor.ts +12 -12
- package/apps/storefront/src/lib/activity/workerCommit.ts +6 -6
- package/apps/storefront/src/lib/admin/adminShell.ts +290 -0
- package/apps/storefront/src/lib/admin/ai/approvalMode.ts +2 -3
- package/apps/storefront/src/lib/admin/ai/audit.ts +3 -5
- package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +7 -7
- package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +3 -3
- package/apps/storefront/src/lib/admin/orderClient.ts +3 -5
- package/apps/storefront/src/lib/admin/services/orders.ts +17 -19
- package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +4 -6
- package/apps/storefront/src/lib/admin/viewmodels/settings.ts +1 -1
- package/apps/storefront/src/lib/apps/adminService.ts +4 -4
- package/apps/storefront/src/lib/apps/adminSession.ts +53 -44
- package/apps/storefront/src/lib/apps/apiAuth.ts +2 -2
- package/apps/storefront/src/lib/apps/apiRoute.ts +2 -2
- package/apps/storefront/src/lib/apps/catalogMapper.ts +1 -1
- package/apps/storefront/src/lib/apps/credentials.ts +2 -2
- package/apps/storefront/src/lib/apps/gatewayKeys.ts +4 -4
- package/apps/storefront/src/lib/apps/healthAggregate.ts +5 -5
- package/apps/storefront/src/lib/apps/orders/attributionService.ts +2 -2
- package/apps/storefront/src/lib/apps/orders/customerHash.ts +1 -1
- package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +12 -12
- package/apps/storefront/src/lib/apps/orders/idempotency.ts +7 -7
- package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +1 -1
- package/apps/storefront/src/lib/apps/orders/orderMapper.ts +2 -2
- package/apps/storefront/src/lib/apps/orders/ordersStore.ts +6 -6
- package/apps/storefront/src/lib/apps/registryService.ts +6 -6
- package/apps/storefront/src/lib/apps/scopes.ts +2 -2
- package/apps/storefront/src/lib/apps/tokenIssuer.ts +4 -4
- package/apps/storefront/src/lib/apps/tokenVerifier.ts +6 -6
- package/apps/storefront/src/lib/apps/widgets/eligibility.ts +2 -2
- package/apps/storefront/src/lib/apps/widgets/frameProps.ts +3 -3
- package/apps/storefront/src/lib/apps/widgets/launchToken.ts +4 -4
- package/apps/storefront/src/lib/apps/widgets/placements.ts +2 -2
- package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +1 -1
- package/apps/storefront/src/lib/auth/adminEntry.ts +9 -9
- package/apps/storefront/src/lib/auth/identityToken.ts +6 -14
- package/apps/storefront/src/lib/auth/loginGate.ts +75 -13
- package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +2 -4
- package/apps/storefront/src/lib/auth/operatorSession.ts +113 -0
- package/apps/storefront/src/lib/auth/session.ts +7 -8
- package/apps/storefront/src/lib/auth/totAccessClient.ts +8 -18
- package/apps/storefront/src/lib/basePath.ts +9 -2
- package/apps/storefront/src/lib/blog/access.ts +29 -0
- package/apps/storefront/src/lib/blog/collection.ts +313 -0
- package/apps/storefront/src/lib/blog/markdown.ts +174 -0
- package/apps/storefront/src/lib/blog/provider.ts +5 -0
- package/apps/storefront/src/lib/blog/reactions.ts +87 -0
- package/apps/storefront/src/lib/blog/rss.ts +92 -0
- package/apps/storefront/src/lib/blog/teaser.ts +83 -0
- package/apps/storefront/src/lib/blog/types.ts +34 -0
- package/apps/storefront/src/lib/blog/viewCounts.ts +243 -0
- package/apps/storefront/src/lib/d1/catalog.ts +2 -2
- package/apps/storefront/src/lib/d1/schema-apps.ts +96 -17
- package/apps/storefront/src/lib/dev/activityIngestToken.ts +2 -4
- package/apps/storefront/src/lib/dev/activityStore.ts +3 -5
- package/apps/storefront/src/lib/dev/apiBase.ts +1 -4
- package/apps/storefront/src/lib/dev/cockpitStore.ts +1 -9
- package/apps/storefront/src/lib/dev/devDraft.ts +1 -1
- package/apps/storefront/src/lib/dev/previewStatus.ts +9 -21
- package/apps/storefront/src/lib/dev/rendezvousBroker.ts +9 -24
- package/apps/storefront/src/lib/edgeCache.ts +17 -0
- package/apps/storefront/src/lib/http/fetchTransport.ts +63 -0
- package/apps/storefront/src/lib/i18n.ts +25 -0
- package/apps/storefront/src/lib/jsonld.ts +118 -0
- package/apps/storefront/src/lib/monitoring/manifest.ts +11 -11
- package/apps/storefront/src/lib/newsletter/confirmationEmail.ts +30 -0
- package/apps/storefront/src/lib/newsletter/consent.ts +23 -0
- package/apps/storefront/src/lib/newsletter/digest.ts +117 -0
- package/apps/storefront/src/lib/newsletter/resendClient.ts +79 -0
- package/apps/storefront/src/lib/newsletter/send.ts +265 -0
- package/apps/storefront/src/lib/newsletter/subscribers.ts +156 -0
- package/apps/storefront/src/lib/publish/adminPublishTab.ts +3519 -0
- package/apps/storefront/src/lib/publish/apex-readiness.ts +41 -18
- package/apps/storefront/src/lib/publish/dispatchHealth.ts +5 -7
- package/apps/storefront/src/lib/publish/domainState.ts +22 -13
- package/apps/storefront/src/lib/publish/panelPolling.ts +78 -0
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +100 -49
- package/apps/storefront/src/lib/publish/stageBundleUpload.ts +215 -0
- package/apps/storefront/src/lib/rate/kvWindow.ts +53 -0
- package/apps/storefront/src/lib/search/index.ts +2 -2
- package/apps/storefront/src/lib/seo/alternates.ts +42 -0
- package/apps/storefront/src/lib/seo/meta.ts +65 -0
- package/apps/storefront/src/lib/social/golive.ts +212 -0
- package/apps/storefront/src/lib/social/notify.ts +32 -0
- package/apps/storefront/src/lib/social/tokenStore.ts +171 -0
- package/apps/storefront/src/lib/the-build/provider.ts +27 -0
- package/apps/storefront/src/lib/the-build/readingTime.ts +35 -0
- package/apps/storefront/src/lib/the-build/toc.ts +95 -0
- package/apps/storefront/src/lib/tot/ToTClient.ts +1 -1
- package/apps/storefront/src/lib/tot/d1Client.ts +2 -2
- package/apps/storefront/src/lib/tot/totClientInterface.ts +3 -4
- package/apps/storefront/src/lib/webhooks/deliveryStore.ts +16 -17
- package/apps/storefront/src/middleware/index.ts +113 -2
- package/apps/storefront/src/pages/admin.astro +10 -1312
- package/apps/storefront/src/pages/api/blog/[collection]/[slug]/react.ts +104 -0
- package/apps/storefront/src/pages/api/newsletter/[collection]/subscribe.ts +146 -0
- package/apps/storefront/src/pages/api/newsletter/confirm.ts +75 -0
- package/apps/storefront/src/pages/api/newsletter/unsubscribe.ts +90 -0
- package/apps/storefront/src/pages/api/publish/stage-bundle.ts +70 -0
- package/apps/storefront/src/pages/api/rum/vitals.ts +21 -2
- package/apps/storefront/src/pages/blog/[slug].astro +96 -16
- package/apps/storefront/src/pages/blog/author/[author].astro +140 -0
- package/apps/storefront/src/pages/blog/category/[category].astro +67 -0
- package/apps/storefront/src/pages/blog/index.astro +2 -0
- package/apps/storefront/src/pages/blog/tag/[tag].astro +65 -0
- package/apps/storefront/src/pages/cockpit.astro +37 -12
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +15 -11
- package/apps/storefront/src/pages/search.astro +1 -1
- package/apps/storefront/src/pages/sitemap.xml.ts +20 -0
- package/apps/storefront/src/pages/the-build/[slug].astro +129 -0
- package/apps/storefront/src/pages/the-build/index.astro +146 -0
- package/apps/storefront/src/pages/the-build/rss.xml.ts +33 -0
- package/apps/storefront/src/styles/admin.css +1080 -0
- package/apps/storefront/tsconfig.json +5 -0
- package/package.json +1 -1
- package/packages/public-runtime/package.json +1 -0
- package/packages/public-runtime/src/activity/catalog.ts +52 -0
- package/packages/public-runtime/src/catalog-d1.ts +1 -1
- package/packages/public-runtime/src/customization-preview.ts +25 -2
- package/packages/public-runtime/src/customization-runtime.ts +125 -3
- package/packages/public-runtime/src/customization-versioning.ts +10 -0
- package/packages/public-runtime/src/index.ts +1 -0
- package/packages/public-runtime/src/static-bundle.ts +357 -0
- package/packages/public-runtime/src/tenant.ts +36 -8
- package/scripts/build/copy-tenant-assets.mjs +14 -4
- package/scripts/dev/ai-edit.mjs +1 -1
- package/scripts/dev/checkout-watch.mjs +2 -2
- package/scripts/dev/file-browser.mjs +2 -4
- package/scripts/dev/file-writer.mjs +1 -1
- package/scripts/dev/git-status.mjs +2 -2
- package/scripts/dev/locate-handler.mjs +3 -3
- package/scripts/dev/port-check.mjs +3 -3
- package/scripts/dev/publish.mjs +5 -9
- package/scripts/dev/shot.mjs +1 -1
- package/scripts/dev/unified-diff.mjs +8 -3
- package/scripts/tot-dev.mjs +6 -9
- package/apps/storefront/src/lib/dev/vcBinding.ts +0 -80
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Operator-session resolution for the Private Apps admin API
|
|
3
|
-
* (`/api/apps/admin
|
|
3
|
+
* (`/api/apps/admin/*`). Mirrors
|
|
4
4
|
* `../grants/session.ts`'s owner-session resolution — same two paths, same
|
|
5
5
|
* fail-closed shape — but resolves `{ tenantId, actor }` instead of `{ owner }`:
|
|
6
6
|
*
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* Routes depend only on the `{ ok, tenantId, actor }` result. `authorizeAdmin`
|
|
39
39
|
* is the pure core of path 2 (unit-testable, no I/O).
|
|
40
40
|
*
|
|
41
|
-
*
|
|
41
|
+
* Console admin session. The two paths above are host-bound:
|
|
42
42
|
* they resolve exactly one tenant (`context.locals.tenant`). The
|
|
43
43
|
* `/api/apps/admin/*` surface is also reachable from the multi-tenant vendor
|
|
44
44
|
* console, where a signed-in viewer may hold memberships on several vendors —
|
|
@@ -61,6 +61,11 @@ import { sha256Hex } from "@tot/public-runtime";
|
|
|
61
61
|
import { readEnv } from "@/lib/env";
|
|
62
62
|
import { readViewerSession } from "@/lib/auth/route.js";
|
|
63
63
|
import { viewerCanAccess, membershipFor } from "@/lib/dashboard/tenantSelection.js";
|
|
64
|
+
import {
|
|
65
|
+
authorizeBearerOperator,
|
|
66
|
+
resolveHostBoundSession,
|
|
67
|
+
type SessionFailure,
|
|
68
|
+
} from "@/lib/auth/operatorSession.js";
|
|
64
69
|
|
|
65
70
|
/** Capabilities that make a signed-in viewer an app-registry operator for their tenant. */
|
|
66
71
|
const ADMIN_CAPABILITIES: ReadonlySet<string> = new Set(["owner", "admin"]);
|
|
@@ -88,33 +93,30 @@ export interface AdminAuthInput {
|
|
|
88
93
|
|
|
89
94
|
export type AdminAuthResult =
|
|
90
95
|
| { ok: true; tenantId: string; actor: string; role?: string }
|
|
91
|
-
|
|
|
96
|
+
| SessionFailure;
|
|
97
|
+
|
|
98
|
+
/** The one authorized-success shape both host-bound and Bearer admin paths return. */
|
|
99
|
+
type AdminAuthOk = Extract<AdminAuthResult, { ok: true }>;
|
|
92
100
|
|
|
93
101
|
/**
|
|
94
102
|
* Decide the authenticated tenant from env + request, with NO I/O —
|
|
95
103
|
* unit-testable. Fail-closed: unconfigured gate → 503, bad/absent Bearer →
|
|
96
|
-
* 401, no resolvable tenant → 400.
|
|
104
|
+
* 401, no resolvable tenant → 400. A thin adapter over the shared
|
|
105
|
+
* Bearer-operator core that maps its resolved `principal` to this surface's
|
|
106
|
+
* `tenantId` and stamps the fixed `BEARER_ACTOR` (no per-caller identity here).
|
|
97
107
|
*/
|
|
98
108
|
export function authorizeAdmin(input: AdminAuthInput): AdminAuthResult {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (!tenantId) {
|
|
111
|
-
return {
|
|
112
|
-
ok: false,
|
|
113
|
-
status: 400,
|
|
114
|
-
message: "tenant unresolved (set APPS_ADMIN_TENANT or X-Tot-Tenant)",
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
return { ok: true, tenantId, actor: BEARER_ACTOR };
|
|
109
|
+
const res = authorizeBearerOperator({
|
|
110
|
+
authHeader: input.authHeader,
|
|
111
|
+
gateSecret: input.gateSecret,
|
|
112
|
+
configuredPrincipal: input.configuredTenant,
|
|
113
|
+
requestedPrincipal: input.requestedTenant,
|
|
114
|
+
unconfiguredMessage: "apps admin operator secret not configured",
|
|
115
|
+
unresolvedMessage: "tenant unresolved (set APPS_ADMIN_TENANT or X-Tot-Tenant)",
|
|
116
|
+
});
|
|
117
|
+
return res.ok
|
|
118
|
+
? { ok: true, tenantId: res.principal, actor: BEARER_ACTOR }
|
|
119
|
+
: res;
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
/** One-way, stable pseudonym for an audit `actor` — never the email itself. */
|
|
@@ -128,30 +130,37 @@ async function actorFromEmail(email: string): Promise<string> {
|
|
|
128
130
|
* from the body.
|
|
129
131
|
*/
|
|
130
132
|
export async function resolveAdminSession(context: APIContext): Promise<AdminAuthResult> {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
133
|
+
return resolveHostBoundSession<AdminAuthOk>(context, {
|
|
134
|
+
capabilities: ADMIN_CAPABILITIES,
|
|
135
|
+
// A resolved-but-insufficient viewer fails closed with 403 and does NOT
|
|
136
|
+
// fall through to the Bearer gate — an authenticated-but-under-privileged
|
|
137
|
+
// caller must not be silently admitted by a secret it also presents.
|
|
138
|
+
onInsufficientCapability: {
|
|
139
|
+
mode: "failClosed",
|
|
140
|
+
failure: {
|
|
138
141
|
ok: false,
|
|
139
142
|
status: 403,
|
|
140
143
|
message: "not authorized to manage apps for this tenant",
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
// Path 1: the tenant IS the resolved host's appDomain — never the body.
|
|
147
|
+
resolveViewer: async (viewer, tenantId) => ({
|
|
148
|
+
ok: true,
|
|
149
|
+
tenantId,
|
|
150
|
+
actor: await actorFromEmail(viewer.email),
|
|
151
|
+
}),
|
|
152
|
+
// Path 2 (fallback): the headless-console Bearer operator secret. Dedicated
|
|
153
|
+
// secret only — no fallback to another deployment's secret.
|
|
154
|
+
resolveBearer: async (ctx) => {
|
|
155
|
+
const gateSecret = await readEnv("APPS_ADMIN_SECRET");
|
|
156
|
+
const configuredTenant = await readEnv("APPS_ADMIN_TENANT");
|
|
157
|
+
return authorizeAdmin({
|
|
158
|
+
authHeader: ctx.request.headers.get("authorization"),
|
|
159
|
+
gateSecret,
|
|
160
|
+
configuredTenant,
|
|
161
|
+
requestedTenant: ctx.request.headers.get("x-tot-tenant"),
|
|
162
|
+
});
|
|
163
|
+
},
|
|
155
164
|
});
|
|
156
165
|
}
|
|
157
166
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The scoped-API authorization boundary — every
|
|
3
3
|
* `/api/apps/v1/*` route calls `authorizeAppRequest` FIRST. Verifies the
|
|
4
|
-
* app-gateway bearer JWT (
|
|
4
|
+
* app-gateway bearer JWT (`AppTokenVerifier`), resolves the calling
|
|
5
5
|
* install from the registry, and enforces the single scope the route
|
|
6
6
|
* requires per `docs/private-apps/contract/openapi.yaml`'s `x-requiredScope`.
|
|
7
7
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Response helpers shared by every `/api/apps/v1/*` route
|
|
3
|
-
*
|
|
2
|
+
* Response helpers shared by every `/api/apps/v1/*` route — a no-store JSON
|
|
3
|
+
* responder (mirrors `../grants/route.ts` /
|
|
4
4
|
* `./adminService.ts`'s pattern) plus the `tot-request-id` / `traceparent`
|
|
5
5
|
* response headers `docs/private-apps/contract/openapi.yaml` documents on
|
|
6
6
|
* every operation.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Maps the internal `CatalogProduct` read model to the
|
|
2
|
+
* Maps the internal `CatalogProduct` read model to the scoped-API contract's
|
|
3
3
|
* `Product` shape (`docs/private-apps/contract/openapi.yaml`, schema
|
|
4
4
|
* `Product` — `additionalProperties: false`). Drops every internal field
|
|
5
5
|
* the contract doesn't declare (variants, images, metafields, source, …) so
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* D1-backed `CredentialPort` implementation
|
|
3
|
-
*
|
|
2
|
+
* D1-backed `CredentialPort` implementation — issues/rotates/clears the
|
|
3
|
+
* OAuth2 client-credentials for an app install, and
|
|
4
4
|
* verifies a presented `(clientId, secret)` pair for the token endpoint
|
|
5
5
|
* (`tokenIssuer.ts`).
|
|
6
6
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* RS256 signing-key loader for the Storefront App Gateway
|
|
3
|
-
*
|
|
4
|
-
* JWTs a private app presents to the
|
|
2
|
+
* RS256 signing-key loader for the Storefront App Gateway — the key that mints
|
|
3
|
+
* and verifies the OAuth2 client-credentials
|
|
4
|
+
* JWTs a private app presents to the scoped APIs.
|
|
5
5
|
*
|
|
6
6
|
* Mirrors `../auth/brokerAssertion.ts`'s `loadSigningKey` caching + `readEnv`
|
|
7
7
|
* usage EXACTLY, but over a SEPARATE key (`JWT_APP_GATEWAY_PRIVATE_KEY_B64`):
|
|
@@ -34,7 +34,7 @@ export const DEFAULT_APP_GATEWAY_ISSUER = "storefront-app-gateway";
|
|
|
34
34
|
export const APP_TOKEN_TTL_SECONDS = 300;
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Widget launch-token lifetime
|
|
37
|
+
* Widget launch-token lifetime — shorter than
|
|
38
38
|
* {@link APP_TOKEN_TTL_SECONDS}: this token only needs to survive the SSR
|
|
39
39
|
* render → iframe-load round trip, never a standing API session.
|
|
40
40
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Admin Health view aggregation
|
|
2
|
+
* Admin Health view aggregation. Folds
|
|
3
3
|
* one `AppInstall` (`registryService.ts`) and its `InstallHealth` rollup
|
|
4
|
-
* (`../webhooks/deliveryStore.ts`'s `getInstallHealth`,
|
|
4
|
+
* (`../webhooks/deliveryStore.ts`'s `getInstallHealth`, including
|
|
5
5
|
* `p95DeliveryMs`) into the single `AppInstallHealthView` shape the admin
|
|
6
6
|
* console renders. Deliberately transport-free — no D1/fetch here, so it's
|
|
7
7
|
* unit-testable with fakes; the caller (the admin health route) owns
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* a raw URL here, only as `hasWebhookEndpoint` presence — the console shows
|
|
12
12
|
* "configured or not", not the app's receiver address.
|
|
13
13
|
*
|
|
14
|
-
* `runtimeMode` is hardcoded to `"external"
|
|
15
|
-
* runtime-mode column
|
|
16
|
-
* an in-platform runtime mode is
|
|
14
|
+
* `runtimeMode` is hardcoded to `"external"`: there is no per-install
|
|
15
|
+
* runtime-mode column (every install IS an externally-hosted app;
|
|
16
|
+
* an in-platform runtime mode is out of scope).
|
|
17
17
|
*/
|
|
18
18
|
import { validateWebhookEndpoint } from "../webhooks/endpointGuard.js";
|
|
19
19
|
import type { InstallHealth } from "../webhooks/deliveryStore.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* App-owned attribution/commission write model
|
|
3
|
-
*
|
|
2
|
+
* App-owned attribution/commission write model. Owns the
|
|
3
|
+
* `app_attributions` rows `schema.ts` declares: the
|
|
4
4
|
* ledger `POST /attribution` (`docs/private-apps/contract/openapi.yaml`'s
|
|
5
5
|
* `writeAttribution`) writes into. Writes only into the app's own
|
|
6
6
|
* attribution namespace — this service never mutates `orders`, it only
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Opaque customer hash —
|
|
2
|
+
* Opaque customer hash — the `customerHash`
|
|
3
3
|
* every order/attribution surface exposes instead of a raw customer id (see
|
|
4
4
|
* `docs/private-apps/api-event-reference.md`'s PII-minimization matrix and
|
|
5
5
|
* `contract/openapi.yaml`'s `Order.customerHash`).
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* FoxyOrderClient — the boundary to tot-foxycart's order-read surface
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* FoxyOrderClient — the boundary to tot-foxycart's order-read surface.
|
|
3
|
+
* `ordersStore.ts` and the read routes / event receiver never call `fetch`
|
|
4
|
+
* directly; they go through this seam so a fake can stand in for tot-foxycart
|
|
5
|
+
* in tests.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
7
|
+
* The service is `tot-foxycart/routes/foxyOrderApi.js` +
|
|
8
|
+
* `modules/integrations/foxyOrderMapper.node.js`:
|
|
9
9
|
* a `GET {baseUrl}/commerce/orders?appDomain=&limit=&offset=&since=&until=&status=`
|
|
10
10
|
* list endpoint returning `{ orders, limit, offset, total, hasMore }` (limit/offset,
|
|
11
|
-
* NOT page/pageSize
|
|
11
|
+
* NOT page/pageSize), and a
|
|
12
12
|
* `GET {baseUrl}/commerce/orders/{externalOrderId}?appDomain=` detail endpoint
|
|
13
|
-
* returning `{ order }` (an envelope, not the bare order
|
|
13
|
+
* returning `{ order }` (an envelope, not the bare order),
|
|
14
14
|
* both Bearer-authenticated with the tenant's Foxy partner API key. `FoxyOrder`
|
|
15
|
-
* below
|
|
15
|
+
* below mirrors `buildOrderPayload()`'s actual normalized DTO — NOT the
|
|
16
16
|
* contract's minimized `Order` shape (`contract/openapi.yaml`); mapping one to
|
|
17
17
|
* the other is `orderMapper.ts`'s job.
|
|
18
18
|
*
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
* factory shape and `../../webhooks/deliveryEngine.ts`'s bounded-timeout
|
|
21
21
|
* `fetch` (AbortController, injectable `fetchImpl`) — but adds a settable
|
|
22
22
|
* test seam (`setFoxyOrderClientForTesting`) since this client, unlike
|
|
23
|
-
* `messagesClient.ts`, is a shared singleton multiple
|
|
24
|
-
* routes, event receiver)
|
|
23
|
+
* `messagesClient.ts`, is a shared singleton that multiple callers (read
|
|
24
|
+
* routes, event receiver) each independently resolve.
|
|
25
25
|
*/
|
|
26
26
|
import { readEnv } from "@/lib/env";
|
|
27
27
|
|
|
@@ -174,7 +174,7 @@ export class HttpFoxyOrderClient implements FoxyOrderClient {
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
// --- Test seam ---------------------------------------------------------------
|
|
177
|
-
//
|
|
177
|
+
// Callers (read routes, event receiver) each resolve their own client
|
|
178
178
|
// instance via `getFoxyOrderClient()`; a module-level override lets a single
|
|
179
179
|
// `setFoxyOrderClientForTesting` call stand in for tot-foxycart across all of
|
|
180
180
|
// them in one test, without threading a fake through every call site.
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Idempotency-Key ledger
|
|
3
|
-
* `idempotency_keys` table `schema.ts` declares: the
|
|
2
|
+
* Idempotency-Key ledger. Backs the
|
|
3
|
+
* `idempotency_keys` table `schema.ts` declares: the one real (not
|
|
4
4
|
* presence-only) idempotency ledger in this codebase, and the one
|
|
5
5
|
* `POST /attribution` (`docs/private-apps/contract/openapi.yaml`'s
|
|
6
6
|
* `writeAttribution`) requires — a reused key with a body hash that doesn't
|
|
7
7
|
* match the one first stored is a 409, not a silent overwrite or a second
|
|
8
|
-
* row. Contrast
|
|
8
|
+
* row. Contrast the webhook replay endpoint (`webhooks/deliveries/{id}/replay.ts`),
|
|
9
9
|
* which only checks the header's presence because it has no request body to
|
|
10
|
-
* hash and no table backing it
|
|
10
|
+
* hash and no table backing it.
|
|
11
11
|
*
|
|
12
|
-
* Pure over the `Queryable` seam, same shape every other
|
|
13
|
-
* Route-agnostic by design: `attribution.ts` is
|
|
14
|
-
*
|
|
12
|
+
* Pure over the `Queryable` seam, same shape every other orders store uses.
|
|
13
|
+
* Route-agnostic by design: `attribution.ts` is one caller, but any
|
|
14
|
+
* idempotent write route can reuse this.
|
|
15
15
|
*/
|
|
16
16
|
import { sha256Hex } from "@tot/public-runtime";
|
|
17
17
|
import type { Queryable } from "../../d1/catalog.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Inbound order-forward receiver
|
|
2
|
+
* Inbound order-forward receiver. The
|
|
3
3
|
* transport-free core `pages/api/apps/internal/order-forward.ts` calls once
|
|
4
4
|
* it has verified the HMAC signature: given one reconciled order from
|
|
5
5
|
* tot-foxycart, upsert it into `ordersStore.ts` and decide whether that write
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure mapper: `FoxyOrderClient`'s `FoxyOrder` (tot-foxycart's normalized
|
|
3
|
-
* transaction DTO) -> the contract's minimized `Order` shape
|
|
4
|
-
*
|
|
3
|
+
* transaction DTO) -> the contract's minimized `Order` shape
|
|
4
|
+
* (`docs/private-apps/contract/openapi.yaml`'s `Order`).
|
|
5
5
|
*
|
|
6
6
|
* Drops everything the contract's `additionalProperties: false` Order
|
|
7
7
|
* doesn't declare — no raw checkout payload, no signed-cart material, no tax
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `orders` read-model store
|
|
3
|
-
* `orders` rows `schema.ts` declares: the diffing surface
|
|
4
|
-
*
|
|
2
|
+
* `orders` read-model store. Owns the
|
|
3
|
+
* `orders` rows `schema.ts` declares: the diffing surface the event emission
|
|
4
|
+
* reads (`upsertOrder`'s returned `previousStatus` is how it
|
|
5
5
|
* detects a created→fulfilled transition without a second query) and the
|
|
6
|
-
* lookup surface the attribution route
|
|
6
|
+
* lookup surface the attribution route uses for its cross-tenant
|
|
7
7
|
* ownership check.
|
|
8
8
|
*
|
|
9
9
|
* Pure over the `Queryable` seam (see `../../d1/catalog.ts`) — same shape
|
|
@@ -54,8 +54,8 @@ export interface UpsertOrderInput {
|
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
56
|
* `upsertOrder`'s return: the row's status BEFORE this write (`null` for a
|
|
57
|
-
* brand-new order) alongside the row as it now stands — the diffing signal
|
|
58
|
-
*
|
|
57
|
+
* brand-new order) alongside the row as it now stands — the diffing signal the
|
|
58
|
+
* event emission needs to tell "just created" apart from
|
|
59
59
|
* "created → fulfilled" apart from "no-op re-delivery of the same status",
|
|
60
60
|
* without a second round trip.
|
|
61
61
|
*/
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* App registry service
|
|
2
|
+
* App registry service. Owns the
|
|
3
3
|
* `app_installs` / `app_audit_log` rows described in `schema.ts` and
|
|
4
4
|
* orchestrates credential lifecycle through the {@link CredentialPort} seam
|
|
5
|
-
* (
|
|
5
|
+
* (the credential store implements it; this service depends only on the interface, mirroring
|
|
6
6
|
* how `D1VersionStore` implements `@tot/private-controlplane`'s `VersionStore`
|
|
7
7
|
* contract without either side importing the other's internals).
|
|
8
8
|
*
|
|
@@ -131,7 +131,7 @@ export interface IssuedCredential {
|
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
133
|
* `IssuedCredential` plus the one-time plaintext secret. `issue`/`rotate` mint
|
|
134
|
-
* the secret, hash it for storage (
|
|
134
|
+
* the secret, hash it for storage (`sha256Hex`), and hand the
|
|
135
135
|
* plaintext back exactly once here — there is no "show secret again" path by
|
|
136
136
|
* design, so a caller that discards this return has lost it for good (see
|
|
137
137
|
* `AppRegistryService.install`'s `InstallResult`, which is why this seam
|
|
@@ -140,10 +140,10 @@ export interface IssuedCredential {
|
|
|
140
140
|
export type IssuedCredentialSecret = IssuedCredential & { secret: string };
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
|
-
* Minimal seam for the credential lifecycle
|
|
143
|
+
* Minimal seam for the credential lifecycle the credential store
|
|
144
144
|
* implements against `app_credentials`. `AppRegistryService` orchestrates
|
|
145
145
|
* through this interface only — it never touches credential storage directly,
|
|
146
|
-
* so
|
|
146
|
+
* so it can be tested with a fake.
|
|
147
147
|
*/
|
|
148
148
|
export interface CredentialPort {
|
|
149
149
|
/** Issue a fresh client-credential set for a newly installed app. */
|
|
@@ -155,7 +155,7 @@ export interface CredentialPort {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
/**
|
|
158
|
-
* The 3 platform-owned widget slots
|
|
158
|
+
* The 3 platform-owned widget slots. Mirrors the `widgets[].placement`
|
|
159
159
|
* enum in `docs/private-apps/contract/tot-app.schema.json` — widgets render in a
|
|
160
160
|
* platform-managed sandboxed iframe at one of these fixed slots, never as
|
|
161
161
|
* merchant-pasted script at an app-chosen location.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Scope validation — the compliance-floor gate for Storefront Private Apps
|
|
3
|
-
*
|
|
2
|
+
* Scope validation — the compliance-floor gate for Storefront Private Apps.
|
|
3
|
+
* See `docs/private-apps/feature-brief.md` §"The
|
|
4
4
|
* compliance floor" and the machine-readable contract:
|
|
5
5
|
* - `docs/private-apps/contract/scopes.json` — the 8-scope V1 catalog.
|
|
6
6
|
* - `docs/private-apps/contract/forbidden-scopes.json` — the 8 ids that map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OAuth2 client-credentials token issuer — the Storefront App Gateway's
|
|
3
|
-
* `POST /oauth/token` grant
|
|
4
|
-
* short-lived RS256 JWT a private app presents as a bearer token to the
|
|
3
|
+
* `POST /oauth/token` grant. Mints the
|
|
4
|
+
* short-lived RS256 JWT a private app presents as a bearer token to the
|
|
5
5
|
* scoped APIs.
|
|
6
6
|
*
|
|
7
7
|
* `tenant` and `env` are supplied by the CALLER (the route, from
|
|
@@ -97,8 +97,8 @@ export class AppTokenIssuer {
|
|
|
97
97
|
tenant: install.tenantId,
|
|
98
98
|
env: install.env,
|
|
99
99
|
scopes: install.scopes,
|
|
100
|
-
// Discriminates this bearer token from a widget launch token
|
|
101
|
-
//
|
|
100
|
+
// Discriminates this bearer token from a widget launch token so the two
|
|
101
|
+
// can never be replayed for each other even though both
|
|
102
102
|
// are RS256 JWTs signed by the same gateway key — see tokenVerifier.ts.
|
|
103
103
|
token_use: "access",
|
|
104
104
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Offline verification of the Storefront App Gateway's own OAuth2
|
|
3
|
-
* client-credentials JWT
|
|
4
|
-
* imports
|
|
3
|
+
* client-credentials JWT — this is what the scoped-API boundary
|
|
4
|
+
* imports to authorize an app's request.
|
|
5
5
|
*
|
|
6
6
|
* Modeled directly on `../auth/identityToken.ts`'s `IdentityAssertionVerifier`
|
|
7
7
|
* (same `jwtVerify` + JWKS-by-`kid` shape, `aud`/`iss` pinning, alg allowlist),
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
} from "./gatewayKeys.js";
|
|
26
26
|
import { readEnv } from "@/lib/env";
|
|
27
27
|
|
|
28
|
-
/** The verified app-token claims (
|
|
28
|
+
/** The verified app-token claims (the scoped API's authorization input). */
|
|
29
29
|
export interface AppTokenClaims {
|
|
30
30
|
iss: string;
|
|
31
31
|
sub: string; // install id
|
|
@@ -40,8 +40,8 @@ export interface AppTokenClaims {
|
|
|
40
40
|
|
|
41
41
|
/** Normalize a jose JWT payload into typed {@link AppTokenClaims}, throwing on any missing claim. */
|
|
42
42
|
function toAppTokenClaims(payload: Record<string, unknown>): AppTokenClaims {
|
|
43
|
-
// The security-critical discriminator: a widget launch token
|
|
44
|
-
// `token_use: "widget_launch"`) is signed by this SAME gateway key, so
|
|
43
|
+
// The security-critical discriminator: a widget launch token
|
|
44
|
+
// (`token_use: "widget_launch"`) is signed by this SAME gateway key, so
|
|
45
45
|
// signature/aud/iss/exp checks alone can't tell it apart from a bearer
|
|
46
46
|
// token. Requiring `token_use === "access"` here is what stops a launch
|
|
47
47
|
// token — minted for a sandboxed iframe, short-TTL, no scopes checked —
|
|
@@ -128,7 +128,7 @@ export class AppTokenVerifier {
|
|
|
128
128
|
/**
|
|
129
129
|
* Get (or build) the verifier for one tenant, from the gateway's own
|
|
130
130
|
* locally-derived JWKS. Returns `null` when the signing key isn't configured
|
|
131
|
-
* — callers (
|
|
131
|
+
* — callers (the scoped-API routes) fail closed with 503, mirroring `getIdentityVerifier`.
|
|
132
132
|
* Not cached across calls (unlike `identityToken.ts`'s remote-fetch verifier):
|
|
133
133
|
* deriving the local JWKS is cheap (`gatewayKeys.ts` caches the export itself).
|
|
134
134
|
*/
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Widget eligibility
|
|
2
|
+
* Widget eligibility. Mirrors
|
|
3
3
|
* `webhooks/emit.ts`'s `isEligibleTarget` discipline: an install is eligible
|
|
4
4
|
* to render its widget at `slot` only while it is LIVE — `active`, scoped for
|
|
5
5
|
* `widgets:launch`, and carrying a validated placement for that slot. This
|
|
6
6
|
* live check is what makes suspending an install actually stop it rendering
|
|
7
|
-
* on the very next page load — the
|
|
7
|
+
* on the very next page load — the "suspension kills widgets"
|
|
8
8
|
* enforcement — with no separate revoke step; it's just this gate re-run on
|
|
9
9
|
* every request.
|
|
10
10
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure prop-building for `<AppWidgetFrame
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Pure prop-building for `<AppWidgetFrame>`. The query-param handoff and the
|
|
3
|
+
* sandbox attrs live here (not in the `.astro` component) so they are
|
|
4
|
+
* unit-testable: `vitest.config.ts` deliberately does NOT
|
|
5
5
|
* render `.astro` components (Astro Container API breaks under vitest@2 +
|
|
6
6
|
* astro@7 — see that file's docstring), so render *logic* lives here and
|
|
7
7
|
* rendered output is verified on a live preview instead.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Widget launch-token minting
|
|
2
|
+
* Widget launch-token minting. Mints the
|
|
3
3
|
* short-lived RS256 JWT a platform-managed sandboxed widget iframe carries at
|
|
4
|
-
* SSR render time:
|
|
4
|
+
* SSR render time: the page-render path calls `mintWidgetLaunchToken` while rendering the
|
|
5
5
|
* page for each declared placement, and the resulting token is what the
|
|
6
6
|
* widget's own endpoint receives as proof it was launched by THIS platform,
|
|
7
7
|
* for THIS install, at THIS placement — never a merchant-forged or replayed
|
|
@@ -31,10 +31,10 @@ import {
|
|
|
31
31
|
export interface MintWidgetLaunchTokenInput {
|
|
32
32
|
/**
|
|
33
33
|
* Only `installId`/`tenantId`/`env` are read. Narrowed from `AppInstall` to
|
|
34
|
-
* this `Pick` so
|
|
34
|
+
* this `Pick` so the render path — which has `placements.ts`'s
|
|
35
35
|
* `ResolvedPlacement` (installId/appOrigin/endpoint), not the full
|
|
36
36
|
* `AppInstall` — can build the minimal object itself instead of re-fetching
|
|
37
|
-
* the install; a full `AppInstall` still satisfies this shape
|
|
37
|
+
* the install; a full `AppInstall` still satisfies this shape.
|
|
38
38
|
*/
|
|
39
39
|
install: Pick<AppInstall, "installId" | "tenantId" | "env">;
|
|
40
40
|
slot: WidgetPlacement;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Widget placement resolution
|
|
3
|
-
* apps have a live widget at this slot" into the concrete list
|
|
2
|
+
* Widget placement resolution. Turns "which
|
|
3
|
+
* apps have a live widget at this slot" into the concrete list the render path needs
|
|
4
4
|
* to render sandboxed iframes and mint launch tokens: reads
|
|
5
5
|
* `registry.listByTenant`, filters to the requested `env` and
|
|
6
6
|
* {@link isWidgetEligible}, then reads each eligible install's `endpoint`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Widget slot render composition
|
|
2
|
+
* Widget slot render composition. The one
|
|
3
3
|
* call a page/layout makes to turn "render slot X for this request" into the
|
|
4
4
|
* concrete, launch-token-bearing list `AppWidgetFrame` needs: resolves live
|
|
5
5
|
* placements (`placements.ts`), mints each one a launch token
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Admin session-entry resolution
|
|
2
|
+
* Admin session-entry resolution — the SERVER-SIDE decision that puts
|
|
3
3
|
* an authenticated owner/team member into the CORRECT tenant admin context, and
|
|
4
4
|
* keeps a non-member / unauthenticated visitor on the legitimate sign-in gate.
|
|
5
5
|
*
|
|
6
|
-
* NARROW to auth/session ENTRY: it CONSUMES the
|
|
7
|
-
* (`pages/admin.astro` + `components/admin/AdminPublishTab.astro`) and
|
|
6
|
+
* NARROW to auth/session ENTRY: it CONSUMES the admin tab UX
|
|
7
|
+
* (`pages/admin.astro` + `components/admin/AdminPublishTab.astro`) and the
|
|
8
8
|
* ship-capability model (`change/shipCapabilities` + `change/route` decideIsOwner)
|
|
9
9
|
* rather than reimplementing either. Everything it resolves is bound to the
|
|
10
10
|
* RESOLVED tenant, never taken from client input, and never requires a
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
* - `hostCapability` — the viewer's owner/admin/member standing FOR THIS host,
|
|
15
15
|
* from {@link sessionHostCapability} (tenants[] membership → resource-scoped
|
|
16
16
|
* session → live staff selection). null ⇒ not a member of this tenant.
|
|
17
|
-
* - `isOwnerViewer` / `shipCapabilities` —
|
|
17
|
+
* - `isOwnerViewer` / `shipCapabilities` — {@link decideIsOwner} +
|
|
18
18
|
* {@link resolveShipCapabilities} over that host-bound capability. This is the
|
|
19
|
-
* OWNER VIEWER identity
|
|
19
|
+
* OWNER VIEWER identity:
|
|
20
20
|
* a signed-in owner/admin viewer over the resolved tenant, NOT the bearer
|
|
21
21
|
* operator console. The ship-axis `capability` is the fail-closed
|
|
22
22
|
* `preview-access` — a viewer session carries no operator ship grant, so the
|
|
23
23
|
* owner is the ONLY source of `shipOnBehalf`/`apexCutover` here.
|
|
24
24
|
*
|
|
25
|
-
* The distinguishing logic
|
|
25
|
+
* The distinguishing logic: `admitted`
|
|
26
26
|
* separates an authenticated OWNER/member (a host-bound capability, or ToT-staff
|
|
27
27
|
* scope) — who must NEVER see the "Coming soon" holding page at their own admin —
|
|
28
28
|
* from an authenticated NON-member and an anonymous visitor, who legitimately do
|
|
@@ -56,9 +56,9 @@ export interface AdminEntryDecision {
|
|
|
56
56
|
admitted: boolean;
|
|
57
57
|
/** The owner/admin/member standing bound to this host, or null. */
|
|
58
58
|
hostCapability: string | null;
|
|
59
|
-
/**
|
|
59
|
+
/** The acting viewer IS the tenant owner/admin (superset of ship-on-behalf). */
|
|
60
60
|
isOwnerViewer: boolean;
|
|
61
|
-
/**
|
|
61
|
+
/** The two derived ship-gate capabilities for this viewer entry. */
|
|
62
62
|
shipCapabilities: ShipCapabilities;
|
|
63
63
|
/** Which of the four entry cases this is. */
|
|
64
64
|
principal: AdminEntryPrincipal;
|
|
@@ -90,7 +90,7 @@ export function resolveAdminEntry(input: {
|
|
|
90
90
|
const staffScope = Array.isArray(record?.staff) && record!.staff.length > 0;
|
|
91
91
|
const admitted = hostCapability !== null || staffScope;
|
|
92
92
|
|
|
93
|
-
//
|
|
93
|
+
// Owner resolution — the OWNER VIEWER, not the bearer console: authorized
|
|
94
94
|
// (a member/staff viewer of THIS tenant) AND holding an owner/admin capability.
|
|
95
95
|
const isOwnerViewer = decideIsOwner({
|
|
96
96
|
authorized: admitted,
|