@githolon/dsl 0.97.5 → 0.98.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.97.5",
3
+ "version": "0.98.1",
4
4
  "type": "module",
5
5
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -245,6 +245,12 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
245
245
  for (const a of BIRTH_METER_AGGREGATES) {
246
246
  if (!byId.has(a.id)) byId.set(a.id, a);
247
247
  }
248
+ // AUTHPROVIDER INHERITANCE (birthHome's default-on parent read — the same courtesy): a birth-capable
249
+ // law serves the `authProvider` captured read (parent side) AND can fold the inherited
250
+ // `setAuthProvider` genesis step (child side; the one-package fixture shape is both at once). Without
251
+ // this carry, default-on inheritance would refuse on every law that never hand-exported the trio.
252
+ if (!byId.has(AuthProvider.id)) byId.set(AuthProvider.id, AuthProvider);
253
+ if (!directives.some((d) => d.id === setAuthProvider.id)) directives.push(setAuthProvider);
248
254
  }
249
255
  // SECONDARY-CREATE AUTO-DECLARE (secondary_ensure.ts — the id-mint-gate DX): a plan may `ensure(B, "det:id")`
250
256
  // a SECOND aggregate at a deterministic id of a type its directive does NOT declare `.ensures()` for. The
@@ -316,6 +322,8 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
316
322
  for (const q of BIRTH_METER_QUERIES) {
317
323
  if (!haveQuery.has(q.id)) queries.push(q);
318
324
  }
325
+ // The inheritance read (`.reads("authProvider")` on birthHome) routes like any declared query.
326
+ if (!haveQuery.has(authProvider.id)) queries.push(authProvider);
319
327
  }
320
328
  const relationTypes = [...(spec.relationTypes ?? [])];
321
329
  // RELATION-USING LAW AUTO-COMPOSES ITS AUDIT READ (co2 recovery DX — same idiom as the meter
@@ -55,6 +55,7 @@ import { executeDirectiveToIntent } from "./wire_encode.js";
55
55
  import { expandCapabilityExports } from "./capability_exports.js";
56
56
  import { RelationTuple, __setDispatchContext } from "./framework/relations.js";
57
57
  import { BIRTH_METER_AGGREGATES } from "./birth_meter.js";
58
+ import { AuthProvider as FrameworkAuthProvider, setAuthProvider as frameworkSetAuthProvider } from "./framework/auth_provider_law.js";
58
59
  import { namespacedAggregateIds, qualifyReadProjectionType, READ_PROJECTION_DELIMITER } from "./read_projection_namespace.js";
59
60
  import { isRelationTypeDecl } from "./relation_schema.js";
60
61
  import { synthesizePreconditionInvariants } from "./precondition.js";
@@ -313,6 +314,18 @@ function buildDomainSlice(mod: DomainModuleExports): DomainSlice {
313
314
  aggs.set(fw.id, fw);
314
315
  }
315
316
  }
317
+ // AUTHPROVIDER AUTO-ENSURE (the compile-side declaresBirth/declaresPrivateRead carry, MIRRORED — the
318
+ // P2-001 rule: every compile-side injection needs its engine twin or the manifest and the dispatch
319
+ // registry skew into "no directive registered"). A births-declaring or private module can always fold
320
+ // `setAuthProvider` (the inherited genesis step / the verifier author) + its AuthProvider aggregate.
321
+ {
322
+ const declaresBirthHere = [...dirs.values()].some((d) => (d as { birthsChild?: boolean }).birthsChild === true);
323
+ const declaresPrivateHere = [...aggs.values()].some((a) => (a as { visibility?: unknown }).visibility !== undefined);
324
+ if (declaresBirthHere || declaresPrivateHere) {
325
+ if (!aggs.has(FrameworkAuthProvider.id)) aggs.set(FrameworkAuthProvider.id, FrameworkAuthProvider);
326
+ if (!dirs.has(frameworkSetAuthProvider.id)) dirs.set(frameworkSetAuthProvider.id, frameworkSetAuthProvider as unknown as Directive<unknown>);
327
+ }
328
+ }
316
329
  for (const [dirId, directive] of dirs) {
317
330
  const agg = aggs.get(directive.aggregateId);
318
331
  if (agg === undefined) {
@@ -0,0 +1,121 @@
1
+ // NOMOS — framework/auth_provider_law.ts — THE AUTHPROVIDER LAW SURFACE, as a LEAF module.
2
+ //
3
+ // Extracted from workspaces.ts so the ENGINE ENTRY (and birthHome's inheritance) can import the
4
+ // aggregate + directive WITHOUT dragging the whole workspaces/home graph into every sealed lump
5
+ // (workspaces re-exports from home.js, whose birthHome plan calls read() — bundling that into a
6
+ // read-free law trips the captured-read compile gate). workspaces.ts re-exports everything here,
7
+ // so the public surface (`@githolon/dsl` names, ids, law bytes) is unchanged.
8
+ import { z } from "zod";
9
+ import { aggregate, instance } from "../aggregate.js";
10
+ import { directive } from "../directive.js";
11
+ import { t } from "../fields.js";
12
+ import { Lww } from "../drivers.js";
13
+ import { set, withMarker } from "../ops.js";
14
+ import { query } from "../query.js";
15
+
16
+ const epochMs = z.number().int().nonnegative();
17
+
18
+ /** THE AUTHPROVIDER SINGLETON INSTANCE ID — the ONE place this string lives DSL-side (the kernel owns the
19
+ * matching `nomos_identity::auth_provider::AUTH_PROVIDER_ID`; a drift-guard test keeps them identical). The
20
+ * kernel verifies presented tokens against the AuthProvider at this id, so its SHAPE is kernel-owned
21
+ * first-class machinery; `setAuthProvider`/`rotateKeys` compile to it. */
22
+ export const AUTH_PROVIDER_ID = "auth-provider:platform";
23
+
24
+ export const AuthProvider = aggregate("AuthProvider", {
25
+ /** The singleton key — always `"platform"`. */
26
+ scope: t.string().merge(Lww),
27
+ /** The token issuer (the `iss` claim tokens must carry). */
28
+ issuer: t.string().merge(Lww),
29
+ /** The provider's public JWKS endpoint (asymmetric verification keys). The KERNEL never fetches — it's a
30
+ * reference for the rotation capability that re-fetches + re-sets {@link keys}. */
31
+ jwksUrl: t.string().merge(Lww),
32
+ /** THE VERIFYING KEYS — the JWKS JSON (`{keys:[{kid,kty,alg,crv,x,y,n,e,...}]}`) as an OPAQUE STRING the
33
+ * KERNEL parses + verifies a presented token against (sovereign-auth arc — see
34
+ * architecture/sovereign_adapter.md). `t.string` (NOT `t.json`): the kernel `Value` has no JSON variant,
35
+ * so the raw JWKS text must ride verbatim as a scalar — the kernel does `serde_json` on it, no projection
36
+ * parse, no double-encoding. LAW-STATE: set by {@link setAuthProvider} (gated), refreshed by the
37
+ * JWKS-fetch rotation capability (the host FETCHES — impure — but the law OWNS the keys + the kernel does
38
+ * the verification). Absent ⇒ no in-kernel verify for this workspace (shell-issuer fallback, transitional). */
39
+ keys: t.string().merge(Lww).optional(),
40
+ /** OPTIONAL expected `aud` claim. When the platform sets it, a token's `aud` MUST carry this value
41
+ * (defense-in-depth beyond the issuer match — e.g. a Firebase project id, an Auth0 API audience).
42
+ * Absent ⇒ issuer match alone. This is LAW-STATE the PLATFORM owns; the host merely enforces it as a
43
+ * capability (it never chooses the audience). */
44
+ audience: t.string().merge(Lww),
45
+ /** OPTIONAL — the JWT claim that carries the PRINCIPAL (default `"sub"` when absent). Azure AD's stable
46
+ * user id is `oid` (its `sub` is pairwise-per-app, useless as identity); some setups key on a custom
47
+ * claim. The KERNEL extracts THIS claim as the principal — never hardcoded to `sub`. */
48
+ principalClaim: t.string().merge(Lww).optional(),
49
+ /** OPTIONAL — the PINNED CA ROOT (base64 DER) for TRUSTLESS key rotation. When set, {@link rotateKeys}
50
+ * accepts a new JWKS IFF each key's `x5c` chain validates to THIS root IN THE KERNEL (RFC 5280, the IdP's
51
+ * CA vouches) and each key matches its leaf cert — no host trust, no admin. Pinned once via the
52
+ * admin-gated {@link setAuthProvider}. Absent ⇒ no trustless rotation (rotate via setAuthProvider). */
53
+ rootCert: t.string().merge(Lww).optional(),
54
+ /** OPTIONAL — the JWT claim carrying the DEVICE BINDING for the keyless first-device enroll
55
+ * (the `enrollFirstDevice` lane, keyless_verified_onboard.md): the kernel asserts
56
+ * `token[deviceBindingClaim] == sha256(the signing device pubkey)`, so an on-chain token is useless to a
57
+ * thief (no device secret). Standard values: `"nonce"` (OIDC) or `"cnf"`. Absent ⇒ none configured. */
58
+ deviceBindingClaim: t.string().merge(Lww).optional(),
59
+ /** OPTIONAL — `"false"` to OPT OUT of the device-binding requirement (accept the bounded ~token-lifetime
60
+ * replay window — for IdPs that can't carry a nonce, eyes-open). The kernel applies STRICT (require the
61
+ * binding) when this is absent or `"true"` — strict-by-default, so a naked token never lands unless the
62
+ * tenant explicitly relaxes it. */
63
+ requireDeviceBinding: t.string().merge(Lww).optional(),
64
+ /** Human label for audit ("acme supabase", ...). */
65
+ label: t.string().merge(Lww),
66
+ /** Who set it (attribution). */
67
+ setBy: t.string().merge(Lww),
68
+ /** Epoch-ms it was set (caller-stamped). */
69
+ setAt: t.int().merge(Lww),
70
+ });
71
+
72
+ /**
73
+ * setAuthProvider — declare (or amend) the platform's identity provider. An
74
+ * Ensure on the singleton; every change is an attributed, replayable intent.
75
+ */
76
+ export const setAuthProvider = directive("setAuthProvider")
77
+ .ensures(AuthProvider)
78
+ .payload(
79
+ z.object({
80
+ // NOT z.url(): zod's url() needs the URL global, which the sealed
81
+ // deterministic engine does not provide — a regex keeps the plan pure.
82
+ issuer: z.string().regex(/^https?:\/\/\S+$/, "expected an http(s) URL"),
83
+ jwksUrl: z.string().regex(/^https?:\/\/\S+$/, "expected an http(s) URL"),
84
+ audience: z.string().min(1).optional(),
85
+ /** The JWKS JSON (the public verifying keys) the KERNEL checks tokens against. Optional so the
86
+ * rotation capability can re-set just the keys; absent keeps the prior keys (Lww). */
87
+ keys: z.string().min(1).optional(),
88
+ /** OPTIONAL — the claim carrying the principal (default "sub"; e.g. "oid" for Azure AD). */
89
+ principalClaim: z.string().min(1).optional(),
90
+ /** OPTIONAL — the pinned CA root (base64 DER) enabling trustless {@link rotateKeys}. */
91
+ rootCert: z.string().min(1).optional(),
92
+ /** OPTIONAL — the device-binding claim for keyless first-device enroll ("nonce" | "cnf"). */
93
+ deviceBindingClaim: z.string().min(1).optional(),
94
+ /** OPTIONAL — "false" to accept the bounded replay window (strict device-binding is the default). */
95
+ requireDeviceBinding: z.enum(["true", "false"]).optional(),
96
+ label: z.string().min(1),
97
+ setBy: z.string().min(1),
98
+ setAt: epochMs,
99
+ }),
100
+ )
101
+ .plan((p) => {
102
+ const ap = instance(AuthProvider, AUTH_PROVIDER_ID);
103
+ return [
104
+ withMarker(set(ap, "scope", "platform"), "ensures"),
105
+ set(ap, "issuer", p.issuer),
106
+ set(ap, "jwksUrl", p.jwksUrl),
107
+ ...(p.audience ? [set(ap, "audience", p.audience)] : []),
108
+ ...(p.keys ? [set(ap, "keys", p.keys)] : []),
109
+ ...(p.principalClaim ? [set(ap, "principalClaim", p.principalClaim)] : []),
110
+ ...(p.rootCert ? [set(ap, "rootCert", p.rootCert)] : []),
111
+ ...(p.deviceBindingClaim ? [set(ap, "deviceBindingClaim", p.deviceBindingClaim)] : []),
112
+ ...(p.requireDeviceBinding ? [set(ap, "requireDeviceBinding", p.requireDeviceBinding)] : []),
113
+ set(ap, "label", p.label),
114
+ set(ap, "setBy", p.setBy),
115
+ set(ap, "setAt", p.setAt),
116
+ ];
117
+ })
118
+ .requires("admin");
119
+
120
+ /** The declared read serving a workspace's OWN AuthProvider row (and birthHome's inheritance capture). */
121
+ export const authProvider = query("authProvider").key("scope").returns(AuthProvider);
@@ -35,6 +35,8 @@ import { create } from "../authoring.js";
35
35
  import { set, type PlannedOp } from "../ops.js";
36
36
  import { query } from "../query.js";
37
37
  import { birth } from "../births.js";
38
+ import { readFrameworkDeclared } from "../read.js";
39
+ import { authProvider as authProviderQuery } from "./auth_provider_law.js";
38
40
  import { workspaceInvariant, refAs } from "./workspace_invariant.js";
39
41
  import { identityRelations } from "./signing_identity.js";
40
42
  import { writeTuple } from "./relations.js";
@@ -213,12 +215,23 @@ export const birthHome = directive("birthHome")
213
215
  * `links[0]` of the travelling chain — the kernel signs nothing (it holds no secret; it only verifies).
214
216
  * Optional: absent ⇒ the era stays inert for this birth (pre-feature lane). */
215
217
  homeCert: z.string().optional(),
218
+ /** AUTHPROVIDER INHERITANCE — DEFAULT ON, opt out with `false`. Verification is PER-WORKSPACE (the
219
+ * kernel never walks up to the parent), so a PRIVATE child born without its own AuthProvider cannot
220
+ * verify any token — its own owner is locked out of verified reads (the co2 phase-6 wall). By
221
+ * default the birth READS the parent's `authProvider` row (the #58 captured-read lane — live at
222
+ * author, CAS-re-derived at the gate) and, when one is declared, COPIES it AS DATA into the child's
223
+ * genesis (a `setAuthProvider` step) — the child still OWNS its verifier as its own law-state; the
224
+ * parent's later rotations do NOT propagate (re-set on the child to rotate). Requirements the
225
+ * refusals name: the parent law must compose the `authProvider` query and the child law the
226
+ * `setAuthProvider` directive (both ride the framework export set). Pass `false` for a child that
227
+ * must NOT trust the parent's IdP (it then seeds its own or stays bare-principal). */
228
+ inheritAuthProvider: z.boolean().optional(),
216
229
  }),
217
230
  )
218
231
  .plan((p) => {
219
232
  const founder = p.owner; // the actor for the child's own genesis steps (its self-bound owner subject)
220
233
  const homeName = homeNameOf(p.owner); // workspace name == seeded homeName — derived from the SAME owner
221
- const genesisChain = [
234
+ const genesisChain: Array<{ domain: string; directiveId: string; actor: string; payload: Record<string, unknown>; domainHash?: string; domainPackageB64?: string }> = [
222
235
  // THE ORDER-DECLARED LAW INSTALL: two layers, bytes-by-hash (empty package — the kernel resolves them
223
236
  // from the parent's custody when it folds the birth; never the host). Matches buildGenesisChain's shape.
224
237
  { domain: "bootstrap", directiveId: "installDomain", actor: founder, payload: { domainHash: p.frameworkHash }, domainHash: p.frameworkHash, domainPackageB64: "" },
@@ -237,12 +250,49 @@ export const birthHome = directive("birthHome")
237
250
  // rides the LAW hash, not this branch.
238
251
  { domain: HOME_LAW_DOMAIN, directiveId: "enrollSigner", actor: founder, payload: { keyHash: p.ownerKeyHash, grantedBy: p.owner, grantedAt: 1 } },
239
252
  ];
253
+ // AUTHPROVIDER INHERITANCE (default ON — see the payload doc): read the PARENT's declared provider
254
+ // (captured read, re-derived at the gate) and copy it AS DATA into the child genesis. The child owns
255
+ // the copy as its own law-state; nothing is inherited-by-reference. Opt out: `inheritAuthProvider: false`.
256
+ if (p.inheritAuthProvider !== false) {
257
+ const rows = readFrameworkDeclared<Array<Record<string, unknown>>>("authProvider", { scope: "platform" });
258
+ const raw = Array.isArray(rows) && rows.length > 0 ? rows[0] : undefined;
259
+ // Projected rows are `{type,id,data:{…fields}}`; folded shapes may be flat — accept both.
260
+ const ap = (raw && typeof raw === "object" && typeof (raw as { data?: unknown }).data === "object"
261
+ ? (raw as { data: Record<string, unknown> }).data
262
+ : raw) as Record<string, unknown> | undefined;
263
+ const str = (k: string): string | undefined => {
264
+ const v = ap?.[k];
265
+ return typeof v === "string" && v.length > 0 ? v : undefined;
266
+ };
267
+ const issuer = str("issuer");
268
+ if (issuer !== undefined) {
269
+ genesisChain.push({
270
+ domain: HOME_LAW_DOMAIN,
271
+ directiveId: "setAuthProvider",
272
+ actor: founder,
273
+ payload: {
274
+ issuer,
275
+ jwksUrl: str("jwksUrl") ?? issuer, // required by the framework schema; a keys-only provider mirrors its issuer
276
+ ...(str("keys") !== undefined ? { keys: str("keys") } : {}),
277
+ ...(str("audience") !== undefined ? { audience: str("audience") } : {}),
278
+ ...(str("principalClaim") !== undefined ? { principalClaim: str("principalClaim") } : {}),
279
+ ...(str("rootCert") !== undefined ? { rootCert: str("rootCert") } : {}),
280
+ ...(str("deviceBindingClaim") !== undefined ? { deviceBindingClaim: str("deviceBindingClaim") } : {}),
281
+ ...(str("requireDeviceBinding") !== undefined ? { requireDeviceBinding: str("requireDeviceBinding") } : {}),
282
+ label: str("label") ?? "inherited-from-parent",
283
+ setBy: p.owner,
284
+ setAt: 1, // genesis position — deterministic, like enrollSigner's grantedAt
285
+ },
286
+ });
287
+ }
288
+ }
240
289
  // THE DECLARED BIRTH EFFECT (the unix model): the recipe rides the committed intent as a declared output
241
290
  // (captured_ports.births). The kernel — reading the `.births()` declaration off the law manifest — takes
242
291
  // the recipe and spawns + self-validates the child through the child's OWN gate (each step self-verifies).
243
292
  birth({ workspace: homeName, genesisChain });
244
293
  return [];
245
294
  })
295
+ .reads(authProviderQuery) // the parent-side AuthProvider capture the inheritance copies (the #58 lane)
246
296
  .births(); // BIRTH IS A DECLARED KERNEL PRIMITIVE — emits the `nomosBirthGate` era key; the kernel folds the recipe
247
297
 
248
298
  /**
@@ -1351,107 +1351,11 @@ export const setPlatformPool = directive("setPlatformPool")
1351
1351
  * principals only WITHIN that platform's own tree. The cloud's own provider
1352
1352
  * (captain app) is the depth-0 case, configured by the host.
1353
1353
  */
1354
- /** THE AUTHPROVIDER SINGLETON INSTANCE ID — the ONE place this string lives DSL-side (the kernel owns the
1355
- * matching `nomos_identity::auth_provider::AUTH_PROVIDER_ID`; a drift-guard test keeps them identical). The
1356
- * kernel verifies presented tokens against the AuthProvider at this id, so its SHAPE is kernel-owned
1357
- * first-class machinery; `setAuthProvider`/`rotateKeys` compile to it. */
1358
- export const AUTH_PROVIDER_ID = "auth-provider:platform";
1359
-
1360
- export const AuthProvider = aggregate("AuthProvider", {
1361
- /** The singleton key — always `"platform"`. */
1362
- scope: t.string().merge(Lww),
1363
- /** The token issuer (the `iss` claim tokens must carry). */
1364
- issuer: t.string().merge(Lww),
1365
- /** The provider's public JWKS endpoint (asymmetric verification keys). The KERNEL never fetches — it's a
1366
- * reference for the rotation capability that re-fetches + re-sets {@link keys}. */
1367
- jwksUrl: t.string().merge(Lww),
1368
- /** THE VERIFYING KEYS — the JWKS JSON (`{keys:[{kid,kty,alg,crv,x,y,n,e,...}]}`) as an OPAQUE STRING the
1369
- * KERNEL parses + verifies a presented token against (sovereign-auth arc — see
1370
- * architecture/sovereign_adapter.md). `t.string` (NOT `t.json`): the kernel `Value` has no JSON variant,
1371
- * so the raw JWKS text must ride verbatim as a scalar — the kernel does `serde_json` on it, no projection
1372
- * parse, no double-encoding. LAW-STATE: set by {@link setAuthProvider} (gated), refreshed by the
1373
- * JWKS-fetch rotation capability (the host FETCHES — impure — but the law OWNS the keys + the kernel does
1374
- * the verification). Absent ⇒ no in-kernel verify for this workspace (shell-issuer fallback, transitional). */
1375
- keys: t.string().merge(Lww).optional(),
1376
- /** OPTIONAL expected `aud` claim. When the platform sets it, a token's `aud` MUST carry this value
1377
- * (defense-in-depth beyond the issuer match — e.g. a Firebase project id, an Auth0 API audience).
1378
- * Absent ⇒ issuer match alone. This is LAW-STATE the PLATFORM owns; the host merely enforces it as a
1379
- * capability (it never chooses the audience). */
1380
- audience: t.string().merge(Lww),
1381
- /** OPTIONAL — the JWT claim that carries the PRINCIPAL (default `"sub"` when absent). Azure AD's stable
1382
- * user id is `oid` (its `sub` is pairwise-per-app, useless as identity); some setups key on a custom
1383
- * claim. The KERNEL extracts THIS claim as the principal — never hardcoded to `sub`. */
1384
- principalClaim: t.string().merge(Lww).optional(),
1385
- /** OPTIONAL — the PINNED CA ROOT (base64 DER) for TRUSTLESS key rotation. When set, {@link rotateKeys}
1386
- * accepts a new JWKS IFF each key's `x5c` chain validates to THIS root IN THE KERNEL (RFC 5280, the IdP's
1387
- * CA vouches) and each key matches its leaf cert — no host trust, no admin. Pinned once via the
1388
- * admin-gated {@link setAuthProvider}. Absent ⇒ no trustless rotation (rotate via setAuthProvider). */
1389
- rootCert: t.string().merge(Lww).optional(),
1390
- /** OPTIONAL — the JWT claim carrying the DEVICE BINDING for the keyless first-device enroll
1391
- * (the `enrollFirstDevice` lane, keyless_verified_onboard.md): the kernel asserts
1392
- * `token[deviceBindingClaim] == sha256(the signing device pubkey)`, so an on-chain token is useless to a
1393
- * thief (no device secret). Standard values: `"nonce"` (OIDC) or `"cnf"`. Absent ⇒ none configured. */
1394
- deviceBindingClaim: t.string().merge(Lww).optional(),
1395
- /** OPTIONAL — `"false"` to OPT OUT of the device-binding requirement (accept the bounded ~token-lifetime
1396
- * replay window — for IdPs that can't carry a nonce, eyes-open). The kernel applies STRICT (require the
1397
- * binding) when this is absent or `"true"` — strict-by-default, so a naked token never lands unless the
1398
- * tenant explicitly relaxes it. */
1399
- requireDeviceBinding: t.string().merge(Lww).optional(),
1400
- /** Human label for audit ("acme supabase", ...). */
1401
- label: t.string().merge(Lww),
1402
- /** Who set it (attribution). */
1403
- setBy: t.string().merge(Lww),
1404
- /** Epoch-ms it was set (caller-stamped). */
1405
- setAt: t.int().merge(Lww),
1406
- });
1407
-
1408
- /**
1409
- * setAuthProvider — declare (or amend) the platform's identity provider. An
1410
- * Ensure on the singleton; every change is an attributed, replayable intent.
1411
- */
1412
- export const setAuthProvider = directive("setAuthProvider")
1413
- .ensures(AuthProvider)
1414
- .payload(
1415
- z.object({
1416
- // NOT z.url(): zod's url() needs the URL global, which the sealed
1417
- // deterministic engine does not provide — a regex keeps the plan pure.
1418
- issuer: z.string().regex(/^https?:\/\/\S+$/, "expected an http(s) URL"),
1419
- jwksUrl: z.string().regex(/^https?:\/\/\S+$/, "expected an http(s) URL"),
1420
- audience: z.string().min(1).optional(),
1421
- /** The JWKS JSON (the public verifying keys) the KERNEL checks tokens against. Optional so the
1422
- * rotation capability can re-set just the keys; absent keeps the prior keys (Lww). */
1423
- keys: z.string().min(1).optional(),
1424
- /** OPTIONAL — the claim carrying the principal (default "sub"; e.g. "oid" for Azure AD). */
1425
- principalClaim: z.string().min(1).optional(),
1426
- /** OPTIONAL — the pinned CA root (base64 DER) enabling trustless {@link rotateKeys}. */
1427
- rootCert: z.string().min(1).optional(),
1428
- /** OPTIONAL — the device-binding claim for keyless first-device enroll ("nonce" | "cnf"). */
1429
- deviceBindingClaim: z.string().min(1).optional(),
1430
- /** OPTIONAL — "false" to accept the bounded replay window (strict device-binding is the default). */
1431
- requireDeviceBinding: z.enum(["true", "false"]).optional(),
1432
- label: z.string().min(1),
1433
- setBy: z.string().min(1),
1434
- setAt: epochMs,
1435
- }),
1436
- )
1437
- .plan((p) => {
1438
- const ap = instance(AuthProvider, AUTH_PROVIDER_ID);
1439
- return [
1440
- withMarker(set(ap, "scope", "platform"), "ensures"),
1441
- set(ap, "issuer", p.issuer),
1442
- set(ap, "jwksUrl", p.jwksUrl),
1443
- ...(p.audience ? [set(ap, "audience", p.audience)] : []),
1444
- ...(p.keys ? [set(ap, "keys", p.keys)] : []),
1445
- ...(p.principalClaim ? [set(ap, "principalClaim", p.principalClaim)] : []),
1446
- ...(p.rootCert ? [set(ap, "rootCert", p.rootCert)] : []),
1447
- ...(p.deviceBindingClaim ? [set(ap, "deviceBindingClaim", p.deviceBindingClaim)] : []),
1448
- ...(p.requireDeviceBinding ? [set(ap, "requireDeviceBinding", p.requireDeviceBinding)] : []),
1449
- set(ap, "label", p.label),
1450
- set(ap, "setBy", p.setBy),
1451
- set(ap, "setAt", p.setAt),
1452
- ];
1453
- })
1454
- .requires("admin");
1354
+ // THE AUTHPROVIDER LAW SURFACE moved to ./auth_provider_law.js (a LEAF module the engine entry +
1355
+ // birthHome inheritance import without dragging this whole graph into every lump). Re-exported
1356
+ // here verbatim — the public surface and the law bytes are unchanged.
1357
+ export { AUTH_PROVIDER_ID, AuthProvider, setAuthProvider, authProvider } from "./auth_provider_law.js";
1358
+ import { AUTH_PROVIDER_ID, AuthProvider, setAuthProvider, authProvider } from "./auth_provider_law.js";
1455
1359
 
1456
1360
  /**
1457
1361
  * rotateKeys — TRUSTLESS key rotation. Replace {@link AuthProvider.keys} with a new JWKS whose every key
@@ -1805,7 +1709,6 @@ export const platformGrantByPrincipal = query("platformGrantByPrincipal")
1805
1709
  .returns(PlatformGrant);
1806
1710
 
1807
1711
  /** The platform's identity provider (BYOAuth) — `scope: "platform"` singleton. */
1808
- export const authProvider = query("authProvider").key("scope").returns(AuthProvider);
1809
1712
 
1810
1713
  /** Host → platform: the router's resolver (active claim = the hostname serves). */
1811
1714
  export const hostnameClaimByHostname = query("hostnameClaimByHostname")
package/src/read.ts CHANGED
@@ -80,6 +80,30 @@ export function read<T = unknown>(
80
80
  footprint.push({ queryId: query.id, args, result });
81
81
  return result;
82
82
  }
83
+ return readForeign(query, args, from);
84
+ }
85
+
86
+ /**
87
+ * FRAMEWORK-INTERNAL local read for plans whose directive ALWAYS declares the query (birthHome's
88
+ * authProvider inheritance). Same serve + same footprint capture as {@link read}'s local lane — but a
89
+ * DIFFERENT missing-host error: the compile gate's lump marker lives ONLY in `read()`'s body so it
90
+ * fingers UNDECLARED tenant usage; a framework plan bundled via the `@githolon/dsl` barrel must not pin
91
+ * that marker into read-free lumps (the framework caller is declared by construction).
92
+ */
93
+ export function readFrameworkDeclared<T = unknown>(queryId: string, args: Record<string, string>): T {
94
+ const h = host() as { read?: unknown } | undefined;
95
+ if (typeof h?.read !== "function") {
96
+ throw new Error(
97
+ `framework read('${queryId}'): this engine serves no captured reads — the composing law must ` +
98
+ `carry the directive's declared read (recompile the law under this DSL; the declaration rides the framework directive).`,
99
+ );
100
+ }
101
+ const result = host().read(queryId, args) as T;
102
+ footprint.push({ queryId, args, result });
103
+ return result;
104
+ }
105
+
106
+ function readForeign<T = unknown>(query: QueryDecl | { id: string }, args: Record<string, string>, from: string): T {
83
107
  // ── THE FOREIGN (attested cross-workspace) LANE (slice B — attested_reads_design.md §4.1).
84
108
  // The port answers the `{rows, attestation}` wrapper: at AUTHOR the kernel verified the
85
109
  // injected source envelope before serving it; at VERIFY the recorded capture replays