@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/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(_actor: PlatformActorId, input: TenantStoreProvisionInput): Promise<TenantStoreHandle>;
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 shape the ScopeDO
270
- * stores. Entitlements (#304) ride the same projection so a hosted scope reads plan/quota/
271
- * expiry locally; expiry is applied at READ (in the scope), so the full list is carried. */
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
@@ -1 +1 @@
1
- {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"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"}
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 vocabulary, directory ledger and pure-adapter
502
- // implementation land first; the LIVE Cloudflare D1 create/bind/HTTP-query is the second
503
- // step (it needs real CF API calls that cannot be exercised on the local/SQLite path).
504
- // Until then these fail loudly rather than silently no-op — a hosted vertical that declares
505
- // a `tenantStoreNeed` must not appear provisioned while its store does not exist.
506
- async provisionTenantStore(_actor, input) {
507
- throw new Error(`provisionTenantStore is not yet wired on Cloudflare (#301, PR-2: live D1 minting) — ` +
508
- `tenant=${input.tenantId} vertical=${input.vertical} binding=${input.binding}`);
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
- throw new Error(`openTenantStore is not yet wired on Cloudflare (#301, PR-2: live D1 binding) — binding=${handle.binding}`);
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 shape the ScopeDO
1870
- * stores. Entitlements (#304) ride the same projection so a hosted scope reads plan/quota/
1871
- * expiry locally; expiry is applied at READ (in the scope), so the full list is carried. */
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