@tokenoftrust/storefront-runner 1.4.0 → 1.4.2-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/storefront/astro.config.mjs +17 -1
- package/apps/storefront/integrations/materialize-guard.mjs +56 -0
- package/apps/storefront/migrations-apps/0001_woozy_lyja.sql +22 -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/0001_snapshot.json +990 -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 +21 -0
- package/apps/storefront/package.json +0 -1
- 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 +1756 -1382
- 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 +426 -0
- package/apps/storefront/src/lib/activity/changeActorAttribution.ts +85 -0
- package/apps/storefront/src/lib/activity/deployVersion.ts +120 -0
- package/apps/storefront/src/lib/activity/ingest.ts +188 -0
- package/apps/storefront/src/lib/activity/ingestAuth.ts +105 -0
- package/apps/storefront/src/lib/activity/killSwitch.ts +80 -0
- package/apps/storefront/src/lib/activity/query.ts +403 -0
- package/apps/storefront/src/lib/activity/recordActivity.ts +101 -0
- package/apps/storefront/src/lib/activity/store.ts +120 -0
- package/apps/storefront/src/lib/activity/uiActor.ts +150 -0
- package/apps/storefront/src/lib/activity/workerCommit.ts +70 -0
- 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 +6 -4
- package/apps/storefront/src/lib/auth/operatorSession.ts +113 -0
- package/apps/storefront/src/lib/auth/route.ts +44 -4
- 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 +147 -14
- 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/env.ts +51 -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 -42
- 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 +130 -2
- package/apps/storefront/src/pages/admin/ops-timeline.astro +420 -0
- package/apps/storefront/src/pages/admin.astro +52 -1206
- package/apps/storefront/src/pages/api/activity.ts +137 -0
- package/apps/storefront/src/pages/api/admin/activity-alerts.ts +73 -0
- package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +27 -1
- package/apps/storefront/src/pages/api/apps/admin/install.ts +26 -1
- package/apps/storefront/src/pages/api/apps/admin/resume.ts +24 -1
- package/apps/storefront/src/pages/api/apps/admin/suspend.ts +24 -1
- package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +25 -1
- package/apps/storefront/src/pages/api/apps/admin/update.ts +24 -1
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +11 -0
- package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +11 -0
- package/apps/storefront/src/pages/api/apps/v1/attribution.ts +20 -0
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +12 -0
- package/apps/storefront/src/pages/api/blog/[collection]/[slug]/react.ts +104 -0
- package/apps/storefront/src/pages/api/cache-purge.ts +11 -0
- package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +30 -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/auth/login.astro +5 -4
- 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 +74 -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/README.md +146 -0
- package/packages/public-runtime/src/activity/catalog.ts +553 -0
- package/packages/public-runtime/src/activity/event.ts +168 -0
- package/packages/public-runtime/src/activity/index.ts +16 -0
- package/packages/public-runtime/src/activity/redaction.ts +263 -0
- package/packages/public-runtime/src/candidate-index.ts +13 -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 +7 -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/webhooks/signing.ts +0 -29
- package/apps/storefront/src/lib/webhooks/webhookSigningKey.ts +0 -146
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared emit helper for UI-ORIGINATED operational activity.
|
|
3
|
+
*
|
|
4
|
+
* The UI cohort's entry point onto the actor×action telemetry contract. It sits
|
|
5
|
+
* ON TOP of the server emitter ({@link recordActivity}) — every admin/cockpit action
|
|
6
|
+
* handler already runs SERVER-SIDE in the same Worker, so a UI action reaches
|
|
7
|
+
* telemetry through the same in-process `recordActivity()` call the server path uses (one
|
|
8
|
+
* structured Workers-Logs line), NOT a client-side `fetch` to the ingest endpoint.
|
|
9
|
+
* There is no browser-only signal to capture: an admin/cockpit "Publish / Connect /
|
|
10
|
+
* Invite / Approve / Rollback" click is a server-rendered form/JSON POST to an API
|
|
11
|
+
* route, so the route handler that already does the work is exactly where the emit
|
|
12
|
+
* belongs (zero extra HTTP round-trip, no new auth/trust boundary). The HMAC
|
|
13
|
+
* ingest path exists for emitters that CAN'T call `recordActivity()` directly (the
|
|
14
|
+
* out-of-process CLI cohort) — it is not this cohort's path.
|
|
15
|
+
*
|
|
16
|
+
* WHAT THIS ADDS over calling `recordActivity()` raw: it stamps `source: "ui"` and,
|
|
17
|
+
* critically, resolves the ACTOR KIND from the authenticated session so the timeline
|
|
18
|
+
* can attribute an action to the right cohort:
|
|
19
|
+
*
|
|
20
|
+
* • "admin" — a ToT-STAFF viewer (the session carries `staff[]` scope). ToT
|
|
21
|
+
* operators acting through the admin shell / staff vendor selection.
|
|
22
|
+
* • "merchant" — a store OWNER acting on THEIR OWN store (a signed-in viewer whose
|
|
23
|
+
* host-bound capability is owner/admin). The self-serve cohort.
|
|
24
|
+
* • "dev" — a signed-in DEVELOPER viewer who is neither staff nor owner (e.g. a
|
|
25
|
+
* ship-on-behalf developer invited onto someone else's store).
|
|
26
|
+
* • "agent" — a HEADLESS, capability-driven caller with no human viewer session
|
|
27
|
+
* (the Bearer/CLI operator seam) — automation acting on the surface.
|
|
28
|
+
*
|
|
29
|
+
* The classifier is a PURE function ({@link classifyActorKind}) so the cohort mapping
|
|
30
|
+
* is unit-tested without a live request. The opaque `actor.id` is the salted,
|
|
31
|
+
* non-reversible hash of the acting identity (never raw PII in the core zone) — the
|
|
32
|
+
* SAME {@link hashActorId} + `ACTIVITY_ACTOR_SALT` the server path uses, so one person stays
|
|
33
|
+
* correlatable across the CLI, server, and UI cohorts.
|
|
34
|
+
*
|
|
35
|
+
* USAGE (mirrors reject.ts — resolve the actor ONCE after auth, emit at each
|
|
36
|
+
* exit point, zero change to the handler's control flow):
|
|
37
|
+
*
|
|
38
|
+
* const actor = await resolveUiActor(context, { owner: auth.owner });
|
|
39
|
+
* // …do the work…
|
|
40
|
+
* recordUiActivity(actor, { action: "change.ship", outcome: { status: "succeeded" }, scope });
|
|
41
|
+
*/
|
|
42
|
+
import type { APIContext } from "astro";
|
|
43
|
+
import type {
|
|
44
|
+
ActivityActor,
|
|
45
|
+
ActorKind,
|
|
46
|
+
ActivityOutcome,
|
|
47
|
+
ActivityScope,
|
|
48
|
+
ActivityPayload,
|
|
49
|
+
} from "@tot/public-runtime";
|
|
50
|
+
import type { ActionKey } from "@tot/public-runtime";
|
|
51
|
+
import { readViewerSession } from "@/lib/auth/route";
|
|
52
|
+
import { isStaffSession } from "@/lib/dashboard/staffAdmission";
|
|
53
|
+
import { readEnv } from "@/lib/env";
|
|
54
|
+
import { hashActorId, recordActivity } from "./recordActivity";
|
|
55
|
+
|
|
56
|
+
/** The host-bound capabilities that mark a viewer as the store OWNER (merchant). */
|
|
57
|
+
const OWNER_CAPABILITIES = new Set(["owner", "admin"]);
|
|
58
|
+
|
|
59
|
+
/** The minimal viewer chrome the middleware stamps on `locals.viewer`. */
|
|
60
|
+
interface ViewerLike {
|
|
61
|
+
email?: string;
|
|
62
|
+
capability?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Classify the acting cohort from the authenticated session — PURE so the mapping is
|
|
67
|
+
* testable without a request. Order matters and is fail-safe:
|
|
68
|
+
*
|
|
69
|
+
* 1. no signed-in viewer → "agent" (headless Bearer/CLI operator — capability-
|
|
70
|
+
* driven automation; there is no human identity on this request).
|
|
71
|
+
* 2. ToT-staff session → "admin" (checked BEFORE ownership: a staff viewer who
|
|
72
|
+
* entered a vendor via a live selection carries an owner-ish host capability, but
|
|
73
|
+
* they are ToT staff, not the merchant).
|
|
74
|
+
* 3. owner/admin capability on this host → "merchant" (the store owner on their own store).
|
|
75
|
+
* 4. otherwise → "dev" (a signed-in developer without ownership, e.g.
|
|
76
|
+
* ship-on-behalf).
|
|
77
|
+
*/
|
|
78
|
+
export function classifyActorKind(
|
|
79
|
+
viewer: ViewerLike | null | undefined,
|
|
80
|
+
session: { staff?: string[] } | null | undefined,
|
|
81
|
+
): ActorKind {
|
|
82
|
+
if (!viewer?.email) return "agent";
|
|
83
|
+
if (isStaffSession(session)) return "admin";
|
|
84
|
+
if (viewer.capability && OWNER_CAPABILITIES.has(viewer.capability)) return "merchant";
|
|
85
|
+
return "dev";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ResolveUiActorInput {
|
|
89
|
+
/**
|
|
90
|
+
* Fallback acting identity for the HEADLESS path (no signed-in viewer) — typically
|
|
91
|
+
* the resolved tenant owner (`auth.owner`). Only used to seed the opaque id when
|
|
92
|
+
* there is no viewer email; never stored raw (always hashed).
|
|
93
|
+
*/
|
|
94
|
+
owner?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve the {@link ActivityActor} for a UI-originated action from the request's
|
|
99
|
+
* authenticated session. Reads the full viewer session ONCE (for the staff scope the
|
|
100
|
+
* host-bound `locals.viewer` chrome doesn't carry) and computes the opaque, salted
|
|
101
|
+
* actor id. Best-effort: any read fault degrades to a coarse non-PII actor rather
|
|
102
|
+
* than throwing — telemetry must never break the action it describes. Resolve this
|
|
103
|
+
* ONCE per request (after auth) and reuse the result across every emit exit point.
|
|
104
|
+
*/
|
|
105
|
+
export async function resolveUiActor(
|
|
106
|
+
context: APIContext,
|
|
107
|
+
input: ResolveUiActorInput = {},
|
|
108
|
+
): Promise<ActivityActor> {
|
|
109
|
+
// The host-bound `locals.viewer` chrome omits `staff[]`, so read the full session
|
|
110
|
+
// to tell a ToT-staff (admin) actor from a store owner (merchant). Best-effort.
|
|
111
|
+
const session = await readViewerSession(context).catch(() => null);
|
|
112
|
+
// Prefer the host-scoped `locals.viewer` (set by middleware for tenant-gated
|
|
113
|
+
// surfaces — the ship/grants routes); fall back to the raw session's
|
|
114
|
+
// email/capability for a route that never went through that host gate (e.g. the
|
|
115
|
+
// /dev cockpit's rendezvous-approval endpoint) but still has a real signed-in
|
|
116
|
+
// viewer. Both describe the SAME signed-in-or-not shape.
|
|
117
|
+
const viewer: ViewerLike | undefined =
|
|
118
|
+
(context.locals?.viewer as ViewerLike | undefined) ??
|
|
119
|
+
(session ? { email: session.email, capability: session.capability } : undefined);
|
|
120
|
+
const kind = classifyActorKind(viewer, session);
|
|
121
|
+
const identifier = viewer?.email ?? input.owner ?? "system";
|
|
122
|
+
const id = await hashActorId(identifier, await readEnv("ACTIVITY_ACTOR_SALT"));
|
|
123
|
+
return { kind, id };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Emit ONE UI-originated activity event with a PRE-RESOLVED actor. A thin, SYNC
|
|
128
|
+
* wrapper over {@link recordActivity} that stamps `source: "ui"` — so a handler
|
|
129
|
+
* calls it at each exit point (succeeded / failed / refused) exactly as the server
|
|
130
|
+
* emitters do, with no control-flow change. Best-effort + never throws (inherited
|
|
131
|
+
* from `recordActivity`).
|
|
132
|
+
*/
|
|
133
|
+
export function recordUiActivity(
|
|
134
|
+
actor: ActivityActor,
|
|
135
|
+
input: {
|
|
136
|
+
action: ActionKey;
|
|
137
|
+
outcome: ActivityOutcome;
|
|
138
|
+
scope?: ActivityScope;
|
|
139
|
+
payload?: ActivityPayload;
|
|
140
|
+
},
|
|
141
|
+
): void {
|
|
142
|
+
recordActivity({
|
|
143
|
+
action: input.action,
|
|
144
|
+
actor,
|
|
145
|
+
source: "ui",
|
|
146
|
+
outcome: input.outcome,
|
|
147
|
+
...(input.scope ? { scope: input.scope } : {}),
|
|
148
|
+
...(input.payload ? { payload: input.payload } : {}),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worker_commit — build provenance for the running Worker.
|
|
3
|
+
*
|
|
4
|
+
* The commit SHA THIS Worker build was cut from, made available REPO-WIDE at
|
|
5
|
+
* runtime so every server-emitted ActivityEvent can carry it (see event.ts
|
|
6
|
+
* `ActivityScope.workerCommit`) and the query surface can derive **deploy skew** — "is the
|
|
7
|
+
* build serving this request == the pushed umbrella tip, or is a stale worker
|
|
8
|
+
* still live?".
|
|
9
|
+
*
|
|
10
|
+
* HOW IT GETS HERE. Workers have no filesystem/git at runtime, so the SHA is
|
|
11
|
+
* baked in at BUILD time: astro.config.mjs's Vite `define` replaces the
|
|
12
|
+
* `__GIT_SHA__` token with a string literal of the build's commit (CI-provided
|
|
13
|
+
* `GIT_SHA`/`WORKERS_CI_COMMIT_SHA`/`GITHUB_SHA`, else local `git rev-parse`,
|
|
14
|
+
* else "unknown"). This is the SAME source /health already reports (lib/health.ts)
|
|
15
|
+
* — we read it through the identical `typeof` guard so both agree by construction,
|
|
16
|
+
* and so the value is safe under vitest (where the define isn't applied → "unknown").
|
|
17
|
+
*
|
|
18
|
+
* This is the single accessor the whole repo threads through: recordActivity()
|
|
19
|
+
* spreads serverActivityScope() into every server event; the deploy.version_observed
|
|
20
|
+
* emitter (deployVersion.ts) reads workerCommit() directly.
|
|
21
|
+
*/
|
|
22
|
+
import type { ActivityScope } from "@tot/public-runtime";
|
|
23
|
+
|
|
24
|
+
/** Sentinel when the build had no git context (also the vitest value). */
|
|
25
|
+
export const UNKNOWN_COMMIT = "unknown";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The commit SHA this Worker build was cut from, or "unknown". Never throws; the
|
|
29
|
+
* `typeof` guard keeps it safe under vitest (the `__GIT_SHA__` define is only
|
|
30
|
+
* applied by the Astro/Vite build). Optionally overridable for tests.
|
|
31
|
+
*/
|
|
32
|
+
export function workerCommit(override?: string): string {
|
|
33
|
+
if (override != null) return override;
|
|
34
|
+
return typeof __GIT_SHA__ !== "undefined" ? __GIT_SHA__ : UNKNOWN_COMMIT;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** First 7 chars of the build commit for eyeballing against `git log`, or "unknown". */
|
|
38
|
+
export function workerCommitShort(override?: string): string {
|
|
39
|
+
const sha = workerCommit(override);
|
|
40
|
+
return sha === UNKNOWN_COMMIT ? UNKNOWN_COMMIT : sha.slice(0, 7);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* True when the build has a real git commit (not the "unknown" sentinel). The
|
|
45
|
+
* skew derivation only compares against the umbrella tip when this holds — an
|
|
46
|
+
* "unknown" build can't be meaningfully skew-checked.
|
|
47
|
+
*/
|
|
48
|
+
export function hasKnownWorkerCommit(override?: string): boolean {
|
|
49
|
+
return workerCommit(override) !== UNKNOWN_COMMIT;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build the CORE scope for a SERVER-sourced ActivityEvent with `worker_commit`
|
|
54
|
+
* stamped in. This is the seam recordActivity() spreads into every server
|
|
55
|
+
* event so provenance is present WITHOUT each call site remembering it:
|
|
56
|
+
*
|
|
57
|
+
* createActivityEvent({ ..., source: "server", scope: serverActivityScope({ tenantId }) })
|
|
58
|
+
*
|
|
59
|
+
* Pass the event's other scope fields (tenantId/changeId/traceId) as `extra`;
|
|
60
|
+
* workerCommit always wins for the `workerCommit` slot. Omits the field entirely
|
|
61
|
+
* when the build commit is "unknown" so a downstream skew query never mistakes the
|
|
62
|
+
* sentinel for a real deployed SHA.
|
|
63
|
+
*/
|
|
64
|
+
export function serverActivityScope(extra?: ActivityScope): ActivityScope {
|
|
65
|
+
const sha = workerCommit();
|
|
66
|
+
return {
|
|
67
|
+
...extra,
|
|
68
|
+
...(sha !== UNKNOWN_COMMIT ? { workerCommit: sha } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin console shell client logic — hash-router tab switching, the floating
|
|
3
|
+
* Ask envelope, tenant-picker navigation, and roving-tabindex keyboard nav for
|
|
4
|
+
* the tablists. Imported from a processed `<script>` in admin.astro (ADR 0013):
|
|
5
|
+
* type-checked, bundled, tree-shaken, and its bundle hash is in the enforced
|
|
6
|
+
* CSP. The Ask envelope is built with the shared, unit-tested
|
|
7
|
+
* `buildAskEnvelope` (ai/askEnvelope) instead of a hand-copied inline mirror.
|
|
8
|
+
*
|
|
9
|
+
* There is no chat backend yet — the Ask envelope is emitted for dev inspection
|
|
10
|
+
* only (window.__toTAdminAsk + a "tot:admin-ask" CustomEvent), never sent over
|
|
11
|
+
* the network.
|
|
12
|
+
*/
|
|
13
|
+
import { buildAskEnvelope, type AskEnvelope } from "./ai/askEnvelope";
|
|
14
|
+
|
|
15
|
+
const TAB_IDS = [
|
|
16
|
+
"orders",
|
|
17
|
+
"products",
|
|
18
|
+
"subscriptions",
|
|
19
|
+
"customers",
|
|
20
|
+
"fulfillment",
|
|
21
|
+
"reporting",
|
|
22
|
+
"ai-workflows",
|
|
23
|
+
"blog",
|
|
24
|
+
"publish",
|
|
25
|
+
"settings",
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
const ASK_CONTEXTS: Record<string, string> = {
|
|
29
|
+
orders: "Orders",
|
|
30
|
+
products: "Products",
|
|
31
|
+
subscriptions: "Subscriptions",
|
|
32
|
+
customers: "Customers",
|
|
33
|
+
fulfillment: "Fulfillment",
|
|
34
|
+
reporting: "Reporting",
|
|
35
|
+
"ai-workflows": "AI Workflows",
|
|
36
|
+
blog: "Blog",
|
|
37
|
+
publish: "Publish",
|
|
38
|
+
settings: "Settings",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
declare global {
|
|
42
|
+
interface Window {
|
|
43
|
+
__toTAdminAsk?: AskEnvelope;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function initAdminShell(): void {
|
|
48
|
+
const tabSet = new Set<string>(TAB_IDS);
|
|
49
|
+
const tabHashPattern = new RegExp(`^#(${TAB_IDS.join("|")})$`);
|
|
50
|
+
const canViewInternalGuide =
|
|
51
|
+
document.documentElement.dataset.internalAdminGuide === "true";
|
|
52
|
+
const defaultTab = canViewInternalGuide ? "orders" : "settings";
|
|
53
|
+
|
|
54
|
+
const buildAskEnvelopeForTab = (id: string): AskEnvelope => {
|
|
55
|
+
const root = document.documentElement.dataset;
|
|
56
|
+
const isStaff = root.adminIsStaff === "true";
|
|
57
|
+
const panel = document.getElementById(id);
|
|
58
|
+
const selectedRecordId =
|
|
59
|
+
panel?.querySelector<HTMLElement>("[data-admin-selected-id]")?.dataset
|
|
60
|
+
.adminSelectedId || null;
|
|
61
|
+
|
|
62
|
+
return buildAskEnvelope({
|
|
63
|
+
tenant: {
|
|
64
|
+
adminAppTenant: root.adminAppTenant ?? "",
|
|
65
|
+
targetTenant: root.adminTargetTenant ?? "",
|
|
66
|
+
operator: root.adminOperator || null,
|
|
67
|
+
},
|
|
68
|
+
isStaff,
|
|
69
|
+
activeTab: id,
|
|
70
|
+
selectedRecordId,
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const setAskContext = (id: string): void => {
|
|
75
|
+
const label = ASK_CONTEXTS[id] ?? "Storefront";
|
|
76
|
+
const askButton = document.querySelector<HTMLElement>("[data-admin-ask]");
|
|
77
|
+
const askContext = document.querySelector<HTMLElement>(
|
|
78
|
+
"[data-admin-ask-context]",
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (askContext) askContext.textContent = label;
|
|
82
|
+
if (askButton) {
|
|
83
|
+
askButton.setAttribute("aria-label", `Ask about ${label}`);
|
|
84
|
+
askButton.setAttribute("title", `Ask about ${label}`);
|
|
85
|
+
askButton.dataset.askScope = id;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
window.__toTAdminAsk = buildAskEnvelopeForTab(id);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const panelIdFromHash = (): string | null => {
|
|
92
|
+
const rawHash = window.location.hash.slice(1);
|
|
93
|
+
// The tab id is the part before any "?" query suffix (#publish?pr=42);
|
|
94
|
+
// the raw hash is still used for internal-guide anchor resolution.
|
|
95
|
+
const hash = rawHash.split("?")[0] ?? "";
|
|
96
|
+
if (!rawHash) return defaultTab;
|
|
97
|
+
if (tabSet.has(hash)) return hash;
|
|
98
|
+
if (!canViewInternalGuide) return defaultTab;
|
|
99
|
+
|
|
100
|
+
const target = rawHash ? document.getElementById(rawHash) : null;
|
|
101
|
+
const panel = target?.closest<HTMLElement>("[data-admin-panel]");
|
|
102
|
+
if (panel?.id && tabSet.has(panel.id)) return panel.id;
|
|
103
|
+
|
|
104
|
+
return null;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const setActiveTab = (id: string): void => {
|
|
108
|
+
if (!tabSet.has(id)) return;
|
|
109
|
+
|
|
110
|
+
document.documentElement.dataset.adminMode = "work-area";
|
|
111
|
+
document.documentElement.dataset.adminTab = id;
|
|
112
|
+
setAskContext(id);
|
|
113
|
+
|
|
114
|
+
document.querySelectorAll<HTMLElement>("[data-admin-panel]").forEach((panel) => {
|
|
115
|
+
const active = panel.id === id;
|
|
116
|
+
panel.hidden = !active;
|
|
117
|
+
panel.setAttribute("aria-hidden", active ? "false" : "true");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
document
|
|
121
|
+
.querySelectorAll<HTMLAnchorElement>("[data-admin-tab-link]")
|
|
122
|
+
.forEach((link) => {
|
|
123
|
+
const active = link.hash === `#${id}`;
|
|
124
|
+
link.classList.toggle("active", active);
|
|
125
|
+
|
|
126
|
+
if (link.getAttribute("role") === "tab") {
|
|
127
|
+
link.setAttribute("aria-selected", active ? "true" : "false");
|
|
128
|
+
link.tabIndex = active ? 0 : -1;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const setGuideMode = (): void => {
|
|
134
|
+
if (!canViewInternalGuide) {
|
|
135
|
+
setActiveTab(defaultTab);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
document.documentElement.dataset.adminMode = "guide";
|
|
140
|
+
setAskContext(defaultTab);
|
|
141
|
+
|
|
142
|
+
document.querySelectorAll<HTMLElement>("[data-admin-panel]").forEach((panel) => {
|
|
143
|
+
panel.hidden = true;
|
|
144
|
+
panel.setAttribute("aria-hidden", "true");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
document
|
|
148
|
+
.querySelectorAll<HTMLAnchorElement>("[data-admin-tab-link]")
|
|
149
|
+
.forEach((link) => {
|
|
150
|
+
link.classList.remove("active");
|
|
151
|
+
|
|
152
|
+
if (link.getAttribute("role") === "tab") {
|
|
153
|
+
link.setAttribute("aria-selected", "false");
|
|
154
|
+
link.tabIndex = 0;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const scrollWorkspaceToTop = (): void => {
|
|
160
|
+
const scroller = document.querySelector<HTMLElement>(".main");
|
|
161
|
+
const reset = (): void => {
|
|
162
|
+
if (scroller) {
|
|
163
|
+
scroller.scrollTo({ top: 0, left: 0 });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
window.scrollTo({ top: 0, left: 0 });
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
reset();
|
|
171
|
+
requestAnimationFrame(reset);
|
|
172
|
+
setTimeout(reset, 0);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const scrollToHashTarget = (panelId: string): void => {
|
|
176
|
+
const hash = window.location.hash.slice(1);
|
|
177
|
+
const target = hash ? document.getElementById(hash) : null;
|
|
178
|
+
|
|
179
|
+
if (!target || tabSet.has(hash) || target.id === panelId) {
|
|
180
|
+
scrollWorkspaceToTop();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
target?.scrollIntoView({ block: "start" });
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const activateFromHash = ({ scroll = false }: { scroll?: boolean } = {}): void => {
|
|
188
|
+
const panelId = panelIdFromHash();
|
|
189
|
+
|
|
190
|
+
if (panelId) {
|
|
191
|
+
setActiveTab(panelId);
|
|
192
|
+
// Strip any query suffix before deciding whether the hash names a
|
|
193
|
+
// real tab — a valid deep link like #publish?pr=42 must not be
|
|
194
|
+
// normalized away to #settings for merchant viewers.
|
|
195
|
+
const hashTab = window.location.hash.slice(1).split("?")[0] ?? "";
|
|
196
|
+
if (!canViewInternalGuide && !tabSet.has(hashTab)) {
|
|
197
|
+
history.replaceState(null, "", "#settings");
|
|
198
|
+
}
|
|
199
|
+
if (scroll) requestAnimationFrame(() => scrollToHashTarget(panelId));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
setGuideMode();
|
|
204
|
+
if (scroll) {
|
|
205
|
+
const hash = window.location.hash.slice(1);
|
|
206
|
+
requestAnimationFrame(() =>
|
|
207
|
+
document.getElementById(hash)?.scrollIntoView({ block: "start" }),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
document
|
|
213
|
+
.querySelector<HTMLSelectElement>("[data-admin-tenant-picker-select]")
|
|
214
|
+
?.addEventListener("change", (event) => {
|
|
215
|
+
const params = new URLSearchParams(window.location.search);
|
|
216
|
+
params.set("asTenant", (event.target as HTMLSelectElement).value);
|
|
217
|
+
window.location.assign(
|
|
218
|
+
`${window.location.pathname}?${params}${window.location.hash}`,
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Emit the Ask envelope on open. No chat backend yet — dev-inspectable
|
|
223
|
+
// only via window.__toTAdminAsk (already kept current by setAskContext)
|
|
224
|
+
// and this "tot:admin-ask" CustomEvent.
|
|
225
|
+
document.querySelector("[data-admin-ask]")?.addEventListener("click", () => {
|
|
226
|
+
const activeTab = document.documentElement.dataset.adminTab ?? defaultTab;
|
|
227
|
+
const envelope = buildAskEnvelopeForTab(activeTab);
|
|
228
|
+
window.__toTAdminAsk = envelope;
|
|
229
|
+
document.dispatchEvent(new CustomEvent("tot:admin-ask", { detail: envelope }));
|
|
230
|
+
if (
|
|
231
|
+
window.location.hostname === "localhost" ||
|
|
232
|
+
window.location.hostname === "127.0.0.1"
|
|
233
|
+
) {
|
|
234
|
+
console.debug("[admin:ask]", envelope);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
document.addEventListener("click", (event) => {
|
|
239
|
+
const target =
|
|
240
|
+
event.target instanceof Element
|
|
241
|
+
? event.target
|
|
242
|
+
: (event.target as Node | null)?.parentElement ?? null;
|
|
243
|
+
const link = target?.closest<HTMLAnchorElement>("a[href^='#']");
|
|
244
|
+
if (!link || !tabHashPattern.test(link.hash)) return;
|
|
245
|
+
|
|
246
|
+
event.preventDefault();
|
|
247
|
+
const id = link.hash.slice(1);
|
|
248
|
+
setActiveTab(id);
|
|
249
|
+
history.pushState(null, "", link.hash);
|
|
250
|
+
scrollWorkspaceToTop();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
document.addEventListener("keydown", (event) => {
|
|
254
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
255
|
+
const currentTab = target?.closest<HTMLElement>(
|
|
256
|
+
"[role='tab'][data-admin-tab-link]",
|
|
257
|
+
);
|
|
258
|
+
if (!currentTab) return;
|
|
259
|
+
|
|
260
|
+
// Filter to visible tabs only: the mobile tab bar and the internal
|
|
261
|
+
// Admin Tab Map can both be present in the DOM at once (one hidden
|
|
262
|
+
// via CSS depending on viewport/viewer), and this selector matches
|
|
263
|
+
// role="tab" elements in either — without this filter, arrow/home/
|
|
264
|
+
// end navigation could walk into a display:none tablist whose
|
|
265
|
+
// .focus() is a silent no-op.
|
|
266
|
+
const tabs = Array.from(
|
|
267
|
+
document.querySelectorAll<HTMLElement>("[role='tab'][data-admin-tab-link]"),
|
|
268
|
+
).filter((el) => el.offsetParent !== null);
|
|
269
|
+
const currentIndex = tabs.indexOf(currentTab);
|
|
270
|
+
let nextIndex = currentIndex;
|
|
271
|
+
|
|
272
|
+
if (event.key === "ArrowRight") nextIndex = (currentIndex + 1) % tabs.length;
|
|
273
|
+
if (event.key === "ArrowLeft")
|
|
274
|
+
nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
|
|
275
|
+
if (event.key === "Home") nextIndex = 0;
|
|
276
|
+
if (event.key === "End") nextIndex = tabs.length - 1;
|
|
277
|
+
|
|
278
|
+
if (nextIndex === currentIndex) return;
|
|
279
|
+
|
|
280
|
+
const nextTab = tabs[nextIndex];
|
|
281
|
+
if (!nextTab) return;
|
|
282
|
+
|
|
283
|
+
event.preventDefault();
|
|
284
|
+
nextTab.focus();
|
|
285
|
+
nextTab.click();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
window.addEventListener("hashchange", () => activateFromHash({ scroll: true }));
|
|
289
|
+
activateFromHash({ scroll: Boolean(window.location.hash) });
|
|
290
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Canonical approval state machine
|
|
3
|
-
*
|
|
4
|
-
* declared their own copies of this union; both now import it from here.
|
|
2
|
+
* Canonical approval state machine. `settings.ts` and `aiWorkflows.ts` import
|
|
3
|
+
* this union from here.
|
|
5
4
|
*/
|
|
6
5
|
export type ApprovalMode = "draft_only" | "approval_required" | "guarded_automation";
|
|
7
6
|
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Canonical audit event model
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* one of these, keyed by `targetTenant` — never `adminAppTenant`, per
|
|
6
|
-
* decision `tenant-envelope-contract`.
|
|
2
|
+
* Canonical audit event model — records both the operator and the target
|
|
3
|
+
* tenant. Every mutation-shaped or AI-suggested action in the admin console
|
|
4
|
+
* should record one of these, keyed by `targetTenant` — never `adminAppTenant`.
|
|
7
5
|
*/
|
|
8
6
|
export interface AuditEvent {
|
|
9
7
|
/** The signed-in operator's email, or a system actor id for automation-triggered events. */
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Validate + sanitize merchant-submitted checkout styling into a safe, complete
|
|
3
|
-
* `CheckoutStyle` before it is published to the versioned spine (
|
|
3
|
+
* `CheckoutStyle` before it is published to the versioned spine (the save path).
|
|
4
4
|
*
|
|
5
5
|
* The submitted body is a partial (`DeepPartial<CheckoutStyle>`): a merchant may
|
|
6
6
|
* set one field without restating the rest, so this merges any valid override
|
|
7
|
-
* over `DEFAULT_CHECKOUT_STYLE` (reusing
|
|
8
|
-
* complete descriptor. It is the WRITE-side guard; the
|
|
7
|
+
* over `DEFAULT_CHECKOUT_STYLE` (reusing `mergeCheckoutStyle`) and returns a
|
|
8
|
+
* complete descriptor. It is the WRITE-side guard; the GateReport
|
|
9
9
|
* still does the deeper compliance/contrast/perf pass on promote.
|
|
10
10
|
*
|
|
11
11
|
* Two hard constraints enforced here:
|
|
12
12
|
* - **ASCII-safe.** Hosted checkout cache pass is not Unicode-safe — a non-ASCII
|
|
13
|
-
* byte in a value
|
|
13
|
+
* byte in a value rendered into the pushed CSS can blank the checkout.
|
|
14
14
|
* Every provided string leaf must be printable ASCII.
|
|
15
|
-
* - **CSS-token hygiene.** These values are interpolated into generated CSS
|
|
16
|
-
*
|
|
15
|
+
* - **CSS-token hygiene.** These values are interpolated into generated CSS,
|
|
16
|
+
* so we reject the characters that could break out of a declaration
|
|
17
17
|
* (`;` `{` `}` `<` `>` and newlines). Defense-in-depth, not the full gate.
|
|
18
18
|
*
|
|
19
19
|
* Pure + dependency-free (no I/O, no Astro): unit-testable in isolation and safe
|
|
20
|
-
* to reuse from the gate
|
|
20
|
+
* to reuse from the gate or the CSS renderer if promoted to
|
|
21
21
|
* public-runtime later.
|
|
22
22
|
*/
|
|
23
23
|
import {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Decide which tenant a checkout-style save may write to
|
|
3
|
-
*
|
|
2
|
+
* Decide which tenant a checkout-style save may write to — staff cross-tenant
|
|
3
|
+
* save.
|
|
4
4
|
*
|
|
5
5
|
* The base save gate (`resolveOwnerSession`) resolves the operator's OWN tenant
|
|
6
|
-
* (the routed appDomain).
|
|
6
|
+
* (the routed appDomain). But a ToT-staff operator can
|
|
7
7
|
* "operate as" another tenant via the admin picker (`?asTenant=`), and their
|
|
8
8
|
* save must land on THAT tenant's artifact — provided they're actually
|
|
9
9
|
* authorized for it. This resolver makes that decision, and it deliberately
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Server-side client for the checkout order READ API
|
|
2
|
+
* Server-side client for the checkout order READ API.
|
|
3
3
|
*
|
|
4
4
|
* Calls `GET /commerce/orders[/:orderId]` with the tenant's per-tenant bearer key
|
|
5
5
|
* (the ToT `partnerApi.apiKey`, held as a Worker secret — NEVER shipped to the
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* The fetch runs on the Cloudflare Worker (server), so the call is same-origin →
|
|
10
10
|
* cross-service server-to-server; it is NOT browser-originated and therefore not
|
|
11
|
-
* CSP-governed — the
|
|
12
|
-
* decision `u2-csp-server-side-fetch`).
|
|
11
|
+
* CSP-governed — the CSP connect-src entry is intentionally skipped.
|
|
13
12
|
*
|
|
14
13
|
* `fetchImpl` is injectable so the client is unit-testable offline.
|
|
15
14
|
*/
|
|
@@ -68,8 +67,7 @@ export interface CheckoutOrderClientConfig {
|
|
|
68
67
|
/**
|
|
69
68
|
* Resolve the checkout order API base URL for an environment (contract §2).
|
|
70
69
|
* qa/test use the non-prod proxy; live uses prod. Kept private so it does not
|
|
71
|
-
* collide with
|
|
72
|
-
* the integrator can dedup into one shared export.
|
|
70
|
+
* collide with the exported checkout theme base URL.
|
|
73
71
|
*/
|
|
74
72
|
export function orderApiBaseUrl(env: CustomizationEnv): string {
|
|
75
73
|
return env === "live"
|