@substrat-run/adapter-cloudflare 0.32.0 → 0.34.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/dist/control-plane-do.d.ts +46 -0
- package/dist/control-plane-do.d.ts.map +1 -1
- package/dist/control-plane-do.js +79 -0
- package/dist/control-plane-do.js.map +1 -1
- package/dist/d1.d.ts +55 -0
- package/dist/d1.d.ts.map +1 -0
- package/dist/d1.js +128 -0
- package/dist/d1.js.map +1 -0
- package/dist/host.d.ts +35 -5
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +148 -18
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +50 -1
- package/dist/scope-do.js.map +1 -1
- package/package.json +4 -4
package/dist/host.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { type ConnectionId, type ModuleId, type EntitlementGrant, type PlatformActorId, type PrincipalId, type QueryScopeInput, type ReadScopeTableInput, type RoleDefinition, type ScopeDump, type ScopeDumpTable, type ScopeId, type PlatformRequest, type PlatformRequestId, type PlatformRequestStatus, type ScopeTable, type ScopeQueryResult, type ScopeTablePage, type TenantId, type TenantStoreHandle } from '@substrat-run/contracts';
|
|
1
|
+
import { type ConnectionId, type ModuleId, type EntitlementGrant, type ProjectedIdentityLink, type PlatformActorId, type PrincipalId, type ResolvedIdentity, type QueryScopeInput, type ReadScopeTableInput, type RoleDefinition, type ScopeDump, type ScopeDumpTable, type ScopeId, type PlatformRequest, type PlatformRequestId, type PlatformRequestStatus, type ScopeTable, type ScopeQueryResult, type ScopeTablePage, type TenantId, type TenantStoreHandle } from '@substrat-run/contracts';
|
|
2
2
|
import { type ExecutorDeadLetter, type ExecutorDrainReport, type ExecutorHandler, type ExecutorRetryPolicy, type MigrateScopeOutcome, type MigrationFrontier, type ConnectorHandler, type ConnectorOptions, type FetchLike, type SecretBox, type HostAdmin, type ModuleRegistration, type OperationHandler, type PermissionChecker, type ProvisionScopeInput, type ScheduleRegistration, type ScheduleRunReport, type ScopeHost, type ScopeStub, type TenantRelationalStore, type TenantStoreProvisionInput } from '@substrat-run/kernel';
|
|
3
|
+
import { type D1TenantStores } from './d1.js';
|
|
3
4
|
export interface CloudflareScopeHostOptions {
|
|
4
5
|
scope: DurableObjectNamespace;
|
|
5
6
|
/**
|
|
@@ -39,6 +40,15 @@ export interface CloudflareScopeHostOptions {
|
|
|
39
40
|
* it for existing scopes wants a one-time `reconcileTenantProjection` back-fill.
|
|
40
41
|
*/
|
|
41
42
|
scopeLocalPermissions?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The live D1 client for per-tenant relational stores (#301) —
|
|
45
|
+
* `createD1TenantStores` with the platform's Cloudflare credential. Lives on the
|
|
46
|
+
* COORDINATOR like `secretBox`: the ControlPlaneDO keeps the ledger and has never
|
|
47
|
+
* held the credential. Omitted (dev, CP-less verticals, a deployment with no D1
|
|
48
|
+
* credential), `provisionTenantStore`/`openTenantStore` refuse loudly rather than
|
|
49
|
+
* letting a declared `tenantStoreNeed` appear provisioned while no store exists.
|
|
50
|
+
*/
|
|
51
|
+
tenantStores?: D1TenantStores;
|
|
42
52
|
}
|
|
43
53
|
export declare class CloudflareScopeHost implements ScopeHost {
|
|
44
54
|
readonly admin: HostAdmin;
|
|
@@ -56,6 +66,8 @@ export declare class CloudflareScopeHost implements ScopeHost {
|
|
|
56
66
|
/** Executor id → {eventType, handler} (K-22 §4.2). Coordinator-side, not in the DO. */
|
|
57
67
|
private readonly secretBox;
|
|
58
68
|
private readonly fetchImpl;
|
|
69
|
+
/** The live D1 client for per-tenant stores (#301); undefined ⇒ refuse loudly. */
|
|
70
|
+
private readonly tenantStores?;
|
|
59
71
|
private readonly executors;
|
|
60
72
|
/**
|
|
61
73
|
* The event currently being effected, stamped onto admin rows the executor writes.
|
|
@@ -212,8 +224,10 @@ export declare class CloudflareScopeHost implements ScopeHost {
|
|
|
212
224
|
defineOperation<I, O>(name: string, _handler: OperationHandler<I, O>): void;
|
|
213
225
|
private bindOperation;
|
|
214
226
|
provisionScope(actor: PlatformActorId, input: ProvisionScopeInput): Promise<void>;
|
|
215
|
-
provisionTenantStore(
|
|
227
|
+
provisionTenantStore(actor: PlatformActorId, input: TenantStoreProvisionInput): Promise<TenantStoreHandle>;
|
|
216
228
|
openTenantStore(handle: TenantStoreHandle): TenantRelationalStore;
|
|
229
|
+
/** The injected D1 client, or a loud refusal naming what to configure. */
|
|
230
|
+
private requireTenantStores;
|
|
217
231
|
importScope(actor: PlatformActorId, input: ProvisionScopeInput, dump: ScopeDump): Promise<void>;
|
|
218
232
|
restoreScope(actor: PlatformActorId, tenantId: TenantId, scopeId: ScopeId, dump: ScopeDump): Promise<void>;
|
|
219
233
|
snapshotScope(actor: PlatformActorId, tenantId: TenantId, scopeId: ScopeId, opts?: {
|
|
@@ -266,9 +280,10 @@ export declare class CloudflareScopeHost implements ScopeHost {
|
|
|
266
280
|
private recordAdmin;
|
|
267
281
|
private writeScopeTuple;
|
|
268
282
|
private scopeStub;
|
|
269
|
-
/** The tenant's current roles + tenant-level tuples + entitlements, in the
|
|
270
|
-
* stores. Entitlements (#304) ride the same projection so a hosted scope
|
|
271
|
-
* expiry locally; expiry is applied at READ (in the scope), so the full list
|
|
283
|
+
/** The tenant's current roles + tenant-level tuples + entitlements + identity links, in the
|
|
284
|
+
* shape the ScopeDO stores. Entitlements (#304) ride the same projection so a hosted scope
|
|
285
|
+
* reads plan/quota/expiry locally; expiry is applied at READ (in the scope), so the full list
|
|
286
|
+
* is carried. Identity links (#406) ride it too, so a scope resolves logins locally. */
|
|
272
287
|
private tenantProjection;
|
|
273
288
|
/** Project the tenant's current state into ONE scope + flip it to local. */
|
|
274
289
|
private projectScope;
|
|
@@ -307,7 +322,22 @@ export declare class CloudflareScopeHost implements ScopeHost {
|
|
|
307
322
|
* the scope's per-operation gate + `ctx.entitlement` read them locally. Absent ⇒ none
|
|
308
323
|
* projected (the gate then fails closed for any gated operation until a projection lands). */
|
|
309
324
|
entitlements?: EntitlementGrant[];
|
|
325
|
+
/** The tenant's identity links, passed by the platform at provision (#406) — projected so
|
|
326
|
+
* the vertical's auth adapter resolves `(provider, externalId) → principal` from the
|
|
327
|
+
* scope's own storage (`resolveIdentityLocal`). Absent ⇒ untouched, so a provision path
|
|
328
|
+
* predating #406 never wipes links a fan-out or reconcile already delivered. */
|
|
329
|
+
identityLinks?: ProjectedIdentityLink[];
|
|
310
330
|
}): Promise<void>;
|
|
331
|
+
/**
|
|
332
|
+
* Resolve an external identity from a scope's PROJECTED links (#406) — the CP-less
|
|
333
|
+
* auth adapter's read path, the local equivalent of `HostAdmin.resolveIdentity` (same
|
|
334
|
+
* exemption: a machine read on the request path, not audited). Reads only what the
|
|
335
|
+
* platform projected into the scope (provision / reconcile / fan-out), so a miss means
|
|
336
|
+
* "unknown login — deny": an un-projected scope can only refuse a legitimate login,
|
|
337
|
+
* never admit a revoked one. A CP-full deployment keeps using `admin.resolveIdentity`;
|
|
338
|
+
* this exists precisely for deployments where that surface is unavailable.
|
|
339
|
+
*/
|
|
340
|
+
resolveIdentityLocal(tenantId: TenantId, scopeId: ScopeId, provider: string, externalId: string): Promise<ResolvedIdentity | undefined>;
|
|
311
341
|
/**
|
|
312
342
|
* Grant a principal a role at SCOPE level in a CP-less vertical — the MEMBER half of
|
|
313
343
|
* `provisionScopeLocal`'s owner grant. An invite-accept flow calls this so a newly-invited
|
package/dist/host.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AAAA,OAAO,EAsCL,KAAK,YAAY,EAEjB,KAAK,QAAQ,EAUb,KAAK,gBAAgB,
|
|
1
|
+
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"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"}
|
package/dist/host.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { accessLogEntry, adminLogEntry, createTenantInput, identityLink, identityPool, createOrgInput, promotionAcknowledgement, bindHostnameInput, channelHistoryEntry, hostnameBinding, publishVersionInput, AUTO_ADMISSION_NOTE, registerVerticalInput, vertical as verticalSchema, verticalServingState, verticalChannel, verticalVersion, connection, connectionGrant, connectionSecret, systemGrant, entitlementGrant, entitlementGrantInput, subjectRef, createConnectionInput, moduleManifest, org as orgSchema, orgMembership, resolvedIdentity, roleDefinition, scope as scopeSchema, tenant as tenantSchema, tenantRole, platformRequest, } from '@substrat-run/contracts';
|
|
2
2
|
import { normalizeHostname, toRouteTarget } from './route-resolver.js';
|
|
3
3
|
import { resolveScopeRecord, ulid, backoffAt, resolveRetryPolicy, unconfiguredSecretBox, } from '@substrat-run/kernel';
|
|
4
|
+
import { tenantStoreDatabaseName } from './d1.js';
|
|
4
5
|
/** DO row → contract shape. Never reads the secrets table — that is the split. */
|
|
5
6
|
const toConnection = (r) => connection.parse({
|
|
6
7
|
id: r.id,
|
|
@@ -82,6 +83,8 @@ export class CloudflareScopeHost {
|
|
|
82
83
|
/** Executor id → {eventType, handler} (K-22 §4.2). Coordinator-side, not in the DO. */
|
|
83
84
|
secretBox;
|
|
84
85
|
fetchImpl;
|
|
86
|
+
/** The live D1 client for per-tenant stores (#301); undefined ⇒ refuse loudly. */
|
|
87
|
+
tenantStores;
|
|
85
88
|
executors = new Map();
|
|
86
89
|
/**
|
|
87
90
|
* The event currently being effected, stamped onto admin rows the executor writes.
|
|
@@ -105,6 +108,7 @@ export class CloudflareScopeHost {
|
|
|
105
108
|
*/
|
|
106
109
|
constructor(options) {
|
|
107
110
|
this.secretBox = options.secretBox ?? unconfiguredSecretBox;
|
|
111
|
+
this.tenantStores = options.tenantStores;
|
|
108
112
|
this.fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
109
113
|
this.scopeLocalPermissions = options.scopeLocalPermissions ?? false;
|
|
110
114
|
this.scopeNs = options.scope;
|
|
@@ -498,17 +502,79 @@ export class CloudflareScopeHost {
|
|
|
498
502
|
await this.recordAdmin(actor, 'provisionScope', { tenantId: input.tenantId, scopeId: input.scopeId, vertical: record.vertical }, null, record);
|
|
499
503
|
}
|
|
500
504
|
}
|
|
501
|
-
// Per-tenant relational stores (#301). The
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
505
|
+
// Per-tenant relational stores (#301, PR-2 — the live D1 path). The coordinator holds
|
|
506
|
+
// the platform's D1 credential (the same split secretBox uses: the DO records, it never
|
|
507
|
+
// holds a key), the ControlPlaneDO serializes the ledger write, and the D1 REST client
|
|
508
|
+
// does the actual mint. Unconfigured (dev without a CF credential, a CP-less vertical),
|
|
509
|
+
// these fail loudly rather than silently no-op — a hosted vertical that declares a
|
|
510
|
+
// `tenantStoreNeed` must not appear provisioned while its store does not exist.
|
|
511
|
+
async provisionTenantStore(actor, input) {
|
|
512
|
+
const d1 = this.requireTenantStores(`provisionTenantStore(tenant=${input.tenantId} vertical=${input.vertical} binding=${input.binding})`);
|
|
513
|
+
// Fail closed on an unknown/non-active tenant, exactly as provisionScope does (§4.1's
|
|
514
|
+
// "tenant is an FK string" hole). Checked here for the honest error; re-checked
|
|
515
|
+
// inside the DO's ledger write, which is the serialization point that actually holds.
|
|
516
|
+
const tenant = await this.cp.getTenant(input.tenantId);
|
|
517
|
+
if (!tenant) {
|
|
518
|
+
throw new Error(`cannot provision tenant store under unknown tenant: ${input.tenantId}`);
|
|
519
|
+
}
|
|
520
|
+
if (tenant.status !== 'active') {
|
|
521
|
+
throw new Error(`cannot provision tenant store under non-active tenant (status: ${tenant.status}): ${input.tenantId}`);
|
|
522
|
+
}
|
|
523
|
+
// Idempotent on (tenant, vertical, binding): a retried provision re-resolves the SAME
|
|
524
|
+
// store rather than minting a second database (the K-31 ready-gate retries the whole
|
|
525
|
+
// callback). An existing row short-circuits before any Cloudflare call and is NOT
|
|
526
|
+
// re-audited — nothing changed.
|
|
527
|
+
const existing = await this.cp.getTenantStore(input.tenantId, input.vertical, input.binding);
|
|
528
|
+
if (existing)
|
|
529
|
+
return { binding: input.binding, kind: 'relational', ref: existing.ref };
|
|
530
|
+
// Mint the database, then record it. The name is deterministic so a provision that
|
|
531
|
+
// crashed BETWEEN these two steps converges on the same database on retry (create
|
|
532
|
+
// resolves a name collision to the existing id); the ledger row — not the name — is
|
|
533
|
+
// the source of truth, carrying the D1 database_id Cloudflare assigned as the ref.
|
|
534
|
+
const name = await tenantStoreDatabaseName(input.tenantId, input.vertical, input.binding);
|
|
535
|
+
const ref = await d1.create(name);
|
|
536
|
+
const stored = await this.cp.putTenantStore({
|
|
537
|
+
tenantId: input.tenantId,
|
|
538
|
+
vertical: input.vertical,
|
|
539
|
+
binding: input.binding,
|
|
540
|
+
kind: 'relational',
|
|
541
|
+
ref,
|
|
542
|
+
createdAt: new Date().toISOString(),
|
|
543
|
+
});
|
|
544
|
+
if (stored.ref !== ref) {
|
|
545
|
+
// A concurrent provision won the ledger write. One database is canonical — the
|
|
546
|
+
// ledger's — so drop ours rather than orphan it (best-effort: a leaked delete
|
|
547
|
+
// failure leaves an unreferenced empty database, never a wrong handle).
|
|
548
|
+
await d1.remove(ref).catch(() => undefined);
|
|
549
|
+
return { binding: input.binding, kind: 'relational', ref: stored.ref };
|
|
550
|
+
}
|
|
551
|
+
await this.recordAdmin(actor, 'provisionTenantStore', { tenantId: input.tenantId, vertical: input.vertical }, null, { binding: input.binding, kind: 'relational', ref });
|
|
552
|
+
return { binding: input.binding, kind: 'relational', ref };
|
|
509
553
|
}
|
|
510
554
|
openTenantStore(handle) {
|
|
511
|
-
|
|
555
|
+
// The COORDINATOR-side open: out-of-band SQL over the D1 HTTP API — driving a store's
|
|
556
|
+
// migrations externally, ops reads, tests. The request-time open happens in the
|
|
557
|
+
// vertical's worker instead, against the real `d1` binding the platform attached to
|
|
558
|
+
// the serving script (`env[tenantStoreBindingName(handle.binding, tenantId)]`, wrapped
|
|
559
|
+
// by `d1TenantRelationalStore`) — which is also why `native` is null here: this store
|
|
560
|
+
// has no in-process driver to hand out.
|
|
561
|
+
const d1 = this.requireTenantStores(`openTenantStore(binding=${handle.binding})`);
|
|
562
|
+
return {
|
|
563
|
+
query: async (sql, params = []) => (await d1.query(handle.ref, sql, params)).results,
|
|
564
|
+
exec: async (sql, params = []) => ({
|
|
565
|
+
changes: (await d1.query(handle.ref, sql, params)).changes,
|
|
566
|
+
}),
|
|
567
|
+
native: null,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
/** The injected D1 client, or a loud refusal naming what to configure. */
|
|
571
|
+
requireTenantStores(what) {
|
|
572
|
+
if (!this.tenantStores) {
|
|
573
|
+
throw new Error(`per-tenant stores are not configured on this host (#301): pass ` +
|
|
574
|
+
`CloudflareScopeHostOptions.tenantStores (createD1TenantStores with the platform's ` +
|
|
575
|
+
`Cloudflare credential) — refused ${what}`);
|
|
576
|
+
}
|
|
577
|
+
return this.tenantStores;
|
|
512
578
|
}
|
|
513
579
|
async importScope(actor, input, dump) {
|
|
514
580
|
// Create the destination scope (directory row + DO + lazy migrate); the DO then
|
|
@@ -1498,6 +1564,21 @@ export class CloudflareScopeHost {
|
|
|
1498
1564
|
await this.recordAccess(actor, 'getScopeRecord', { tenantId, scopeId }, null, row ? 1 : 0);
|
|
1499
1565
|
return row ? mapScope(row) : undefined;
|
|
1500
1566
|
},
|
|
1567
|
+
listTenantStores: async (actor, filter) => {
|
|
1568
|
+
const rows = await this.cp.listTenantStores({
|
|
1569
|
+
tenantId: filter?.tenantId,
|
|
1570
|
+
vertical: filter?.vertical,
|
|
1571
|
+
});
|
|
1572
|
+
await this.recordAccess(actor, 'listTenantStores', { tenantId: filter?.tenantId ?? null }, filter ?? null, rows.length);
|
|
1573
|
+
return rows.map((r) => ({
|
|
1574
|
+
tenantId: r.tenant_id,
|
|
1575
|
+
vertical: r.vertical,
|
|
1576
|
+
binding: r.binding,
|
|
1577
|
+
kind: 'relational',
|
|
1578
|
+
ref: r.ref,
|
|
1579
|
+
createdAt: r.created_at,
|
|
1580
|
+
}));
|
|
1581
|
+
},
|
|
1501
1582
|
listScopeTables: async (actor, tenantId, scopeId) => {
|
|
1502
1583
|
// K-3 cross-check on the shared directory BEFORE reaching the scope DO: a pair
|
|
1503
1584
|
// that does not resolve is unreachable, never another tenant's database.
|
|
@@ -1648,6 +1729,18 @@ export class CloudflareScopeHost {
|
|
|
1648
1729
|
await this.recordAccess(actor, 'listIdentityTenants', {}, { provider }, tenants.length);
|
|
1649
1730
|
return tenants;
|
|
1650
1731
|
},
|
|
1732
|
+
listIdentityLinks: async (actor, tenantId) => {
|
|
1733
|
+
const rows = await this.cp.dumpTenantIdentities(tenantId);
|
|
1734
|
+
const links = rows.map((r) => identityLink.parse({
|
|
1735
|
+
provider: r.provider,
|
|
1736
|
+
externalId: r.external_id,
|
|
1737
|
+
principal: r.principal_id,
|
|
1738
|
+
tenantId,
|
|
1739
|
+
scopeId: r.scope_id ?? undefined,
|
|
1740
|
+
}));
|
|
1741
|
+
await this.recordAccess(actor, 'listIdentityLinks', { tenantId }, null, links.length);
|
|
1742
|
+
return links;
|
|
1743
|
+
},
|
|
1651
1744
|
// -- the integrations hub (#101) ---------------------------------------
|
|
1652
1745
|
createConnection: async (actor, raw) => {
|
|
1653
1746
|
const input = createConnectionInput.parse(raw);
|
|
@@ -1755,6 +1848,10 @@ export class CloudflareScopeHost {
|
|
|
1755
1848
|
if (!changed)
|
|
1756
1849
|
return;
|
|
1757
1850
|
await this.recordAdmin(actor, 'linkIdentity', { tenantId: parsed.tenantId, scopeId: parsed.scopeId }, null, { provider: parsed.provider, externalId: parsed.externalId, principal: parsed.principal });
|
|
1851
|
+
// #406: an identity link is a tenant-level write, so it fans out into the tenant's
|
|
1852
|
+
// projected scopes exactly as entitlements do (#304) — a no-op unless scope-local
|
|
1853
|
+
// projection is on; the reconcile sweep repairs any drop.
|
|
1854
|
+
await this.fanOut(parsed.tenantId);
|
|
1758
1855
|
},
|
|
1759
1856
|
unlinkIdentity: async (actor, tenantId, principal) => {
|
|
1760
1857
|
// DELETE by principal (audit is the log), so the caller who removed a member
|
|
@@ -1763,6 +1860,10 @@ export class CloudflareScopeHost {
|
|
|
1763
1860
|
if (!changed)
|
|
1764
1861
|
return;
|
|
1765
1862
|
await this.recordAdmin(actor, 'unlinkIdentity', { tenantId, scopeId: null }, { principal }, null);
|
|
1863
|
+
// The unlink must reach the projected scopes so the severed login stops resolving
|
|
1864
|
+
// there — this is what makes revocation durable at request time (#406); a dropped
|
|
1865
|
+
// fan-out is repaired by the reconcile sweep.
|
|
1866
|
+
await this.fanOut(tenantId);
|
|
1766
1867
|
},
|
|
1767
1868
|
resolveIdentity: async (tenantId, provider, externalId) => {
|
|
1768
1869
|
const row = await this.cp.resolveIdentity(tenantId, provider, externalId);
|
|
@@ -1866,14 +1967,16 @@ export class CloudflareScopeHost {
|
|
|
1866
1967
|
// the coordinator PROJECTS the tenant's current roles + tenant-level tuples into
|
|
1867
1968
|
// its scopes, which then evaluate permissions from their own storage. Cost moves
|
|
1868
1969
|
// from the request hot path (every check) to the admin write path (rare).
|
|
1869
|
-
/** The tenant's current roles + tenant-level tuples + entitlements, in the
|
|
1870
|
-
* stores. Entitlements (#304) ride the same projection so a hosted scope
|
|
1871
|
-
* expiry locally; expiry is applied at READ (in the scope), so the full list
|
|
1970
|
+
/** The tenant's current roles + tenant-level tuples + entitlements + identity links, in the
|
|
1971
|
+
* shape the ScopeDO stores. Entitlements (#304) ride the same projection so a hosted scope
|
|
1972
|
+
* reads plan/quota/expiry locally; expiry is applied at READ (in the scope), so the full list
|
|
1973
|
+
* is carried. Identity links (#406) ride it too, so a scope resolves logins locally. */
|
|
1872
1974
|
async tenantProjection(tenantId) {
|
|
1873
|
-
const [roleRows, tuples, entitlementRows] = await Promise.all([
|
|
1975
|
+
const [roleRows, tuples, entitlementRows, identities] = await Promise.all([
|
|
1874
1976
|
this.cp.listRoles({ tenantId }),
|
|
1875
1977
|
this.cp.dumpTenantTuples(tenantId),
|
|
1876
1978
|
this.cp.listEntitlements(tenantId),
|
|
1979
|
+
this.cp.dumpTenantIdentities(tenantId),
|
|
1877
1980
|
]);
|
|
1878
1981
|
return {
|
|
1879
1982
|
roles: roleRows.map((r) => ({ role_key: r.role_key, permissions: r.permissions, source: r.source })),
|
|
@@ -1884,14 +1987,15 @@ export class CloudflareScopeHost {
|
|
|
1884
1987
|
quota: e.quota,
|
|
1885
1988
|
plan: e.plan,
|
|
1886
1989
|
})),
|
|
1990
|
+
identities,
|
|
1887
1991
|
};
|
|
1888
1992
|
}
|
|
1889
1993
|
/** Project the tenant's current state into ONE scope + flip it to local. */
|
|
1890
1994
|
async projectScope(tenantId, scopeId) {
|
|
1891
1995
|
if (!this.scopeLocalPermissions)
|
|
1892
1996
|
return;
|
|
1893
|
-
const { roles, tuples, entitlements } = await this.tenantProjection(tenantId);
|
|
1894
|
-
await this.scopeStub(scopeId).applyProjection(tenantId, roles, tuples, entitlements);
|
|
1997
|
+
const { roles, tuples, entitlements, identities } = await this.tenantProjection(tenantId);
|
|
1998
|
+
await this.scopeStub(scopeId).applyProjection(tenantId, roles, tuples, entitlements, undefined, identities);
|
|
1895
1999
|
}
|
|
1896
2000
|
/**
|
|
1897
2001
|
* Fan the tenant's current state out into ALL its scopes — called after any
|
|
@@ -1901,9 +2005,9 @@ export class CloudflareScopeHost {
|
|
|
1901
2005
|
async fanOut(tenantId) {
|
|
1902
2006
|
if (!this.scopeLocalPermissions)
|
|
1903
2007
|
return;
|
|
1904
|
-
const { roles, tuples, entitlements } = await this.tenantProjection(tenantId);
|
|
2008
|
+
const { roles, tuples, entitlements, identities } = await this.tenantProjection(tenantId);
|
|
1905
2009
|
const scopes = await this.cp.listScopes({ tenantId });
|
|
1906
|
-
await Promise.all(scopes.map((s) => this.scopeStub(s.scope_id).applyProjection(tenantId, roles, tuples, entitlements)));
|
|
2010
|
+
await Promise.all(scopes.map((s) => this.scopeStub(s.scope_id).applyProjection(tenantId, roles, tuples, entitlements, undefined, identities)));
|
|
1907
2011
|
}
|
|
1908
2012
|
/**
|
|
1909
2013
|
* Re-project a tenant's full state into every one of its scopes — the
|
|
@@ -1953,7 +2057,33 @@ export class CloudflareScopeHost {
|
|
|
1953
2057
|
object: `scope:${input.scopeId}`,
|
|
1954
2058
|
expires_at: null,
|
|
1955
2059
|
},
|
|
1956
|
-
]
|
|
2060
|
+
],
|
|
2061
|
+
// #406: identity links, delivered by the platform with provisioning exactly as
|
|
2062
|
+
// entitlements are (#310). Preserve-on-undefined, so an absent field never wipes
|
|
2063
|
+
// links a prior fan-out or reconcile projected.
|
|
2064
|
+
input.identityLinks
|
|
2065
|
+
? input.identityLinks.map((l) => ({
|
|
2066
|
+
provider: l.provider,
|
|
2067
|
+
external_id: l.externalId,
|
|
2068
|
+
principal_id: l.principal,
|
|
2069
|
+
scope_id: l.scopeId ?? null,
|
|
2070
|
+
}))
|
|
2071
|
+
: undefined);
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2074
|
+
* Resolve an external identity from a scope's PROJECTED links (#406) — the CP-less
|
|
2075
|
+
* auth adapter's read path, the local equivalent of `HostAdmin.resolveIdentity` (same
|
|
2076
|
+
* exemption: a machine read on the request path, not audited). Reads only what the
|
|
2077
|
+
* platform projected into the scope (provision / reconcile / fan-out), so a miss means
|
|
2078
|
+
* "unknown login — deny": an un-projected scope can only refuse a legitimate login,
|
|
2079
|
+
* never admit a revoked one. A CP-full deployment keeps using `admin.resolveIdentity`;
|
|
2080
|
+
* this exists precisely for deployments where that surface is unavailable.
|
|
2081
|
+
*/
|
|
2082
|
+
async resolveIdentityLocal(tenantId, scopeId, provider, externalId) {
|
|
2083
|
+
const row = await this.scopeStub(scopeId).resolveProjectedIdentity(tenantId, provider, externalId);
|
|
2084
|
+
if (!row)
|
|
2085
|
+
return undefined;
|
|
2086
|
+
return resolvedIdentity.parse({ principal: row.principal, scopeId: row.scopeId });
|
|
1957
2087
|
}
|
|
1958
2088
|
/**
|
|
1959
2089
|
* Grant a principal a role at SCOPE level in a CP-less vertical — the MEMBER half of
|