@tokenoftrust/storefront-runner 2.0.17 → 2.0.18
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/env.d.ts +18 -0
- package/apps/storefront/migrations-apps/0012_test_destinations.sql +29 -0
- package/apps/storefront/src/components/blog/ReactionBar.astro +1 -1
- package/apps/storefront/src/components/islands/CheckoutComplianceGate.tsx +365 -49
- package/apps/storefront/src/components/islands/checkoutVerificationFlow.ts +251 -0
- package/apps/storefront/src/config/compliance/rulesets.ts +18 -7
- package/apps/storefront/src/config/devTenantSeed.ts +24 -1
- package/apps/storefront/src/config/evidenceTenantRoster.ts +25 -0
- package/apps/storefront/src/config/publicationSpaceConfig.ts +25 -0
- package/apps/storefront/src/config/resolver.ts +5 -0
- package/apps/storefront/src/config/tenantAnalyticsFiles.ts +43 -0
- package/apps/storefront/src/config/tenants.ts +11 -0
- package/apps/storefront/src/layouts/Layout.astro +5 -1
- package/apps/storefront/src/lib/activity/killSwitch.ts +1 -1
- package/apps/storefront/src/lib/admin/consentSettingsClient.ts +113 -0
- package/apps/storefront/src/lib/apps/consentConfigRead.ts +24 -0
- package/apps/storefront/src/lib/apps/gtmMigrationDraft.ts +103 -0
- package/apps/storefront/src/lib/auth/loginGate.ts +25 -0
- package/apps/storefront/src/lib/checkoutCommerce.ts +1 -1
- package/apps/storefront/src/lib/compliance/enforcement.ts +25 -3
- package/apps/storefront/src/lib/d1/schema-apps.ts +44 -0
- package/apps/storefront/src/lib/env.ts +13 -111
- package/apps/storefront/src/lib/integration/tenantAnalyticsDeclarations.ts +198 -0
- package/apps/storefront/src/lib/monitoring/manifest.ts +1 -1
- package/apps/storefront/src/lib/rawMarketingHtml.ts +9 -1
- package/apps/storefront/src/lib/runtimeEnv.ts +124 -0
- package/apps/storefront/src/lib/verification/totVerificationClient.ts +485 -0
- package/apps/storefront/src/middleware/candidatePin.ts +35 -0
- package/apps/storefront/src/middleware/controlPlane.ts +270 -0
- package/apps/storefront/src/middleware/index.ts +100 -796
- package/apps/storefront/src/middleware/publicRender.ts +43 -0
- package/apps/storefront/src/middleware/renderChain.ts +685 -0
- package/apps/storefront/src/middleware/routeBranch.ts +160 -0
- package/apps/storefront/src/middleware/staticBundle.ts +109 -0
- package/apps/storefront/src/pages/[...slug].astro +1 -0
- package/apps/storefront/src/pages/api/compliance/preflight.ts +179 -2
- package/apps/storefront/src/pages/index.astro +1 -0
- package/apps/storefront/vitest.config.ts +5 -0
- package/package.json +1 -1
- package/packages/public-runtime/package.json +1 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/deliver-test-event-request.schema.json +122 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/get-test-destination-delivery-request.schema.json +79 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/register-test-destination-request.schema.json +93 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/test-destination-delivery.schema.json +106 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/test-destination.schema.json +77 -0
- package/packages/public-runtime/schemas/publication-space/v1/manifest.schema.json +195 -0
- package/packages/public-runtime/schemas/publication-space/v1/normalized-mount.schema.json +80 -0
- package/packages/public-runtime/schemas/publication-space/v1/ref.schema.json +20 -0
- package/packages/public-runtime/schemas/publication-version/v1/pointer.schema.json +28 -0
- package/packages/public-runtime/schemas/publication-version/v1/transition-request.schema.json +44 -0
- package/packages/public-runtime/schemas/publication-version/v1/transition.schema.json +47 -0
- package/packages/public-runtime/schemas/publication-version/v1/version.schema.json +58 -0
- package/packages/public-runtime/src/activity/catalog.ts +1 -1
- package/packages/public-runtime/src/candidate-index.ts +6 -0
- package/packages/public-runtime/src/customization-reconcile.ts +8 -0
- package/packages/public-runtime/src/html-cache-policy.ts +57 -0
- package/packages/public-runtime/src/index.ts +7 -0
- package/packages/public-runtime/src/integration/consent-broker.ts +115 -0
- package/packages/public-runtime/src/integration/interim-bridge.ts +111 -27
- package/packages/public-runtime/src/integration/managed-analytics-api-v1.schemas.ts +65 -0
- package/packages/public-runtime/src/integration/managed-analytics-api-v1.ts +81 -0
- package/packages/public-runtime/src/integration/render-phases.ts +8 -3
- package/packages/public-runtime/src/publication-artifact.ts +203 -0
- package/packages/public-runtime/src/publication-space.ts +853 -0
- package/packages/public-runtime/src/publication-version.ts +587 -0
- package/packages/public-runtime/src/route-tier.ts +257 -0
- package/packages/public-runtime/src/static-bundle.ts +18 -5
- package/packages/public-runtime/src/tenant-assets.ts +85 -17
- package/packages/public-runtime/src/tenant.ts +70 -0
- package/packages/public-runtime/tests/fixtures/publication-spaces/internal.json +32 -0
- package/packages/public-runtime/tests/fixtures/publication-spaces/mounted-public.json +38 -0
- package/packages/public-runtime/tests/fixtures/publication-spaces/normalized-mounts.json +39 -0
- package/packages/public-runtime/tests/fixtures/publication-spaces/ref.json +5 -0
- package/packages/public-runtime/tests/fixtures/publication-spaces/root-public.json +31 -0
- package/packages/public-runtime/tests/fixtures/publication-versions/articles-v1.json +20 -0
- package/packages/public-runtime/tests/fixtures/publication-versions/articles-v2.json +20 -0
- package/packages/public-runtime/tests/fixtures/publication-versions/internal-v1.json +20 -0
- package/packages/public-runtime/tests/fixtures/publication-versions/live-pointer.json +11 -0
- package/packages/public-runtime/tests/fixtures/publication-versions/live-transition-request.json +14 -0
- package/packages/public-runtime/tests/fixtures/publication-versions/live-transition.json +17 -0
- package/packages/public-runtime/tests/fixtures/publication-versions/root-v1.json +20 -0
- package/packages/public-runtime/tests/fixtures/publication-versions/root-v2.json +20 -0
- package/scripts/dev/ai-edit.mjs +16 -3
- package/scripts/dev/port-check.mjs +18 -3
- package/scripts/dev/publish.mjs +26 -3
- package/scripts/tenant/validate.mjs +14 -1
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The control-plane branch: everything a route reaches only with authorization,
|
|
3
|
+
* plus every route on a login-gated host.
|
|
4
|
+
*
|
|
5
|
+
* It owns the three request-time D1 reads (managed installs, consent, the catalog
|
|
6
|
+
* edge replica), the sign-in gate and its admin landing, and the `/dev` alias —
|
|
7
|
+
* then delegates to the shared render chain with those values supplied.
|
|
8
|
+
*/
|
|
9
|
+
import type { APIContext, MiddlewareNext } from "astro";
|
|
10
|
+
import { readEnv, readD1, readKv } from "@/lib/env";
|
|
11
|
+
import { fromD1, type D1Like } from "@/lib/d1/catalog";
|
|
12
|
+
import { readCurrentTenantConsentConfig } from "@/lib/apps/consentConfigRead";
|
|
13
|
+
import { loadActiveManagedInstallDeclarations } from "@/lib/integration/managedInstallDeclarations";
|
|
14
|
+
import { buildTenantConsentBroker, verifyPreviewToken } from "@tot/public-runtime";
|
|
15
|
+
import type { ConsentSnapshot, IntegrationDeclarationInput, TenantConfig } from "@tot/public-runtime";
|
|
16
|
+
import {
|
|
17
|
+
evaluateLoginGate,
|
|
18
|
+
isAuthPath,
|
|
19
|
+
isAdminPath,
|
|
20
|
+
sessionAdmitsHost,
|
|
21
|
+
sessionHostCapability,
|
|
22
|
+
PUBLIC_HOME_TENANT_ID,
|
|
23
|
+
type GateMode,
|
|
24
|
+
} from "@/lib/auth/loginGate";
|
|
25
|
+
import { resolveAdminLanding, isAdminSelectPath } from "@/lib/auth/adminLanding";
|
|
26
|
+
import { resolveAdminTenantMode } from "@/lib/adminTenantEnvelope";
|
|
27
|
+
import { gatedHoldingResponse } from "@/lib/auth/gatePage";
|
|
28
|
+
import { readSession, KvSessionStore, SESSION_COOKIE } from "@/lib/auth/session";
|
|
29
|
+
import { runRenderChain } from "./renderChain.js";
|
|
30
|
+
|
|
31
|
+
export interface ControlPlaneInput {
|
|
32
|
+
context: APIContext;
|
|
33
|
+
next: MiddlewareNext;
|
|
34
|
+
host: string;
|
|
35
|
+
url: URL;
|
|
36
|
+
tenant: TenantConfig;
|
|
37
|
+
basePath: string;
|
|
38
|
+
renderPath: string;
|
|
39
|
+
cache: Cache | undefined;
|
|
40
|
+
kv: KVNamespace | undefined;
|
|
41
|
+
gateMode: GateMode;
|
|
42
|
+
/** `DEMO_GATE_HOSTS`, raw. */
|
|
43
|
+
demoGateHosts: string | null | undefined;
|
|
44
|
+
/** True when this host is withheld behind sign-in — see `resolveMiddlewareBranchV1`. */
|
|
45
|
+
hostLoginGated: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function serveControlPlane(input: ControlPlaneInput): Promise<Response> {
|
|
49
|
+
const { context, next, host, url, tenant, basePath, renderPath, cache, kv } = input;
|
|
50
|
+
const { request, locals } = context;
|
|
51
|
+
|
|
52
|
+
// Login-gate enforcement (DEMO_GATE_MODE="login"): runs before the render so a
|
|
53
|
+
// gated page is never served to an unauthenticated viewer. The platform's own
|
|
54
|
+
// "self" home tenant is EXEMPT — it renders publicly (browse + sign up); every
|
|
55
|
+
// other tenant on a gated host is withheld behind sign-in. A valid,
|
|
56
|
+
// resource-bound tot_session sets locals.viewer (email + roles) for chrome;
|
|
57
|
+
// otherwise an unauthenticated visitor gets a "Coming soon" holding page with
|
|
58
|
+
// a Sign-in button (auth paths are exempt so sign-in itself is reachable). The
|
|
59
|
+
// grant `resource` is the tenant's appDomain — never input.
|
|
60
|
+
if (input.hostLoginGated) {
|
|
61
|
+
const tenantExempt = tenant.tenant_id === PUBLIC_HOME_TENANT_ID;
|
|
62
|
+
// UNIFIED on the federated session: read the FULL session (no resource filter —
|
|
63
|
+
// federated sign-in's aud is the storefront app, not this tenant) and admit the
|
|
64
|
+
// viewer iff they hold a MEMBERSHIP for the host's tenant. One session model for
|
|
65
|
+
// the console + the preview gate. Deny-closed: no session / no membership → gate.
|
|
66
|
+
let viewerRecord = null;
|
|
67
|
+
if (!isAuthPath(url.pathname)) {
|
|
68
|
+
const sessionKv = await readKv("SESSION");
|
|
69
|
+
if (sessionKv) {
|
|
70
|
+
const sid = context.cookies.get(SESSION_COOKIE)?.value ?? null;
|
|
71
|
+
viewerRecord = await readSession(
|
|
72
|
+
new KvSessionStore(sessionKv),
|
|
73
|
+
sid,
|
|
74
|
+
Math.floor(Date.now() / 1000),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
79
|
+
const admitted = sessionAdmitsHost(viewerRecord, tenant.appDomain, nowSeconds);
|
|
80
|
+
// QA content-bypass: only when the server has configured DEMO_GATE_BYPASS_TOKEN
|
|
81
|
+
// (never in a prod slot) AND the request presents a matching X-Demo-Gate-Bypass
|
|
82
|
+
// header. Unset token ⇒ false ⇒ fully inert; a client cannot self-grant. The
|
|
83
|
+
// public-content-only restriction (admin/cockpit/api excluded) is enforced
|
|
84
|
+
// inside evaluateLoginGate via isBypassPrivilegedPath.
|
|
85
|
+
const bypassToken = await readEnv("DEMO_GATE_BYPASS_TOKEN");
|
|
86
|
+
const qaBypass =
|
|
87
|
+
!!bypassToken && request.headers.get("x-demo-gate-bypass") === bypassToken;
|
|
88
|
+
|
|
89
|
+
// Customization-preview admission, available to EVERY tenant: a signed,
|
|
90
|
+
// unexpired `?__preview=` token (or the `tot_preview` cookie it persists as)
|
|
91
|
+
// whose `tenantId` claim matches THIS tenant admits the viewer to that
|
|
92
|
+
// tenant's public content. Verified here — before the gate decides — because
|
|
93
|
+
// previewing an unpublished storefront is the token's whole purpose, and a
|
|
94
|
+
// viewer holding one must not be handed the "Coming soon" holding page.
|
|
95
|
+
// Tenant-bound and HMAC-signed, so one tenant's token never opens another's,
|
|
96
|
+
// and an absent CUSTOMIZATION_PREVIEW_SECRET leaves it false (fail-closed).
|
|
97
|
+
// The admin/cockpit/api exclusion is enforced in evaluateLoginGate.
|
|
98
|
+
const gatePreviewSecret = await readEnv("CUSTOMIZATION_PREVIEW_SECRET");
|
|
99
|
+
const gatePreviewToken =
|
|
100
|
+
url.searchParams.get("__preview") ??
|
|
101
|
+
context.cookies.get("tot_preview")?.value ??
|
|
102
|
+
null;
|
|
103
|
+
let previewTokenValid = false;
|
|
104
|
+
if (gatePreviewSecret && gatePreviewToken) {
|
|
105
|
+
const verifiedPreview = await verifyPreviewToken(
|
|
106
|
+
gatePreviewSecret,
|
|
107
|
+
gatePreviewToken,
|
|
108
|
+
nowSeconds,
|
|
109
|
+
);
|
|
110
|
+
previewTokenValid =
|
|
111
|
+
verifiedPreview.ok && verifiedPreview.claims.tenantId === tenant.tenant_id;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Canonical admin tenant landing: an AUTHENTICATED viewer who is not a
|
|
115
|
+
// member/staff of THIS host's tenant must be routed to their OWN tenant
|
|
116
|
+
// (auto when sole) or the tenant picker (when several / none) — never bounced
|
|
117
|
+
// into a sign-in loop and never silently shown the wrong tenant. Only for
|
|
118
|
+
// admin paths, only for an authed non-member, and never on the /admin/select
|
|
119
|
+
// picker itself (whose whole purpose is to be reachable by such a viewer).
|
|
120
|
+
if (
|
|
121
|
+
isAdminPath(url.pathname) &&
|
|
122
|
+
!isAdminSelectPath(url.pathname) &&
|
|
123
|
+
!admitted &&
|
|
124
|
+
viewerRecord &&
|
|
125
|
+
!qaBypass
|
|
126
|
+
) {
|
|
127
|
+
const landing = resolveAdminLanding({
|
|
128
|
+
record: viewerRecord,
|
|
129
|
+
routedTenant: tenant.appDomain,
|
|
130
|
+
basePath,
|
|
131
|
+
intended: url.pathname + url.search,
|
|
132
|
+
nowSeconds,
|
|
133
|
+
});
|
|
134
|
+
if (landing.action === "auto" || landing.action === "select") {
|
|
135
|
+
return context.redirect(landing.location, 303);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The /admin/select picker requires only a VALID SESSION (any authed viewer),
|
|
140
|
+
// not membership of the routed tenant — it lists the viewer's OWN tenants. So
|
|
141
|
+
// it passes the gate on the strength of the session alone.
|
|
142
|
+
const hasValidSession =
|
|
143
|
+
admitted || (isAdminSelectPath(url.pathname) && !!viewerRecord);
|
|
144
|
+
const decision = evaluateLoginGate({
|
|
145
|
+
host,
|
|
146
|
+
pathname: url.pathname,
|
|
147
|
+
search: url.search,
|
|
148
|
+
mode: input.gateMode,
|
|
149
|
+
gatedHosts: input.demoGateHosts,
|
|
150
|
+
hasValidSession,
|
|
151
|
+
tenantExempt,
|
|
152
|
+
qaBypass,
|
|
153
|
+
previewTokenValid,
|
|
154
|
+
});
|
|
155
|
+
if (decision.action === "redirect") {
|
|
156
|
+
// ADMIN (and any privileged surface) hard-redirects to sign-in rather than
|
|
157
|
+
// rendering the merchant-facing "Coming soon" holding page: an unauthenticated
|
|
158
|
+
// admin visitor must land on the sign-in prompt, never an ambiguous 200 that
|
|
159
|
+
// reads as "this store isn't ready." The holding page's whole audience is a
|
|
160
|
+
// public browser of a gated STOREFRONT; it is the wrong signal for /admin. The
|
|
161
|
+
// gate already decided to withhold — this only changes HOW (303 vs holding
|
|
162
|
+
// page); it never weakens the gate. `decision.location` already carries
|
|
163
|
+
// `?next=` so the admin returns to exactly where they were headed.
|
|
164
|
+
if (isAdminPath(url.pathname)) {
|
|
165
|
+
return context.redirect(decision.location, 303);
|
|
166
|
+
}
|
|
167
|
+
// Withhold content but offer sign-in (carrying the intended destination).
|
|
168
|
+
return gatedHoldingResponse(decision.location);
|
|
169
|
+
}
|
|
170
|
+
if (admitted && viewerRecord) {
|
|
171
|
+
// Surface the viewer's standing FOR THIS host's tenant as chrome, BOUND to
|
|
172
|
+
// the host so a capability the session holds for another tenant never leaks
|
|
173
|
+
// in: a member sees their tenants[] role; the owner of their own tenant sees
|
|
174
|
+
// the resource-scoped session's capability; a ToT-staff viewer admitted via
|
|
175
|
+
// an explicit vendor selection sees the staffRoles-scoped capability. The
|
|
176
|
+
// ship gates (decideIsOwner/resolveShipPrincipal) read this capability,
|
|
177
|
+
// so binding it here keeps owner-resolution server-side + tenant-correct.
|
|
178
|
+
//
|
|
179
|
+
// On an admin path, "the host" a staff viewer is really acting on is the
|
|
180
|
+
// ?asTenant= impersonation target (mirrors admin.astro/AdminPublishTab.astro's
|
|
181
|
+
// own resolveAdminTenantMode call) — resolving capability against the routed
|
|
182
|
+
// host instead left a staff viewer who selected another tenant reading as
|
|
183
|
+
// "not an owner" of it even while holding a real grant, because their
|
|
184
|
+
// staffSelection is bound to the SELECTED tenant, never the host app.
|
|
185
|
+
const capabilityHost = isAdminPath(url.pathname)
|
|
186
|
+
? resolveAdminTenantMode({
|
|
187
|
+
session: viewerRecord,
|
|
188
|
+
adminAppTenant: tenant.appDomain,
|
|
189
|
+
requestedTarget: url.searchParams.get("asTenant"),
|
|
190
|
+
nowSeconds,
|
|
191
|
+
}).envelope.targetTenant
|
|
192
|
+
: tenant.appDomain;
|
|
193
|
+
const hostCapability = sessionHostCapability(viewerRecord, capabilityHost, nowSeconds);
|
|
194
|
+
locals.viewer = {
|
|
195
|
+
email: viewerRecord.email,
|
|
196
|
+
roles: hostCapability ? [hostCapability] : viewerRecord.roles,
|
|
197
|
+
capability: hostCapability ?? viewerRecord.capability,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// /dev was renamed to /cockpit (2026-07-20). 302-redirect any old /dev link —
|
|
203
|
+
// host-routed (/dev) or path-routed (/<vendor>/dev) — to the new path, preserving
|
|
204
|
+
// the tenant prefix + query. Temporary (302) while the cockpit UX is still evolving.
|
|
205
|
+
if (renderPath === "/dev" || renderPath.startsWith("/dev/")) {
|
|
206
|
+
const rest = renderPath.slice("/dev".length); // "" or "/sub…"
|
|
207
|
+
return Response.redirect(`${url.origin}${basePath}/cockpit${rest}${url.search}`, 302);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Active managed installs share the same declaration/resolver/CSP/broker/cache
|
|
211
|
+
// plane as tenant-config integrations. Read only canonical active rows and their
|
|
212
|
+
// current immutable snapshot; candidates, suspended rows, and drafts produce no
|
|
213
|
+
// declaration. A missing/unmigrated binding fails closed to no managed effects.
|
|
214
|
+
const appsD1 = await readD1("STOREFRONT_APPS_DB");
|
|
215
|
+
let managedIntegrationDeclarations: IntegrationDeclarationInput[] = [];
|
|
216
|
+
if (appsD1) {
|
|
217
|
+
try {
|
|
218
|
+
managedIntegrationDeclarations = await loadActiveManagedInstallDeclarations(
|
|
219
|
+
fromD1(appsD1),
|
|
220
|
+
tenant.tenant_id,
|
|
221
|
+
);
|
|
222
|
+
} catch {
|
|
223
|
+
managedIntegrationDeclarations = [];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return runRenderChain({
|
|
228
|
+
context,
|
|
229
|
+
next,
|
|
230
|
+
host,
|
|
231
|
+
url,
|
|
232
|
+
tenant,
|
|
233
|
+
basePath,
|
|
234
|
+
renderPath,
|
|
235
|
+
cache,
|
|
236
|
+
kv,
|
|
237
|
+
managedIntegrationDeclarations,
|
|
238
|
+
readTenantConsentSnapshot: () => readSavedConsentSnapshot(tenant.tenant_id),
|
|
239
|
+
// The D1 catalog edge read replica, when this deploy opts into it. Deferred to
|
|
240
|
+
// the chain's own catalog build so a request the static bundle or the edge
|
|
241
|
+
// cache answers never pays for it. The public branch never resolves it at all
|
|
242
|
+
// — see `publicRender`.
|
|
243
|
+
readCatalogD1: async (): Promise<D1Like | undefined> =>
|
|
244
|
+
(await readEnv("USE_D1_CATALOG")) === "1" ? await readD1("CATALOG_DB") : undefined,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The live Worker serving preview/admin/gated areas (NOT the public path per ADR
|
|
250
|
+
* 0013) reads the current consent from D1. Kept FAIL-SOFT: a bound D1 whose schema
|
|
251
|
+
* is absent (a schemaless ephemeral preview D1) must never 500 the render — it
|
|
252
|
+
* returns null and the caller keeps the essential-only default. Fail-soft, NOT
|
|
253
|
+
* fail-quiet: consent silently downgrading on a regulated storefront is its own
|
|
254
|
+
* incident class, so log loudly. Expected only against an ephemeral preview D1;
|
|
255
|
+
* against a production D1 it is a real signal to investigate.
|
|
256
|
+
*/
|
|
257
|
+
async function readSavedConsentSnapshot(tenantId: string): Promise<ConsentSnapshot | null> {
|
|
258
|
+
const consentD1 = await readD1("STOREFRONT_APPS_DB");
|
|
259
|
+
if (!consentD1) return null;
|
|
260
|
+
try {
|
|
261
|
+
const config = await readCurrentTenantConsentConfig(fromD1(consentD1), tenantId);
|
|
262
|
+
return config ? buildTenantConsentBroker(config).getSnapshot() : null;
|
|
263
|
+
} catch (err) {
|
|
264
|
+
console.error(
|
|
265
|
+
`consent snapshot read failed for tenant ${tenantId}; serving the ` +
|
|
266
|
+
`fail-closed essential-only default: ${err instanceof Error ? err.message : String(err)}`,
|
|
267
|
+
);
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|