@tokenoftrust/storefront-runner 2.0.3 → 2.0.4
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/content.d.ts +159 -0
- package/apps/storefront/.astro/integrations/_astrojs_cloudflare/cloudflare.d.ts +1 -0
- package/apps/storefront/.astro/types.d.ts +3 -0
- package/apps/storefront/node_modules/.bin/acorn +43 -0
- package/apps/storefront/node_modules/.bin/astro +43 -0
- package/apps/storefront/node_modules/.bin/astro-check +43 -0
- package/apps/storefront/node_modules/.bin/browserslist +43 -0
- package/apps/storefront/node_modules/.bin/cf-wrangler +43 -0
- package/apps/storefront/node_modules/.bin/depcruise +43 -0
- package/apps/storefront/node_modules/.bin/depcruise-baseline +43 -0
- package/apps/storefront/node_modules/.bin/depcruise-fmt +43 -0
- package/apps/storefront/node_modules/.bin/depcruise-wrap-stream-in-html +43 -0
- package/apps/storefront/node_modules/.bin/dependency-cruise +43 -0
- package/apps/storefront/node_modules/.bin/dependency-cruiser +43 -0
- package/apps/storefront/node_modules/.bin/drizzle-kit +43 -0
- package/apps/storefront/node_modules/.bin/esbuild +43 -0
- package/apps/storefront/node_modules/.bin/jiti +43 -0
- package/apps/storefront/node_modules/.bin/playwright +43 -0
- package/apps/storefront/node_modules/.bin/playwright-core +43 -0
- package/apps/storefront/node_modules/.bin/prettier +43 -0
- package/apps/storefront/node_modules/.bin/rollup +43 -0
- package/apps/storefront/node_modules/.bin/tsc +43 -0
- package/apps/storefront/node_modules/.bin/tsserver +43 -0
- package/apps/storefront/node_modules/.bin/tsx +43 -0
- package/apps/storefront/node_modules/.bin/vite +43 -0
- package/apps/storefront/node_modules/.bin/vitest +43 -0
- package/apps/storefront/node_modules/.bin/workerd +43 -0
- package/apps/storefront/node_modules/.bin/wrangler +43 -0
- package/apps/storefront/node_modules/.bin/wrangler2 +43 -0
- package/apps/storefront/node_modules/.bin/yaml +43 -0
- package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_client__js.js +58 -0
- package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_client__js.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_n_@preact_signals.js +741 -0
- package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_n_@preact_signals.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_server__js.js +2549 -0
- package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_server__js.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/_metadata.json +150 -0
- package/apps/storefront/node_modules/.vite/deps/astro-BIHuWFxm.js +434 -0
- package/apps/storefront/node_modules/.vite/deps/astro-BIHuWFxm.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
- package/apps/storefront/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/astro_n_axobject-query.js +2705 -0
- package/apps/storefront/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
- package/apps/storefront/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
- package/apps/storefront/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/audit-DKDpcdEI.js +1535 -0
- package/apps/storefront/node_modules/.vite/deps/audit-DKDpcdEI.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/highlight-B9UoX1wa.js +48 -0
- package/apps/storefront/node_modules/.vite/deps/highlight-B9UoX1wa.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/icons-BWi2wI9n.js +42 -0
- package/apps/storefront/node_modules/.vite/deps/icons-BWi2wI9n.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/package.json +3 -0
- package/apps/storefront/node_modules/.vite/deps/preact.js +310 -0
- package/apps/storefront/node_modules/.vite/deps/preact.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/preact_compat.js +420 -0
- package/apps/storefront/node_modules/.vite/deps/preact_compat.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/preact_compat_jsx-runtime.js +4 -0
- package/apps/storefront/node_modules/.vite/deps/preact_debug.js +236 -0
- package/apps/storefront/node_modules/.vite/deps/preact_debug.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/preact_devtools.js +12 -0
- package/apps/storefront/node_modules/.vite/deps/preact_devtools.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/preact_hooks.js +192 -0
- package/apps/storefront/node_modules/.vite/deps/preact_hooks.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/preact_jsx-dev-runtime.js +3 -0
- package/apps/storefront/node_modules/.vite/deps/preact_jsx-runtime.js +92 -0
- package/apps/storefront/node_modules/.vite/deps/preact_jsx-runtime.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/preact_test-utils.js +59 -0
- package/apps/storefront/node_modules/.vite/deps/preact_test-utils.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/rolldown-runtime-BPOCksWG.js +24 -0
- package/apps/storefront/node_modules/.vite/deps/settings-BVDV0Roj.js +189 -0
- package/apps/storefront/node_modules/.vite/deps/settings-BVDV0Roj.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/static-html-DveYqwMo.js +13 -0
- package/apps/storefront/node_modules/.vite/deps/static-html-DveYqwMo.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/toolbar-B3VIHCXg.js +458 -0
- package/apps/storefront/node_modules/.vite/deps/toolbar-B3VIHCXg.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/ui-library-BK6ricUd.js +1016 -0
- package/apps/storefront/node_modules/.vite/deps/ui-library-BK6ricUd.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/window-BvxDenKN.js +32 -0
- package/apps/storefront/node_modules/.vite/deps/window-BvxDenKN.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/xray-YFiRUU0Z.js +113 -0
- package/apps/storefront/node_modules/.vite/deps/xray-YFiRUU0Z.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps_ssr/_metadata.json +8 -0
- package/apps/storefront/node_modules/.vite/deps_ssr/package.json +3 -0
- package/apps/storefront/src/layouts/Layout.astro +1 -1
- package/apps/storefront/src/lib/apps/registryAccess.ts +45 -0
- package/apps/storefront/src/lib/auth/adminEntry.ts +4 -4
- package/apps/storefront/src/lib/shared/capability.ts +68 -0
- package/apps/storefront/src/lib/shared/developerInvites.ts +1250 -0
- package/apps/storefront/src/lib/shared/invitePreflight.ts +123 -0
- package/apps/storefront/src/lib/webhooks/deliveryEngine.ts +38 -4
- package/apps/storefront/src/lib/webhooks/emit.ts +1 -1
- package/apps/storefront/src/lib/webhooks/getDispatcher.ts +1 -1
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +1 -1
- package/apps/storefront/src/pages/index.astro +1 -1
- package/apps/storefront/src/pages/products/[handle].astro +1 -1
- package/package.json +1 -1
- package/packages/public-runtime/node_modules/.bin/tsc +43 -0
- package/packages/public-runtime/node_modules/.bin/tsserver +43 -0
- package/packages/public-runtime/node_modules/.bin/vitest +43 -0
- package/pnpm-workspace.yaml +3 -0
- package/apps/storefront/src/lib/apps/adminService.ts +0 -116
- package/apps/storefront/src/lib/apps/adminSession.ts +0 -214
- package/apps/storefront/src/lib/apps/apiAuth.ts +0 -91
- package/apps/storefront/src/lib/apps/apiRoute.ts +0 -35
- package/apps/storefront/src/lib/apps/catalogMapper.ts +0 -39
- package/apps/storefront/src/lib/apps/consentConfigStore.ts +0 -228
- package/apps/storefront/src/lib/apps/gtmMigrationInstall.ts +0 -447
- package/apps/storefront/src/lib/apps/gtmMigrationReport.ts +0 -367
- package/apps/storefront/src/lib/apps/gtmMigrationTab.ts +0 -315
- package/apps/storefront/src/lib/apps/healthAggregate.ts +0 -66
- package/apps/storefront/src/lib/apps/integrationLifecycle.ts +0 -614
- package/apps/storefront/src/lib/apps/integrationLifecycleController.ts +0 -125
- package/apps/storefront/src/lib/apps/integrationReadModel.ts +0 -563
- package/apps/storefront/src/lib/apps/integrationsTab.ts +0 -547
- package/apps/storefront/src/lib/apps/integrationsView.ts +0 -106
- package/apps/storefront/src/lib/apps/managedAnalyticsService.ts +0 -599
- package/apps/storefront/src/lib/apps/tenantIntegrationMaterialization.ts +0 -170
- package/apps/storefront/src/lib/apps/tokenIssuer.ts +0 -121
- package/apps/storefront/src/lib/apps/tokenVerifier.ts +0 -148
- package/apps/storefront/src/lib/apps/v84CleanCloneReplayContext.ts +0 -16
- package/apps/storefront/src/lib/apps/v84CloneLifecycleCacheEvidence.ts +0 -105
- package/apps/storefront/src/lib/apps/v84CloneLifecycleEvidence.ts +0 -177
- package/apps/storefront/src/lib/apps/v84CloneLifecycleMaterialization.ts +0 -251
- package/apps/storefront/src/lib/apps/v84CloneLifecycleSemantics.ts +0 -241
- package/apps/storefront/src/lib/apps/v84CloneMigrationEvidence.ts +0 -211
- package/apps/storefront/src/lib/apps/v84CloneNativeInstallEvidence.ts +0 -409
- package/apps/storefront/src/lib/apps/v84NonportableCodeCoaching.ts +0 -368
- package/apps/storefront/src/lib/apps/v84NonportableRouteContract.ts +0 -188
- package/apps/storefront/src/lib/apps/v84NonportableRouteDogfood.ts +0 -291
- package/apps/storefront/src/lib/apps/ws1ManagedAnalyticsClient.ts +0 -95
- package/apps/storefront/src/lib/content-edit/applyDiff.ts +0 -91
- package/apps/storefront/src/lib/content-edit/client.ts +0 -286
- package/apps/storefront/src/lib/content-edit/index.ts +0 -14
- package/apps/storefront/src/lib/content-edit/route.ts +0 -25
- package/apps/storefront/src/lib/content-edit/types.ts +0 -19
- package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +0 -164
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Operator-session resolution for the Private Apps admin API
|
|
3
|
-
* (`/api/apps/admin/*`). Mirrors
|
|
4
|
-
* `../grants/session.ts`'s owner-session resolution — same two paths, same
|
|
5
|
-
* fail-closed shape — but resolves `{ tenantId, actor }` instead of `{ owner }`:
|
|
6
|
-
*
|
|
7
|
-
* - `tenantId` is the `AppInstall.tenantId` scope. It MUST equal the
|
|
8
|
-
* host-resolved `appDomain` the OAuth token issuer checks installs
|
|
9
|
-
* against (`tokenIssuer.ts` compares `install.tenantId` to the caller's
|
|
10
|
-
* `context.locals.tenant.appDomain`) — so an app installed through this
|
|
11
|
-
* admin API is later discoverable by the SAME tenant identity a minted
|
|
12
|
-
* token is scoped to.
|
|
13
|
-
* - `actor` is an OPAQUE id for the audit log (`app_audit_log.actor` —
|
|
14
|
-
* "never email/PII", see `../d1/schema.ts`). A signed-in viewer's verified
|
|
15
|
-
* email is one-way hashed (`sha256Hex`, the same primitive `credentials.ts`
|
|
16
|
-
* uses for secrets) so distinct operators still produce distinct, stable
|
|
17
|
-
* audit actors without the trail ever holding an email address.
|
|
18
|
-
*
|
|
19
|
-
* Two resolution paths, tried in order:
|
|
20
|
-
* 1. SIGNED-IN TENANT OPERATOR (primary). `context.locals.viewer` (set by
|
|
21
|
-
* the login-gate middleware from the `tot_session` cookie) plus
|
|
22
|
-
* `context.locals.tenant`. Admitted only for an owner/admin capability
|
|
23
|
-
* on the resolved tenant — a lower-capability viewer is a resolved
|
|
24
|
-
* session with insufficient authority, so this fails closed with 403
|
|
25
|
-
* rather than silently falling through to path 2.
|
|
26
|
-
* 2. BEARER OPERATOR SECRET (fallback, headless console). Gated by a
|
|
27
|
-
* DEDICATED `APPS_ADMIN_SECRET` only — unlike grants' session, this does
|
|
28
|
-
* NOT fall back to the shared `PREVIEW_RECONCILE_SECRET`, since that
|
|
29
|
-
* secret must not double as a cross-tenant app-registry master key
|
|
30
|
-
* Unconfigured `APPS_ADMIN_SECRET`
|
|
31
|
-
* means this path is unavailable — fails closed rather than trusting
|
|
32
|
-
* another deployment's secret. The tenant is resolved server-side from
|
|
33
|
-
* `APPS_ADMIN_TENANT` env or the `X-Tot-Tenant` header (honoured only
|
|
34
|
-
* for an already-authenticated caller) — never from the request body.
|
|
35
|
-
* There is no per-caller identity on this path, so `actor` is the fixed
|
|
36
|
-
* literal below.
|
|
37
|
-
*
|
|
38
|
-
* Routes depend only on the `{ ok, tenantId, actor }` result. `authorizeAdmin`
|
|
39
|
-
* is the pure core of path 2 (unit-testable, no I/O).
|
|
40
|
-
*
|
|
41
|
-
* Console admin session. The two paths above are host-bound:
|
|
42
|
-
* they resolve exactly one tenant (`context.locals.tenant`). The
|
|
43
|
-
* `/api/apps/admin/*` surface is also reachable from the multi-tenant vendor
|
|
44
|
-
* console, where a signed-in viewer may hold memberships on several vendors —
|
|
45
|
-
* so `resolveConsoleAdminSession` mirrors `../grants/team-members.ts`'s
|
|
46
|
-
* pattern instead: `readViewerSession` (the FULL session, all `tenants[]`)
|
|
47
|
-
* plus an explicit `requestedTenant` from the request, admitted via
|
|
48
|
-
* `viewerCanAccess`/`membershipFor` (deny-by-default, staff-selection-aware).
|
|
49
|
-
* Its `role` is the caller's resolved membership role
|
|
50
|
-
* (`developer`|`appAdmin`|`appOwner`) so routes can gate mutations against
|
|
51
|
-
* `MANAGE_ROLES` — a `developer` is read-only. `resolveOperatorSession` is
|
|
52
|
-
* the single entry point routes call: it tries the console path first and
|
|
53
|
-
* only falls back to `resolveAdminSession` (path 1/2 above) when there is NO
|
|
54
|
-
* console session at all (401) — a console session that exists but lacks
|
|
55
|
-
* access to `requestedTenant` fails closed with its own 403, the same
|
|
56
|
-
* no-fall-through discipline path 1 already uses for an insufficient
|
|
57
|
-
* capability.
|
|
58
|
-
*/
|
|
59
|
-
import type { APIContext } from "astro";
|
|
60
|
-
import { sha256Hex } from "@tot/public-runtime";
|
|
61
|
-
import { readEnv } from "@/lib/env";
|
|
62
|
-
import { readViewerSession } from "@/lib/auth/route.js";
|
|
63
|
-
import { viewerCanAccess, membershipFor } from "@/lib/dashboard/tenantSelection.js";
|
|
64
|
-
import {
|
|
65
|
-
authorizeBearerOperator,
|
|
66
|
-
resolveHostBoundSession,
|
|
67
|
-
type SessionFailure,
|
|
68
|
-
} from "@/lib/auth/operatorSession.js";
|
|
69
|
-
|
|
70
|
-
/** Capabilities that make a signed-in viewer an app-registry operator for their tenant. */
|
|
71
|
-
const ADMIN_CAPABILITIES: ReadonlySet<string> = new Set(["owner", "admin"]);
|
|
72
|
-
|
|
73
|
-
/** Console membership roles authorized to mutate installs — a `developer` is read-only. */
|
|
74
|
-
export const MANAGE_ROLES: ReadonlySet<string> = new Set(["appOwner", "appAdmin"]);
|
|
75
|
-
|
|
76
|
-
/** Opaque `actor` stamped for the shared Bearer fallback — no per-caller identity to hash. */
|
|
77
|
-
const BEARER_ACTOR = "bearer-operator";
|
|
78
|
-
|
|
79
|
-
/** How many hex chars of the email digest to keep — enough entropy to distinguish operators. */
|
|
80
|
-
const ACTOR_HASH_LENGTH = 32;
|
|
81
|
-
|
|
82
|
-
/** Pure inputs to the Bearer-fallback authorization decision (env + request, no I/O). */
|
|
83
|
-
export interface AdminAuthInput {
|
|
84
|
-
/** The request `Authorization` header, if any. */
|
|
85
|
-
authHeader: string | null;
|
|
86
|
-
/** The configured operator gate secret (or undefined if unconfigured). */
|
|
87
|
-
gateSecret: string | undefined;
|
|
88
|
-
/** Server-configured tenant for this deployment (APPS_ADMIN_TENANT), if any. */
|
|
89
|
-
configuredTenant: string | undefined;
|
|
90
|
-
/** `X-Tot-Tenant` header — honoured only once the caller is authenticated. */
|
|
91
|
-
requestedTenant: string | null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export type AdminAuthResult =
|
|
95
|
-
| { ok: true; tenantId: string; actor: string; role?: string }
|
|
96
|
-
| SessionFailure;
|
|
97
|
-
|
|
98
|
-
/** The one authorized-success shape both host-bound and Bearer admin paths return. */
|
|
99
|
-
type AdminAuthOk = Extract<AdminAuthResult, { ok: true }>;
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Decide the authenticated tenant from env + request, with NO I/O —
|
|
103
|
-
* unit-testable. Fail-closed: unconfigured gate → 503, bad/absent Bearer →
|
|
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).
|
|
107
|
-
*/
|
|
108
|
-
export function authorizeAdmin(input: AdminAuthInput): AdminAuthResult {
|
|
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;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** One-way, stable pseudonym for an audit `actor` — never the email itself. */
|
|
123
|
-
async function actorFromEmail(email: string): Promise<string> {
|
|
124
|
-
return `viewer:${(await sha256Hex(email)).slice(0, ACTOR_HASH_LENGTH)}`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Resolve the authenticated `{ tenantId, actor }` for a Private Apps admin
|
|
129
|
-
* request from the runtime env + the incoming request. Never reads `tenantId`
|
|
130
|
-
* from the body.
|
|
131
|
-
*/
|
|
132
|
-
export async function resolveAdminSession(context: APIContext): Promise<AdminAuthResult> {
|
|
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: {
|
|
141
|
-
ok: false,
|
|
142
|
-
status: 403,
|
|
143
|
-
message: "not authorized to manage apps for this tenant",
|
|
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
|
-
},
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Resolve a signed-in console viewer's admin session for `requestedTenant`
|
|
169
|
-
* (the `?tenant=` query param every admin route now reads) — the multi-tenant
|
|
170
|
-
* counterpart to `resolveAdminSession`'s host-bound path 1, mirroring
|
|
171
|
-
* `../grants/team-members.ts`'s `readViewerSession` + `viewerCanAccess`
|
|
172
|
-
* pattern instead of the single-tenant `locals.tenant`/`locals.viewer` gate.
|
|
173
|
-
* Fail-closed: no session → 401 (the signal `resolveOperatorSession` uses to
|
|
174
|
-
* fall back to the host-bound/Bearer path); a session with no membership or
|
|
175
|
-
* live staff admission for `requestedTenant` → 403 (does NOT fall back — an
|
|
176
|
-
* authenticated-but-denied caller is not the "no console session" case).
|
|
177
|
-
* `role` is the caller's resolved membership role for routes to gate
|
|
178
|
-
* mutations against `MANAGE_ROLES`.
|
|
179
|
-
*/
|
|
180
|
-
export async function resolveConsoleAdminSession(
|
|
181
|
-
context: APIContext,
|
|
182
|
-
requestedTenant: string | null,
|
|
183
|
-
): Promise<AdminAuthResult> {
|
|
184
|
-
const session = await readViewerSession(context);
|
|
185
|
-
if (!session) return { ok: false, status: 401, message: "not signed in" };
|
|
186
|
-
|
|
187
|
-
const tenantId = (requestedTenant ?? "").trim();
|
|
188
|
-
const staff = { selection: session.staffSelection, nowSeconds: Math.floor(Date.now() / 1000) };
|
|
189
|
-
if (!viewerCanAccess(session.tenants, tenantId, staff)) {
|
|
190
|
-
return {
|
|
191
|
-
ok: false,
|
|
192
|
-
status: 403,
|
|
193
|
-
message: "not authorized to manage apps for this tenant",
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
const role = membershipFor(session.tenants, tenantId, staff)?.role ?? "developer";
|
|
197
|
-
return { ok: true, tenantId, actor: await actorFromEmail(session.email), role };
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* The single entry point admin routes call: try the signed-in console
|
|
202
|
-
* viewer's session first, and only fall back to the host-bound/Bearer path
|
|
203
|
-
* (`resolveAdminSession`) when there is no console session at all (401) — see
|
|
204
|
-
* {@link resolveConsoleAdminSession}'s doc for why a denied console session
|
|
205
|
-
* (403) does not fall through.
|
|
206
|
-
*/
|
|
207
|
-
export async function resolveOperatorSession(
|
|
208
|
-
context: APIContext,
|
|
209
|
-
requestedTenant: string | null,
|
|
210
|
-
): Promise<AdminAuthResult> {
|
|
211
|
-
const consoleAuth = await resolveConsoleAdminSession(context, requestedTenant);
|
|
212
|
-
if (consoleAuth.ok || consoleAuth.status !== 401) return consoleAuth;
|
|
213
|
-
return resolveAdminSession(context);
|
|
214
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The scoped-API authorization boundary — every
|
|
3
|
-
* `/api/apps/v1/*` route calls `authorizeAppRequest` FIRST. Verifies the
|
|
4
|
-
* app-gateway bearer JWT (`AppTokenVerifier`), resolves the calling
|
|
5
|
-
* install from the registry, and enforces the single scope the route
|
|
6
|
-
* requires per `docs/private-apps/contract/openapi.yaml`'s `x-requiredScope`.
|
|
7
|
-
*
|
|
8
|
-
* Tenant is ALWAYS the host-resolved `context.locals.tenant.appDomain` —
|
|
9
|
-
* never a header or query param — the same binding `tokenIssuer.ts` and
|
|
10
|
-
* `oauth/token.ts` use, so a token can't be replayed against a different
|
|
11
|
-
* tenant than the one it was minted for.
|
|
12
|
-
*
|
|
13
|
-
* Fail-closed throughout: an unconfigured signing key, a missing/invalid/
|
|
14
|
-
* expired token, and an install that's gone missing/cross-tenant/suspended/
|
|
15
|
-
* uninstalled are ALL reported identically as the generic 401 the contract's
|
|
16
|
-
* `Unauthorized` response documents (no detail that would let a caller
|
|
17
|
-
* distinguish "wrong tenant" from "suspended" from "never existed" — the
|
|
18
|
-
* same no-oracle discipline `registryService.ts`'s `mustFindRow` and
|
|
19
|
-
* `tokenIssuer.ts`'s `invalid_client` already keep).
|
|
20
|
-
*/
|
|
21
|
-
import type { APIContext } from "astro";
|
|
22
|
-
import { getAppTokenVerifier, type AppTokenClaims } from "./tokenVerifier.js";
|
|
23
|
-
import { getRegistryService } from "./adminService.js";
|
|
24
|
-
import type { AppInstall } from "./registryService.js";
|
|
25
|
-
import { json } from "./apiRoute.js";
|
|
26
|
-
|
|
27
|
-
export interface AuthorizedAppRequest {
|
|
28
|
-
ok: true;
|
|
29
|
-
claims: AppTokenClaims;
|
|
30
|
-
install: AppInstall;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export type AuthorizeAppResult = AuthorizedAppRequest | { ok: false; response: Response };
|
|
34
|
-
|
|
35
|
-
/** Generic, no-detail 401 — see the fail-closed note above. */
|
|
36
|
-
function unauthorized(): Response {
|
|
37
|
-
return json({ error: "Unauthorized", code: "invalid_token" }, 401);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Authorize one `/api/apps/v1/*` request. Pass the single scope id the route
|
|
42
|
-
* requires (per the contract's `x-requiredScope`), or `null` for the
|
|
43
|
-
* metadata endpoints that only require a valid token.
|
|
44
|
-
*/
|
|
45
|
-
export async function authorizeAppRequest(
|
|
46
|
-
context: APIContext,
|
|
47
|
-
requiredScope: string | null,
|
|
48
|
-
): Promise<AuthorizeAppResult> {
|
|
49
|
-
const tenant = context.locals.tenant.appDomain;
|
|
50
|
-
|
|
51
|
-
const verifier = await getAppTokenVerifier(tenant);
|
|
52
|
-
if (!verifier) {
|
|
53
|
-
return { ok: false, response: json({ error: "app gateway not configured", code: "unconfigured" }, 503) };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const authHeader = context.request.headers.get("authorization");
|
|
57
|
-
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice("Bearer ".length).trim() : "";
|
|
58
|
-
if (!token) {
|
|
59
|
-
return { ok: false, response: unauthorized() };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
let claims: AppTokenClaims;
|
|
63
|
-
try {
|
|
64
|
-
claims = await verifier.verify(token);
|
|
65
|
-
} catch {
|
|
66
|
-
return { ok: false, response: unauthorized() };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const registry = await getRegistryService();
|
|
70
|
-
if (registry instanceof Response) return { ok: false, response: registry };
|
|
71
|
-
|
|
72
|
-
const install = await registry.get(claims.sub);
|
|
73
|
-
if (!install || install.tenantId !== tenant || install.status !== "active") {
|
|
74
|
-
return { ok: false, response: unauthorized() };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Enforce the scope against BOTH the live install grant and the token claims.
|
|
78
|
-
// install.scopes is the authoritative, real-time grant — so an operator
|
|
79
|
-
// narrowing scopes via update() takes effect immediately (not only after the
|
|
80
|
-
// ~300s token TTL), matching how install *status* is already live-checked
|
|
81
|
-
// above. Requiring it in the token too keeps a token from ever exceeding the
|
|
82
|
-
// grant it was minted with. Fail-closed: absent from either → denied.
|
|
83
|
-
if (
|
|
84
|
-
requiredScope &&
|
|
85
|
-
!(install.scopes.includes(requiredScope) && claims.scopes.includes(requiredScope))
|
|
86
|
-
) {
|
|
87
|
-
return { ok: false, response: json({ error: "scope_denied", code: "scope_denied" }, 403) };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return { ok: true, claims, install };
|
|
91
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Response helpers shared by every `/api/apps/v1/*` route — a no-store JSON
|
|
3
|
-
* responder (mirrors `../grants/route.ts` /
|
|
4
|
-
* `./adminService.ts`'s pattern) plus the `tot-request-id` / `traceparent`
|
|
5
|
-
* response headers `docs/private-apps/contract/openapi.yaml` documents on
|
|
6
|
-
* every operation.
|
|
7
|
-
*/
|
|
8
|
-
import type { APIContext } from "astro";
|
|
9
|
-
|
|
10
|
-
/** No-store JSON response — scoped-app API bodies must never be edge/browser-cached. */
|
|
11
|
-
export function json(body: unknown, status = 200): Response {
|
|
12
|
-
return new Response(JSON.stringify(body), {
|
|
13
|
-
status,
|
|
14
|
-
headers: { "content-type": "application/json", "cache-control": "no-store" },
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** The W3C Trace Context shape the contract's `Traceparent` parameter/header pins. */
|
|
19
|
-
const TRACEPARENT_RE = /^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Stamp a fresh `tot-request-id` on every response, and echo `traceparent`
|
|
23
|
-
* back ONLY when the request sent a well-formed one — never fabricate a
|
|
24
|
-
* trace context the caller didn't start. Callers apply this to BOTH success
|
|
25
|
-
* and error responses (including `authorizeAppRequest`'s), so every response
|
|
26
|
-
* off a `/api/apps/v1/*` route carries a request id.
|
|
27
|
-
*/
|
|
28
|
-
export function withRequestId(res: Response, context: Pick<APIContext, "request">): Response {
|
|
29
|
-
res.headers.set("tot-request-id", crypto.randomUUID());
|
|
30
|
-
const traceparent = context.request.headers.get("traceparent");
|
|
31
|
-
if (traceparent && TRACEPARENT_RE.test(traceparent)) {
|
|
32
|
-
res.headers.set("traceparent", traceparent);
|
|
33
|
-
}
|
|
34
|
-
return res;
|
|
35
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Maps the internal `CatalogProduct` read model to the scoped-API contract's
|
|
3
|
-
* `Product` shape (`docs/private-apps/contract/openapi.yaml`, schema
|
|
4
|
-
* `Product` — `additionalProperties: false`). Drops every internal field
|
|
5
|
-
* the contract doesn't declare (variants, images, metafields, source, …) so
|
|
6
|
-
* an app only ever sees the minimized public catalog surface.
|
|
7
|
-
*/
|
|
8
|
-
import type { CatalogProduct } from "@tot/public-runtime";
|
|
9
|
-
|
|
10
|
-
export interface ContractProduct {
|
|
11
|
-
id: string;
|
|
12
|
-
handle: string;
|
|
13
|
-
title: string;
|
|
14
|
-
status: "active" | "draft" | "archived";
|
|
15
|
-
vendor?: string;
|
|
16
|
-
productType?: string;
|
|
17
|
-
tags?: string[];
|
|
18
|
-
priceRange: { min: number; max: number; currency: string };
|
|
19
|
-
updatedAt?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Project one `CatalogProduct` into the contract's `Product` — nothing else survives. */
|
|
23
|
-
export function toContractProduct(p: CatalogProduct): ContractProduct {
|
|
24
|
-
return {
|
|
25
|
-
id: p.id,
|
|
26
|
-
handle: p.handle,
|
|
27
|
-
title: p.title,
|
|
28
|
-
status: p.status,
|
|
29
|
-
vendor: p.vendor,
|
|
30
|
-
productType: p.product_type,
|
|
31
|
-
tags: p.tags,
|
|
32
|
-
priceRange: {
|
|
33
|
-
min: p.price_range.min,
|
|
34
|
-
max: p.price_range.max,
|
|
35
|
-
currency: p.price_range.currency,
|
|
36
|
-
},
|
|
37
|
-
updatedAt: p.updated_at,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The merchant consent configuration store — the durable spine behind ext-admin-03.
|
|
3
|
-
* It persists a tenant's consent configuration as an APPEND-ONLY revision history and
|
|
4
|
-
* the demonstrated-CMP-demand ledger, and reads back exactly the shapes the pure
|
|
5
|
-
* ext-policy-03 layer consumes.
|
|
6
|
-
*
|
|
7
|
-
* Two acceptance properties are schema-level, not conventions:
|
|
8
|
-
* - AUDITED — every save is a new immutable revision that names its actor and time;
|
|
9
|
-
* nothing is mutated in place, so the revision list IS the audit trail.
|
|
10
|
-
* - REVERSIBLE — a revert is a forward-only new revision that copies a prior config
|
|
11
|
-
* and records `revertedFrom`; history is never rewritten, and rolling back leaves
|
|
12
|
-
* its own audited entry.
|
|
13
|
-
*
|
|
14
|
-
* Pure over the `Queryable` seam (same shape `CandidateStore`/`OrdersStore` use), so it
|
|
15
|
-
* runs unchanged against real D1 in the Worker (`fromD1(env.STOREFRONT_APPS_DB)`) and an
|
|
16
|
-
* in-memory `node:sqlite` in tests. This is the DATA MODEL + repository API only — the
|
|
17
|
-
* validation (`validateConsentConfig`) and the broker materialization
|
|
18
|
-
* (`toTenantConsentPolicy`/`buildTenantConsentBroker`) live in `@tot/public-runtime`;
|
|
19
|
-
* this store persists already-validated JSON and never re-derives policy.
|
|
20
|
-
*/
|
|
21
|
-
import type { Queryable } from "../d1/catalog.js";
|
|
22
|
-
import type { CmpDemandLedger, TenantConsentConfig } from "@tot/public-runtime";
|
|
23
|
-
|
|
24
|
-
/** One persisted consent-config revision — an immutable audit-trail entry. */
|
|
25
|
-
export interface ConsentConfigRevision {
|
|
26
|
-
id: string;
|
|
27
|
-
tenantId: string;
|
|
28
|
-
/** Monotonic per tenant; the largest is the current config. */
|
|
29
|
-
revision: number;
|
|
30
|
-
config: TenantConsentConfig;
|
|
31
|
-
/** Opaque actor id — never PII. */
|
|
32
|
-
actor: string;
|
|
33
|
-
note: string | null;
|
|
34
|
-
/** The source revision when this one is a revert, else null. */
|
|
35
|
-
revertedFrom: number | null;
|
|
36
|
-
createdAt: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Save a new revision of a tenant's (already-validated) consent configuration. */
|
|
40
|
-
export interface SaveConsentConfigInput {
|
|
41
|
-
tenantId: string;
|
|
42
|
-
config: TenantConsentConfig;
|
|
43
|
-
actor: string;
|
|
44
|
-
note?: string | null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Revert a tenant's consent configuration to a prior revision (forward-only). */
|
|
48
|
-
export interface RevertConsentConfigInput {
|
|
49
|
-
tenantId: string;
|
|
50
|
-
/** The revision whose config to restore as a NEW revision. */
|
|
51
|
-
toRevision: number;
|
|
52
|
-
actor: string;
|
|
53
|
-
note?: string | null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Record demonstrated demand for a CMP adapter from one tenant. */
|
|
57
|
-
export interface RecordCmpDemandInput {
|
|
58
|
-
tenantId: string;
|
|
59
|
-
adapterId: string;
|
|
60
|
-
source?: string | null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
interface RevisionRow {
|
|
64
|
-
id: string;
|
|
65
|
-
tenant_id: string;
|
|
66
|
-
revision: number;
|
|
67
|
-
config: string;
|
|
68
|
-
actor: string;
|
|
69
|
-
note: string | null;
|
|
70
|
-
reverted_from: number | null;
|
|
71
|
-
created_at: string;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
interface DemandRow {
|
|
75
|
-
id: string;
|
|
76
|
-
tenant_id: string;
|
|
77
|
-
adapter_id: string;
|
|
78
|
-
source: string | null;
|
|
79
|
-
at: string;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function toRevision(row: RevisionRow): ConsentConfigRevision {
|
|
83
|
-
return {
|
|
84
|
-
id: row.id,
|
|
85
|
-
tenantId: row.tenant_id,
|
|
86
|
-
revision: row.revision,
|
|
87
|
-
config: JSON.parse(row.config) as TenantConsentConfig,
|
|
88
|
-
actor: row.actor,
|
|
89
|
-
note: row.note,
|
|
90
|
-
revertedFrom: row.reverted_from,
|
|
91
|
-
createdAt: row.created_at,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export class ConsentConfigStore {
|
|
96
|
-
constructor(
|
|
97
|
-
private readonly db: Queryable,
|
|
98
|
-
private readonly now: () => string = () => new Date().toISOString(),
|
|
99
|
-
private readonly newId: () => string = () => crypto.randomUUID(),
|
|
100
|
-
) {}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Append a new revision of a tenant's consent config. The revision number is
|
|
104
|
-
* assigned as `MAX(revision)+1` for that tenant; the UNIQUE `(tenant_id, revision)`
|
|
105
|
-
* index makes a concurrent double-assignment fail loudly rather than overwrite an
|
|
106
|
-
* earlier entry. The prior revisions are never touched — the history is the audit.
|
|
107
|
-
*/
|
|
108
|
-
async saveRevision(input: SaveConsentConfigInput): Promise<ConsentConfigRevision> {
|
|
109
|
-
return this.appendRevision(input.tenantId, input.config, input.actor, input.note ?? null, null);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Restore a prior revision as a NEW revision (forward-only revert). Reads the target
|
|
114
|
-
* revision's config and appends it again with `revertedFrom` set — so a rollback is
|
|
115
|
-
* itself an audited entry and no history is rewritten. Throws if the target is absent.
|
|
116
|
-
*/
|
|
117
|
-
async revertTo(input: RevertConsentConfigInput): Promise<ConsentConfigRevision> {
|
|
118
|
-
const target = await this.getRevision(input.tenantId, input.toRevision);
|
|
119
|
-
if (!target) {
|
|
120
|
-
throw new Error(
|
|
121
|
-
`Cannot revert tenant "${input.tenantId}" to revision ${input.toRevision}: no such revision.`,
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
const note = input.note ?? `Revert to revision ${input.toRevision}.`;
|
|
125
|
-
return this.appendRevision(input.tenantId, target.config, input.actor, note, input.toRevision);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
private async appendRevision(
|
|
129
|
-
tenantId: string,
|
|
130
|
-
config: TenantConsentConfig,
|
|
131
|
-
actor: string,
|
|
132
|
-
note: string | null,
|
|
133
|
-
revertedFrom: number | null,
|
|
134
|
-
): Promise<ConsentConfigRevision> {
|
|
135
|
-
const maxRow = await this.db.first<{ max_revision: number | null }>(
|
|
136
|
-
`SELECT MAX(revision) AS max_revision FROM consent_config_revisions WHERE tenant_id = ?`,
|
|
137
|
-
tenantId,
|
|
138
|
-
);
|
|
139
|
-
const revision = (maxRow?.max_revision ?? 0) + 1;
|
|
140
|
-
const createdAt = this.now();
|
|
141
|
-
await this.db.run(
|
|
142
|
-
`INSERT INTO consent_config_revisions
|
|
143
|
-
(id, tenant_id, revision, config, actor, note, reverted_from, created_at)
|
|
144
|
-
VALUES (?,?,?,?,?,?,?,?)`,
|
|
145
|
-
`ccr_${this.newId()}`,
|
|
146
|
-
tenantId,
|
|
147
|
-
revision,
|
|
148
|
-
JSON.stringify(config),
|
|
149
|
-
actor,
|
|
150
|
-
note,
|
|
151
|
-
revertedFrom,
|
|
152
|
-
createdAt,
|
|
153
|
-
);
|
|
154
|
-
const row = await this.db.first<RevisionRow>(
|
|
155
|
-
`SELECT * FROM consent_config_revisions WHERE tenant_id = ? AND revision = ?`,
|
|
156
|
-
tenantId,
|
|
157
|
-
revision,
|
|
158
|
-
);
|
|
159
|
-
return toRevision(row!);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/** The current (latest) revision of a tenant's consent config, or null if none saved. */
|
|
163
|
-
async currentRevision(tenantId: string): Promise<ConsentConfigRevision | null> {
|
|
164
|
-
const row = await this.db.first<RevisionRow>(
|
|
165
|
-
`SELECT * FROM consent_config_revisions WHERE tenant_id = ?
|
|
166
|
-
ORDER BY revision DESC LIMIT 1`,
|
|
167
|
-
tenantId,
|
|
168
|
-
);
|
|
169
|
-
return row ? toRevision(row) : null;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** A specific revision, or null if absent. */
|
|
173
|
-
async getRevision(tenantId: string, revision: number): Promise<ConsentConfigRevision | null> {
|
|
174
|
-
const row = await this.db.first<RevisionRow>(
|
|
175
|
-
`SELECT * FROM consent_config_revisions WHERE tenant_id = ? AND revision = ?`,
|
|
176
|
-
tenantId,
|
|
177
|
-
revision,
|
|
178
|
-
);
|
|
179
|
-
return row ? toRevision(row) : null;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** A tenant's full revision history, newest first — the audit trail. */
|
|
183
|
-
async listRevisions(tenantId: string): Promise<ConsentConfigRevision[]> {
|
|
184
|
-
const rows = await this.db.all<RevisionRow>(
|
|
185
|
-
`SELECT * FROM consent_config_revisions WHERE tenant_id = ? ORDER BY revision DESC`,
|
|
186
|
-
tenantId,
|
|
187
|
-
);
|
|
188
|
-
return rows.map(toRevision);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Record demonstrated demand for a CMP adapter from one tenant. Idempotent per
|
|
193
|
-
* `(tenant, adapter)` — a tenant re-asking updates the recorded time/source on the
|
|
194
|
-
* SAME row and never inflates the distinct-tenant demand count.
|
|
195
|
-
*/
|
|
196
|
-
async recordCmpDemand(input: RecordCmpDemandInput): Promise<void> {
|
|
197
|
-
await this.db.run(
|
|
198
|
-
`INSERT INTO consent_cmp_demand (id, tenant_id, adapter_id, source, at)
|
|
199
|
-
VALUES (?,?,?,?,?)
|
|
200
|
-
ON CONFLICT(tenant_id, adapter_id) DO UPDATE SET
|
|
201
|
-
source = excluded.source,
|
|
202
|
-
at = excluded.at`,
|
|
203
|
-
`cmd_${this.newId()}`,
|
|
204
|
-
input.tenantId,
|
|
205
|
-
input.adapterId,
|
|
206
|
-
input.source ?? null,
|
|
207
|
-
this.now(),
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* The whole demand ledger, in the `@tot/public-runtime` shape — passed straight into
|
|
213
|
-
* `validateConsentConfig` so a CMP choice is admitted only from demonstrated demand.
|
|
214
|
-
*/
|
|
215
|
-
async demandLedger(): Promise<CmpDemandLedger> {
|
|
216
|
-
const rows = await this.db.all<DemandRow>(
|
|
217
|
-
`SELECT * FROM consent_cmp_demand ORDER BY adapter_id ASC, tenant_id ASC`,
|
|
218
|
-
);
|
|
219
|
-
return {
|
|
220
|
-
signals: rows.map((r) => ({
|
|
221
|
-
adapterId: r.adapter_id,
|
|
222
|
-
tenantPublicId: r.tenant_id,
|
|
223
|
-
at: r.at,
|
|
224
|
-
source: r.source ?? undefined,
|
|
225
|
-
})),
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
}
|