@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 +1 -1
- package/src/build_package.ts +8 -0
- package/src/engine_entry.ts +13 -0
- package/src/framework/auth_provider_law.ts +121 -0
- package/src/framework/home.ts +51 -1
- package/src/framework/workspaces.ts +5 -102
- package/src/read.ts +24 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
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",
|
package/src/build_package.ts
CHANGED
|
@@ -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
|
package/src/engine_entry.ts
CHANGED
|
@@ -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);
|
package/src/framework/home.ts
CHANGED
|
@@ -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
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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
|