@githolon/dsl 0.99.0 → 0.100.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/births.ts +10 -51
- package/src/business_birth.ts +1 -1
- package/src/codegen_dart_client.ts +2 -2
- package/src/codegen_proof.ts +65 -59
- package/src/codegen_ts.ts +4 -0
- package/src/compile_package_main.ts +5 -8
- package/src/framework/workspaces.ts +97 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.100.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/births.ts
CHANGED
|
@@ -56,29 +56,8 @@ export function __drainBirths(): BirthRecipe[] {
|
|
|
56
56
|
* chains carry no key, so they replay byte-green and the kernel does nothing). A plan may birth at most one
|
|
57
57
|
* child per intent (one offer, one defined consequence — the unix model).
|
|
58
58
|
*
|
|
59
|
-
*
|
|
60
|
-
* `
|
|
61
|
-
* fabricate it and you do NOT need a new primitive (recursion is the law — a home births children exactly
|
|
62
|
-
* as root births platforms). `buildGenesisChain` is the ONE builder for the PLATFORM shape (it adds the
|
|
63
|
-
* `seedGenesis → grantAdmin → birthCloud` tail); a leaf just omits that tail:
|
|
64
|
-
*
|
|
65
|
-
* ```ts
|
|
66
|
-
* export const birthEstateChild = directive("birthEstateChild")
|
|
67
|
-
* .creates(EstateChild)
|
|
68
|
-
* .payload(z.object({ child: z.string(), owner: z.string(),
|
|
69
|
-
* frameworkHash: z.string(), lawHash: z.string(), bornAt: z.string() }))
|
|
70
|
-
* .plan((p) => {
|
|
71
|
-
* const actor = "user:" + p.owner;
|
|
72
|
-
* birth({ workspace: p.child, genesisChain: [
|
|
73
|
-
* { domain: "bootstrap", directiveId: "installDomain", actor, payload: { domainHash: p.frameworkHash }, domainHash: p.frameworkHash, domainPackageB64: "" },
|
|
74
|
-
* { domain: "nomos", directiveId: "installDomain", actor, payload: { domainHash: p.lawHash }, domainHash: p.lawHash, domainPackageB64: "" },
|
|
75
|
-
* { domain: "estate", directiveId: "seedEstateChild", actor, payload: { owner: p.owner, bornAt: p.bornAt } },
|
|
76
|
-
* ] });
|
|
77
|
-
* return [];
|
|
78
|
-
* })
|
|
79
|
-
* .births() // the kernel's run_birth_effect folds the recipe through the CHILD's own gate
|
|
80
|
-
* .requires("creator"); // grant the parent's owner #creator (self-bound) — the birth authority
|
|
81
|
-
* ```
|
|
59
|
+
* This is framework implementation plumbing. Application packages declare
|
|
60
|
+
* `birth(Aggregate, spec)` and never construct a recipe.
|
|
82
61
|
*/
|
|
83
62
|
/**
|
|
84
63
|
* A business thing that is important enough to have its own durable, sovereign life.
|
|
@@ -112,10 +91,10 @@ export interface BusinessBirthSpec<
|
|
|
112
91
|
readonly namespace?: string;
|
|
113
92
|
}
|
|
114
93
|
|
|
115
|
-
/** @internal
|
|
116
|
-
*
|
|
94
|
+
/** @internal Record an already-lowered child recipe during a framework directive plan. Application law
|
|
95
|
+
* declares `birth(Aggregate, spec)` and leaves the recipe, identity, and recovery machinery to Nomos. */
|
|
117
96
|
export function birth(recipe: BirthRecipe): void;
|
|
118
|
-
/**
|
|
97
|
+
/** Declare that one business aggregate is given its own durable life. */
|
|
119
98
|
export function birth<P extends Record<string, unknown>, F extends Record<string, Field>>(
|
|
120
99
|
aggregate: AggregateHandle<string, F>,
|
|
121
100
|
spec: BusinessBirthSpec<P, F>,
|
|
@@ -184,39 +163,19 @@ interface MintedRef {
|
|
|
184
163
|
|
|
185
164
|
/**
|
|
186
165
|
* @deprecated Tenant law should declare `birth(Aggregate, spec)`. This framework helper remains for the
|
|
187
|
-
* built-in
|
|
188
|
-
*
|
|
189
|
-
* `birth.keyed(...)` — THE way to birth a child workspace. A workspace exists to be USED, and using it means
|
|
190
|
-
* FINDING IT AGAIN, so its name is ALWAYS derived from a `key` the caller owns (a business id, an owner uid)
|
|
191
|
-
* via {@link workspaceNameForKey}. The client re-derives the SAME name with `deriveWorkspaceName(namespace,
|
|
192
|
-
* key)` — so the born workspace is addressable from the key alone, with zero persistence. Same key ⇒ same
|
|
193
|
-
* workspace (re-birth is idempotent). This is the only birth verb you need.
|
|
166
|
+
* built-in framework laws that implement that declaration.
|
|
194
167
|
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* .creates(EstateBirth)
|
|
198
|
-
* .payload(z.object({ estateId: z.string(), owner: z.string(), frameworkHash: z.string(), lawHash: z.string(), bornAt: z.string() }))
|
|
199
|
-
* .plan((p) => {
|
|
200
|
-
* birth.keyed({ key: p.estateId, namespace: "estate", genesisChain: (ws) => [
|
|
201
|
-
* { domain: "bootstrap", directiveId: "installDomain", actor: p.owner, payload: { domainHash: p.frameworkHash }, domainHash: p.frameworkHash, domainPackageB64: "" },
|
|
202
|
-
* { domain: "nomos", directiveId: "installDomain", actor: p.owner, payload: { domainHash: p.lawHash }, domainHash: p.lawHash, domainPackageB64: "" },
|
|
203
|
-
* { domain: "estate", directiveId: "seedEstate", actor: p.owner, payload: { estateId: p.estateId, workspaceName: ws, bornAt: p.bornAt } },
|
|
204
|
-
* ] });
|
|
205
|
-
* return [];
|
|
206
|
-
* })
|
|
207
|
-
* .births().requires("estateCreator");
|
|
208
|
-
* // app (Dart / TS): final ws = client.deriveWorkspaceName('estate', estateId); // open it, any time, any device
|
|
209
|
-
* ```
|
|
168
|
+
* Internal compatibility helper: derives a child address from a stable key. Application packages use
|
|
169
|
+
* `birth(Aggregate, spec)`; generated clients expose only `birth(businessFacts)`.
|
|
210
170
|
*/
|
|
211
171
|
export const birthKeyed = ({
|
|
212
172
|
key,
|
|
213
173
|
namespace,
|
|
214
174
|
genesisChain,
|
|
215
175
|
}: {
|
|
216
|
-
/**
|
|
217
|
-
* born workspace by re-deriving its name from this same key — so pick a key the client will have in hand. */
|
|
176
|
+
/** A stable id from which the framework can re-derive the durable address. */
|
|
218
177
|
key: string;
|
|
219
|
-
/**
|
|
178
|
+
/** Internal address namespace. */
|
|
220
179
|
namespace: string;
|
|
221
180
|
genesisChain: GenesisChainOrBuilder;
|
|
222
181
|
}): string => {
|
package/src/business_birth.ts
CHANGED
|
@@ -258,7 +258,7 @@ export function expandBusinessBirthExports<M extends Record<string, unknown>>(
|
|
|
258
258
|
if (declarations.length > 1) {
|
|
259
259
|
throw new Error(
|
|
260
260
|
`domain '${domain}' declares ${declarations.length} business births; one generated birth(payload) cannot be ` +
|
|
261
|
-
"ambiguous — model one
|
|
261
|
+
"ambiguous — model one born aggregate root per domain",
|
|
262
262
|
);
|
|
263
263
|
}
|
|
264
264
|
const lowered = lowerOne(declarations[0]!, domain);
|
|
@@ -348,8 +348,8 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
348
348
|
}).businessBirth;
|
|
349
349
|
if (businessBirth !== undefined) {
|
|
350
350
|
methods.push(
|
|
351
|
-
` /// Give one ${businessBirth.aggregateId} its own durable life. Nomos owns identity
|
|
352
|
-
` ///
|
|
351
|
+
` /// Give one ${businessBirth.aggregateId} its own durable life. Nomos owns identity, authorship,`,
|
|
352
|
+
` /// provenance, persistence and recovery; the app supplies only business facts.`,
|
|
353
353
|
` Future<NomosRef> birth(${payloadClass} payload) async {`,
|
|
354
354
|
` final principal = actor ?? bridge.principal;`,
|
|
355
355
|
` if (principal == null || principal.trim().isEmpty) {`,
|
package/src/codegen_proof.ts
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* counts (the same composed modules every other emitter sees) and emits a
|
|
15
15
|
* domain-shaped live proof:
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* signed session home → connect via the GENERATED typed client →
|
|
18
|
+
* fetch-TRAPPED offline dispatch of the first `.creates`
|
|
19
19
|
* directive (payload synthesized from its zod schema) → first declared query +
|
|
20
20
|
* first count answered LOCALLY → sync (edge admission) → the CLOUD answers →
|
|
21
21
|
* two-client AddWins concurrency (when the law has the shape for it) →
|
|
@@ -30,8 +30,7 @@
|
|
|
30
30
|
* the remedy — the compile then SKIPS the proof (the law still ships) and says why.
|
|
31
31
|
*
|
|
32
32
|
* Deterministic codegen: the emitted bytes are a pure function of the modules +
|
|
33
|
-
* package name + domain hash — no timestamps
|
|
34
|
-
* throwaway workspace name at RUN time, not at generation time).
|
|
33
|
+
* package name + domain hash — no timestamps or generation-time randomness.
|
|
35
34
|
*/
|
|
36
35
|
import type { z } from "zod";
|
|
37
36
|
|
|
@@ -1720,6 +1719,9 @@ interface DiscoveredGenesisStep {
|
|
|
1720
1719
|
* synthesized payload (the synthesizer only produces `sample-*` placeholders, which the child install
|
|
1721
1720
|
* would refuse). */
|
|
1722
1721
|
export interface ChildBirthLeg {
|
|
1722
|
+
/** Present for the one application-facing `birth(Aggregate, spec)` declaration. Proof renderers use the
|
|
1723
|
+
* business noun and keep the lowered framework ceremony out of normal output. */
|
|
1724
|
+
readonly businessAggregateId?: string;
|
|
1723
1725
|
/** The domain the `.births()` directive lives in. */
|
|
1724
1726
|
readonly parentDomain: string;
|
|
1725
1727
|
/** The `.births()` directive id the parent drives to birth the child. */
|
|
@@ -2021,9 +2023,11 @@ export function discoverChildBirthLegOrReason(
|
|
|
2021
2023
|
if (frameworkHashField !== undefined) delete birthPayload[frameworkHashField];
|
|
2022
2024
|
if (lawHashField !== undefined) delete birthPayload[lawHashField];
|
|
2023
2025
|
|
|
2026
|
+
const businessAggregateId = (birthDir as unknown as { businessBirth?: { aggregateId?: string } }).businessBirth?.aggregateId;
|
|
2024
2027
|
const leg: ChildBirthLeg = {
|
|
2025
2028
|
parentDomain,
|
|
2026
2029
|
birthDirectiveId: birthDir.id,
|
|
2030
|
+
...(businessAggregateId !== undefined ? { businessAggregateId } : {}),
|
|
2027
2031
|
birthPayload,
|
|
2028
2032
|
birthAutoStamped: synth.autoStamped,
|
|
2029
2033
|
...(actorField !== undefined ? { birthActorField: actorField } : {}),
|
|
@@ -2143,7 +2147,7 @@ export interface TsProofOptions {
|
|
|
2143
2147
|
* · `standalone-provable` — a `.creates` write proven offline (dispatch → declared query/count locally).
|
|
2144
2148
|
* · `child-birth-provable` — a `.births()` directive proven END-TO-END into the child it births.
|
|
2145
2149
|
* · `relation-bootstrap-provable` — a `.requires(R)`-gated write proven to SUCCEED after granting R (seed-then-prove).
|
|
2146
|
-
* · `live-cloud-provable` — the
|
|
2150
|
+
* · `live-cloud-provable` — the same generated application proof completes in durable cloud custody. */
|
|
2147
2151
|
export type ProofKind = "compile-valid" | "standalone-provable" | "child-birth-provable" | "relation-bootstrap-provable" | "live-cloud-provable";
|
|
2148
2152
|
|
|
2149
2153
|
/** The machine-readable OFFLINE legs for ONE domain (the `@nomos-proof-legs`
|
|
@@ -2529,52 +2533,34 @@ export function proofLegsIndex(modules: readonly DomainModule[]): { default: str
|
|
|
2529
2533
|
// ── shared `.mts` fragments (the v2 contract — G1: no host secret; deploy is OPEN + kernel-gated) ─────────
|
|
2530
2534
|
|
|
2531
2535
|
/**
|
|
2532
|
-
* THE
|
|
2533
|
-
*
|
|
2534
|
-
*
|
|
2535
|
-
* same header `cli/src/cloud.ts deploy()` + the 0.18 host-strip lane present. Declares `fail`/`ok`/`deploy`/
|
|
2536
|
-
* `r`/`d`/`runError`/`PRINCIPAL` and opens the `try {` (closed by {@link v2CatchFinally}).
|
|
2536
|
+
* THE LIVE PREAMBLE: `githolon proof --live` has already established the signed session home and deployed
|
|
2537
|
+
* this package into it. Generated proof code receives only that ready session and drives the application verb.
|
|
2538
|
+
* The old raw create/deploy lane was a second product model and could only create registry/custody orphans.
|
|
2537
2539
|
*/
|
|
2538
2540
|
function v2IdentityAndCleanupPreamble(opts: TsProofOptions, hashConst: string, n: () => number): string[] {
|
|
2541
|
+
void opts;
|
|
2542
|
+
void hashConst;
|
|
2539
2543
|
return [
|
|
2540
|
-
`//
|
|
2541
|
-
`//
|
|
2542
|
-
`// + \`githolon ws create/deploy\`. (G1: a create returns NO workspaceSecret — ownership is a ledger`,
|
|
2543
|
-
`// fact; deploy is OPEN + kernel-gated, authenticated by the principal, not a host secret.)`,
|
|
2544
|
+
`// Nomos prepared this signed home and installed the package before entering generated application code.`,
|
|
2545
|
+
`// The application proof supplies business facts only; identity/custody/law selection stay underneath.`,
|
|
2544
2546
|
`const fail: (m: string) => never = (m) => { throw new Error(m); };`,
|
|
2545
2547
|
`const ok = (m: string) => console.log("✓ " + m);`,
|
|
2546
2548
|
`const PRINCIPAL = process.env.NOMOS_PRINCIPAL || "githolon-proof";`,
|
|
2549
|
+
`const AUTHOR_SECRET = process.env.NOMOS_AUTHOR_SECRET || "";`,
|
|
2547
2550
|
``,
|
|
2548
|
-
`
|
|
2549
|
-
``,
|
|
2550
|
-
`// CLEANUP CONTRACT: the throwaway workspace is RETIRED on every exit (green or jammed) — a proof never`,
|
|
2551
|
-
`// leaves orphans. PROOF_KEEP=1 (githolon proof --live --keep) keeps it and prints the persist command.`,
|
|
2551
|
+
`let d: any;`,
|
|
2552
2552
|
`let runError: unknown = null;`,
|
|
2553
|
-
`let born_ws = false;`,
|
|
2554
2553
|
`try {`,
|
|
2555
2554
|
``,
|
|
2556
|
-
`// ${n()}.
|
|
2557
|
-
`
|
|
2558
|
-
`
|
|
2559
|
-
`if (!d.ok) fail(\`create workspace: \${JSON.stringify(d)}\`);`,
|
|
2560
|
-
`born_ws = true;`,
|
|
2561
|
-
`ok(\`workspace \${WS} created (no host secret — ownership is a ledger fact; PROOF_KEEP=1 keeps it)\`);`,
|
|
2562
|
-
``,
|
|
2563
|
-
`// ${n()}. deploy YOUR compiled law via the OPEN lane (POST /domains, no Bearer — kernel-judged authority,`,
|
|
2564
|
-
`// authenticated as PRINCIPAL via x-nomos-principal; a refusal answers as a TYPED kernel verdict 422)`,
|
|
2565
|
-
`r = await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/domains\`, { method: "POST", headers: { "content-type": "application/json", "x-nomos-principal": PRINCIPAL }, body: JSON.stringify(deploy) });`,
|
|
2566
|
-
`d = await r.json();`,
|
|
2567
|
-
`if (!d.ok || d.installation?.[0]?.data?.["status.phase"] !== "Active") fail(\`deploy: \${JSON.stringify(d)}\`);`,
|
|
2568
|
-
`if (d.domainHash !== ${hashConst}) fail(\`deploy hash \${d.domainHash} != the generated client's \${${hashConst}}\`);`,
|
|
2569
|
-
`ok(\`law deployed Active — \${${hashConst}.slice(0, 12)}… (the content hash baked into your typed client)\`);`,
|
|
2555
|
+
`// ${n()}. the one managed entry: the logged-in user's durable Nomos home`,
|
|
2556
|
+
`if (process.env.NOMOS_PROOF_MANAGED !== "1" || !AUTHOR_SECRET) fail("run this cloud proof through: githolon proof --live");`,
|
|
2557
|
+
`ok(\`Nomos home ready — \${WS}\`);`,
|
|
2570
2558
|
];
|
|
2571
2559
|
}
|
|
2572
2560
|
|
|
2573
2561
|
/**
|
|
2574
|
-
*
|
|
2575
|
-
*
|
|
2576
|
-
* (NOMOS_PROOF_MANAGED=1) the CLI created the workspace REGISTERED and retires it lawfully after exit;
|
|
2577
|
-
* standalone, the proof warns LOUDLY and names the manual command. PROOF_KEEP=1 keeps it.
|
|
2562
|
+
* The generated cloud proof only owns its application call. Its durable signed home is framework state,
|
|
2563
|
+
* so there is no parallel throwaway-provisioning or cleanup protocol to expose here.
|
|
2578
2564
|
*/
|
|
2579
2565
|
function v2CatchFinally(): string[] {
|
|
2580
2566
|
return [
|
|
@@ -2582,19 +2568,6 @@ function v2CatchFinally(): string[] {
|
|
|
2582
2568
|
` runError = e;`,
|
|
2583
2569
|
` console.error("✗ " + ((e as Error)?.message ?? e));`,
|
|
2584
2570
|
`} finally {`,
|
|
2585
|
-
` // THE CLEANUP CONTRACT: never a SILENTLY orphaned cloud workspace. Retirement is a SIGNED`,
|
|
2586
|
-
` // governance offer (retireOwnWorkspace at the parent) — the old open-lane POST /retire is dead`,
|
|
2587
|
-
` // post-warrant-flip. \`githolon proof --live\` orchestrates it (NOMOS_PROOF_MANAGED=1: it created`,
|
|
2588
|
-
` // the workspace REGISTERED and retires it after this process exits). Run standalone, this proof`,
|
|
2589
|
-
` // cannot lawfully retire (no signing key) — so it says so LOUDLY and names the exact command.`,
|
|
2590
|
-
` if (born_ws && process.env.PROOF_KEEP === "1") {`,
|
|
2591
|
-
` console.log(\`workspace \${WS} KEPT — retire it when done: githolon ws retire \${WS}\`);`,
|
|
2592
|
-
` } else if (born_ws && process.env.NOMOS_PROOF_MANAGED === "1") {`,
|
|
2593
|
-
` console.log(\` workspace \${WS} — retirement handled by githolon proof --live (signed retireOwnWorkspace)\`);`,
|
|
2594
|
-
` } else if (born_ws) {`,
|
|
2595
|
-
` console.error(\`⚠ THROWAWAY WORKSPACE NOT RETIRED: \${WS} (retirement is a signed governance offer this standalone proof cannot author)\`);`,
|
|
2596
|
-
` console.error(\`⚠ retire it manually: githolon ws retire \${WS} (run the managed lane next time: githolon proof --live)\`);`,
|
|
2597
|
-
` }`,
|
|
2598
2571
|
` process.exit(runError ? 1 : 0);`,
|
|
2599
2572
|
`}`,
|
|
2600
2573
|
];
|
|
@@ -2667,6 +2640,39 @@ function liveAuthorityChainLines(setup: RelationBootstrapLeg, actorVar: string,
|
|
|
2667
2640
|
return lines;
|
|
2668
2641
|
}
|
|
2669
2642
|
|
|
2643
|
+
function liveBusinessBirthLegLines(cb: ChildBirthLeg, factory: string, n: () => number): string[] {
|
|
2644
|
+
const businessPayload = Object.fromEntries(
|
|
2645
|
+
Object.entries(cb.birthPayload).filter(([field]) => !["principal", "bornAt", "ownerKeyHash", "birthCert"].includes(field)),
|
|
2646
|
+
);
|
|
2647
|
+
return [
|
|
2648
|
+
`// ${n()}. THE APPLICATION SURFACE: one generated business birth`,
|
|
2649
|
+
`{`,
|
|
2650
|
+
` const holon = await connect({ cloud: CLOUD, workspace: WS, clientId: "business-birth-proof", principal: PRINCIPAL, authorSecret: AUTHOR_SECRET });`,
|
|
2651
|
+
` const app = ${factory}(holon, { actor: PRINCIPAL });`,
|
|
2652
|
+
` const facts: Record<string, unknown> = ${JSON.stringify(businessPayload)};`,
|
|
2653
|
+
` const run = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);`,
|
|
2654
|
+
` for (const [field, value] of Object.entries(facts)) {`,
|
|
2655
|
+
` if (typeof value === "string" && value.startsWith("sample-") && value.length > 7) {`,
|
|
2656
|
+
` facts[field] = ("sample-" + run).slice(0, value.length);`,
|
|
2657
|
+
` }`,
|
|
2658
|
+
` }`,
|
|
2659
|
+
` const born = await app.birth(facts as never);`,
|
|
2660
|
+
` const childWs = born.workspace;`,
|
|
2661
|
+
` if (!childWs) fail("app.birth committed without returning the born workspace");`,
|
|
2662
|
+
` ok(\`app.birth(${cb.businessAggregateId}) — business facts became \${childWs}\`);`,
|
|
2663
|
+
` ok("identity, provenance, persistence and recovery verified before app.birth resolved");`,
|
|
2664
|
+
...(cb.childSeedInstanceId !== undefined
|
|
2665
|
+
? [
|
|
2666
|
+
` const restored = await fetch(\`\${CLOUD}/v2/workspaces/\${childWs}/aggregates/\${encodeURIComponent(${JSON.stringify(cb.childSeedInstanceId)})}\`).then((r) => r.json());`,
|
|
2667
|
+
` if (!(restored.rows?.length >= 1)) fail(\`recovered ${cb.businessAggregateId} has no business root: \${JSON.stringify(restored)}\`);`,
|
|
2668
|
+
` ok("business root read back from durable custody");`,
|
|
2669
|
+
]
|
|
2670
|
+
: []),
|
|
2671
|
+
`}`,
|
|
2672
|
+
``,
|
|
2673
|
+
];
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2670
2676
|
function liveChildBirthLegLines(cb: ChildBirthLeg, opts: TsProofOptions, n: () => number): string[] {
|
|
2671
2677
|
const hashConst = tsHashConstName(opts.packageName);
|
|
2672
2678
|
// The birth payload literal: the synthesized values + the REAL hashes onto the discovered fields + the
|
|
@@ -2802,11 +2808,10 @@ function generateBirthOnlyProof(
|
|
|
2802
2808
|
`// recompile, and this proof reshapes itself — no test rewriting. Run it:`,
|
|
2803
2809
|
`//`,
|
|
2804
2810
|
`// npx githolon proof (offline legs on a local holon)`,
|
|
2805
|
-
`// npx githolon proof --live (
|
|
2811
|
+
`// npx githolon proof --live (the same application proof in durable Nomos custody)`,
|
|
2806
2812
|
`//`,
|
|
2807
2813
|
`// Regenerated on every compile; do not edit.`,
|
|
2808
2814
|
`// @nomos-proof-legs ${legsMarker}`,
|
|
2809
|
-
`import { readFileSync } from "node:fs";`,
|
|
2810
2815
|
`import { connect, encodeIntentOfferBytes } from "@githolon/client";`,
|
|
2811
2816
|
`import { ${factory}, ${hashConst} } from "./${opts.packageName}.client.ts";`,
|
|
2812
2817
|
`void connect; void encodeIntentOfferBytes; void ${factory};`,
|
|
@@ -2816,7 +2821,9 @@ function generateBirthOnlyProof(
|
|
|
2816
2821
|
``,
|
|
2817
2822
|
...v2IdentityAndCleanupPreamble(opts, hashConst, n),
|
|
2818
2823
|
``,
|
|
2819
|
-
...
|
|
2824
|
+
...(cb.businessAggregateId !== undefined
|
|
2825
|
+
? liveBusinessBirthLegLines(cb, factory, n)
|
|
2826
|
+
: liveChildBirthLegLines(cb, opts, n)),
|
|
2820
2827
|
// THE RELATION-BOOTSTRAP LEG (incl. LAYER 4 born-context): a birth-only domain may ALSO carry a gated
|
|
2821
2828
|
// non-birth directive whose authority is established by the birth genesis (co2 home's updateHomeProfile
|
|
2822
2829
|
// via birthHome→seedHomeCreator). Emit it so the birth-only proof names it (the born-context proof itself
|
|
@@ -2866,7 +2873,7 @@ function generateBootstrapOnlyProof(
|
|
|
2866
2873
|
`// law, recompile, and this proof reshapes itself — no test rewriting. Run it:`,
|
|
2867
2874
|
`//`,
|
|
2868
2875
|
`// npx githolon proof (offline legs on a local holon)`,
|
|
2869
|
-
`// npx githolon proof --live (
|
|
2876
|
+
`// npx githolon proof --live (the same application proof in durable Nomos custody)`,
|
|
2870
2877
|
`//`,
|
|
2871
2878
|
`// Regenerated on every compile; do not edit.`,
|
|
2872
2879
|
`// @nomos-proof-legs ${legsMarker}`,
|
|
@@ -2881,7 +2888,7 @@ function generateBootstrapOnlyProof(
|
|
|
2881
2888
|
...v2IdentityAndCleanupPreamble(opts, hashConst, n),
|
|
2882
2889
|
``,
|
|
2883
2890
|
`// ${n()}. connect + bind the GENERATED typed client (the relation-bootstrap leg drives it with an actor)`,
|
|
2884
|
-
`const holon = await connect({ cloud: CLOUD, workspace: WS, clientId: "proof-1" });`,
|
|
2891
|
+
`const holon = await connect({ cloud: CLOUD, workspace: WS, clientId: "proof-1", principal: PRINCIPAL, authorSecret: AUTHOR_SECRET });`,
|
|
2885
2892
|
`ok(\`web client connected — ${factory} bound (replica \${holon.replica.slice(0, 6)}…)\`);`,
|
|
2886
2893
|
``,
|
|
2887
2894
|
// The SAME relation-bootstrap emitter the standalone proof uses — seed authority, then prove the GATED
|
|
@@ -3032,7 +3039,6 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
|
|
|
3032
3039
|
`// The flow mirrors the scaffold's test/e2e.mts (docs/ in a scaffolded app explains`,
|
|
3033
3040
|
`// every call — start at docs/01-mental-model.md). Regenerated on every compile; do not edit.`,
|
|
3034
3041
|
`// @nomos-proof-legs ${legsMarker}`,
|
|
3035
|
-
`import { readFileSync } from "node:fs";`,
|
|
3036
3042
|
`import { connect, encodeIntentOfferBytes } from "@githolon/client";`,
|
|
3037
3043
|
`import { ${factory}, ${hashConst} } from "./${opts.packageName}.client.ts";`,
|
|
3038
3044
|
``,
|
|
@@ -3042,7 +3048,7 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
|
|
|
3042
3048
|
...v2IdentityAndCleanupPreamble(opts, hashConst, n),
|
|
3043
3049
|
``,
|
|
3044
3050
|
`// ${n()}. connect + bind the GENERATED typed client`,
|
|
3045
|
-
`const holon = await connect({ cloud: CLOUD, workspace: WS, clientId: "proof-1" });`,
|
|
3051
|
+
`const holon = await connect({ cloud: CLOUD, workspace: WS, clientId: "proof-1", principal: PRINCIPAL, authorSecret: AUTHOR_SECRET });`,
|
|
3046
3052
|
`const app = ${factory}(holon);`,
|
|
3047
3053
|
`ok(\`web client connected — ${factory} bound (replica \${holon.replica.slice(0, 6)}…)\`);`,
|
|
3048
3054
|
``,
|
|
@@ -3208,8 +3214,8 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
|
|
|
3208
3214
|
`// ${n()}. CONCURRENCY — two writers, two clientIds, BLIND to each other: each adds to`,
|
|
3209
3215
|
`// the AddWins set \`${agg.id}.${setField}\` via \`${domain}/${cd.id}\` offline; admission`,
|
|
3210
3216
|
`// merges both and the union keeps every add.`,
|
|
3211
|
-
`const writerA = await connect({ cloud: CLOUD, workspace: WS, clientId: "proof-a" });`,
|
|
3212
|
-
`const writerB = await connect({ cloud: CLOUD, workspace: WS, clientId: "proof-b" });`,
|
|
3217
|
+
`const writerA = await connect({ cloud: CLOUD, workspace: WS, clientId: "proof-a", principal: PRINCIPAL, authorSecret: AUTHOR_SECRET });`,
|
|
3218
|
+
`const writerB = await connect({ cloud: CLOUD, workspace: WS, clientId: "proof-b", principal: PRINCIPAL, authorSecret: AUTHOR_SECRET });`,
|
|
3213
3219
|
`await ${factory}(writerA).${cd.id}(${concPayload("a")});`,
|
|
3214
3220
|
`await ${factory}(writerB).${cd.id}(${concPayload("b")});`,
|
|
3215
3221
|
`await writerA.sync();`,
|
package/src/codegen_ts.ts
CHANGED
|
@@ -1069,6 +1069,8 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1069
1069
|
` principal?: string;`,
|
|
1070
1070
|
` /** Persist local custody and reconcile its configured remote. Business birth awaits this when available. */`,
|
|
1071
1071
|
` sync?(): Promise<unknown>;`,
|
|
1072
|
+
` /** Complete durable custody and cold-chain verification for a newly born aggregate. */`,
|
|
1073
|
+
` settleBirth?(workspace: string, opts?: { timeoutMs?: number; pollMs?: number }): Promise<unknown>;`,
|
|
1072
1074
|
...(encPresent
|
|
1073
1075
|
? [
|
|
1074
1076
|
` /** \`keys\` (E2E sealing): the reader's held scope keys, injected so encrypted fields decrypt;`,
|
|
@@ -1946,6 +1948,8 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1946
1948
|
` const born = typeof result === "object" && result !== null && Array.isArray(result.born) ? result.born[0] : undefined;`,
|
|
1947
1949
|
` if (typeof born !== "object" || born === null || typeof born.workspace !== "string" || !born.workspace) throw new Error("birth() committed without returning the born workspace");`,
|
|
1948
1950
|
` await holon.sync?.();`,
|
|
1951
|
+
` if (!holon.settleBirth) throw new Error("birth() requires the current @githolon/client to complete durable recovery");`,
|
|
1952
|
+
` await holon.settleBirth(born.workspace);`,
|
|
1949
1953
|
` return { workspace: born.workspace, type: ${JSON.stringify(businessBirth.aggregateId)}, id: ${JSON.stringify(businessBirth.rootId)} };`,
|
|
1950
1954
|
` },`,
|
|
1951
1955
|
);
|
|
@@ -2961,19 +2961,16 @@ async function main(): Promise<void> {
|
|
|
2961
2961
|
console.log(` parses on every kernel (larger deploy body; may need the signed-upload deploy lane).`);
|
|
2962
2962
|
}
|
|
2963
2963
|
console.log(``);
|
|
2964
|
-
console.log(`
|
|
2965
|
-
console.log(` githolon login --agent && githolon ws create <ws> && githolon deploy <ws> # no secret — deploy is OPEN, kernel-judged at the offer gate`);
|
|
2966
|
-
console.log(` # raw lane: curl -X POST -H 'content-type: application/json' -H 'x-nomos-principal: <your-auth-uid>' \\`);
|
|
2967
|
-
console.log(` # --data-binary @${rel(deployPath)} https://nomos.captainapp.co.uk/v2/workspaces/<ws>/domains`);
|
|
2964
|
+
console.log(`your business path:`);
|
|
2968
2965
|
if (proofPath !== undefined) {
|
|
2969
|
-
console.log(`
|
|
2970
|
-
console.log(`
|
|
2971
|
-
console.log(`
|
|
2966
|
+
console.log(` npx githolon proof # generated birth -> verify -> durable restore -> business read-back`);
|
|
2967
|
+
console.log(` npx githolon dev # rerun that proof on every save`);
|
|
2968
|
+
console.log(` npx githolon proof --live # the same business proof against Nomos Cloud`);
|
|
2972
2969
|
if (proofDomains.length > 1) {
|
|
2973
2970
|
console.log(` this package proves ${proofDomains.length} domains (${proofDomains.join(", ")}) — default '${proofDomains[0]}'; pick another: npx githolon proof --domain <key>`);
|
|
2974
2971
|
}
|
|
2975
2972
|
} else {
|
|
2976
|
-
console.log(`
|
|
2973
|
+
console.log(` add a business birth declaration to generate an end-to-end proof`);
|
|
2977
2974
|
}
|
|
2978
2975
|
|
|
2979
2976
|
if (analyzeFlag) runDartAnalyze(dartOut);
|
|
@@ -156,7 +156,8 @@ export const workspaceSelfWriteOnce = workspaceInvariant("WorkspaceSelfWriteOnce
|
|
|
156
156
|
// and the ONE wasm gate runs `engine.check(schema, "workspace:self", relation, principal)` over the
|
|
157
157
|
// pre-apply folded relation tuples — refusing any author who does not HOLD the relation. The schema +
|
|
158
158
|
// the `.requires` together arm the gate (the `nomosRelations` + `nomosRelationGate` era keys); the
|
|
159
|
-
//
|
|
159
|
+
// static governance roles use the native relation gate. Creation posture is deliberately dynamic law:
|
|
160
|
+
// open/unset admits any proven author, invite checks `creator`, and closed checks `admin`.
|
|
160
161
|
//
|
|
161
162
|
// THE CONTROL-PLANE CARVE-OUT — LEAF-ONLY (`allow: [user(), key()]`, never a userset/wildcard). A
|
|
162
163
|
// `workspace:self#admin@<group>#member` or `@<type>:*` would let a group/wildcard escalate to admin
|
|
@@ -164,8 +165,8 @@ export const workspaceSelfWriteOnce = workspaceInvariant("WorkspaceSelfWriteOnce
|
|
|
164
165
|
// side) DISCARDED at evaluation. Each relation is a flat `self()` (directly-stored tuples) — NO rewrite
|
|
165
166
|
// algebra (no computed/from/union): control-plane authority is direct membership only.
|
|
166
167
|
// * admin — grantAdmin/revokeAdmin + every admin-mutating governance directive.
|
|
167
|
-
// * creator —
|
|
168
|
-
//
|
|
168
|
+
// * creator — the invite-posture birth role for createWorkspace + birthChild. Open posture needs
|
|
169
|
+
// no pre-seated role; closed posture uses admin. A subject is user: or key:.
|
|
169
170
|
// * platformCreator — VESTIGIAL: its directives (birthPlatform/grant·revokePlatformCreation) are removed;
|
|
170
171
|
// no .requires references it. Kept until the #58 disposition pass (with kind/role).
|
|
171
172
|
// SELF-RELATIVE: custody guarantees a workspace only ever folds its OWN relations table, so
|
|
@@ -370,9 +371,8 @@ export const CloudPolicy = aggregate("CloudPolicy", {
|
|
|
370
371
|
* The workspace-CREATION posture dial (cloud_sovereignty.md S6) —
|
|
371
372
|
* `"open"` (anyone creates), `"invite"` (an active CreationGrant key
|
|
372
373
|
* required), or `"closed"` (admins only). OPTIONAL: absent keeps the
|
|
373
|
-
* implicit posture
|
|
374
|
-
*
|
|
375
|
-
* exactly as before.
|
|
374
|
+
* implicit open posture. Invite and closed are explicit law facts; merely
|
|
375
|
+
* granting somebody authority never changes the door for everybody else.
|
|
376
376
|
*/
|
|
377
377
|
creationPosture: t.string().merge(Lww).optional(),
|
|
378
378
|
/** Who last set the policy (attribution). */
|
|
@@ -400,8 +400,8 @@ export const QuotaGrant = aggregate("QuotaGrant", {
|
|
|
400
400
|
|
|
401
401
|
/**
|
|
402
402
|
* PlatformGrant — legacy audit surface for platform invites. Platform creation now rides
|
|
403
|
-
* {@link birthChild} with genesis data
|
|
404
|
-
* inert compatibility/audit data, never a worker config.
|
|
403
|
+
* {@link birthChild} with genesis data under the same open/invite/closed creation posture as every birth;
|
|
404
|
+
* any remaining PlatformGrant rows are inert compatibility/audit data, never a worker config.
|
|
405
405
|
*/
|
|
406
406
|
export const PlatformGrant = aggregate("PlatformGrant", {
|
|
407
407
|
/** The auth-provider principal uid invited to create platforms. */
|
|
@@ -466,8 +466,8 @@ export const CapabilityBinding = aggregate("CapabilityBinding", {
|
|
|
466
466
|
// and seeds the genesis. Absent ⇒ a plain lineage record, materialized by boot-on-touch (never drained).
|
|
467
467
|
// `kind` is set PURELY as a derived label off genesis presence — a vestige for old readers; NOTHING decides
|
|
468
468
|
// on it (the host now keys off foundingAdminHash, not the label). There is NO node-class: a platform is just
|
|
469
|
-
// a workspace whose birth order carried a genesis. (`birthChild` is the one unified directive
|
|
470
|
-
//
|
|
469
|
+
// a workspace whose birth order carried a genesis. (`birthChild` is the one unified directive;
|
|
470
|
+
// `createWorkspace` remains only as a deprecated compatibility alias.)
|
|
471
471
|
// Returns the record builder so a caller (birthChild) can author the law-authored genesis chain onto it.
|
|
472
472
|
function planBirthRecord(
|
|
473
473
|
p: {
|
|
@@ -708,7 +708,7 @@ function quotaVerdict(
|
|
|
708
708
|
* worker provisions and authors {@link provisionWorkspace}.
|
|
709
709
|
*
|
|
710
710
|
* @deprecated SUPERSEDED by {@link birthChild} (the `kind`-collapse) — a plain birth is `birthChild`
|
|
711
|
-
* with no genesis. Kept as a compat alias (same
|
|
711
|
+
* with no genesis. Kept as a compat alias (same creation-posture law) during migration; the host still
|
|
712
712
|
* authors it by name for namespaced-child + upload births. Retire once those callers move to birthChild.
|
|
713
713
|
*/
|
|
714
714
|
export const createWorkspace = directive("createWorkspace")
|
|
@@ -746,8 +746,7 @@ export const createWorkspace = directive("createWorkspace")
|
|
|
746
746
|
planBirthRecord(p);
|
|
747
747
|
return [];
|
|
748
748
|
})
|
|
749
|
-
.reads(workspacesByPrincipal, quotaByPrincipal, cloudPolicy) //
|
|
750
|
-
.requires("creator"); // posture-gated creation: only a #creator holder may request a workspace
|
|
749
|
+
.reads(workspacesByPrincipal, quotaByPrincipal, cloudPolicy); // quota + creation posture are law invariants
|
|
751
750
|
|
|
752
751
|
/**
|
|
753
752
|
* birthCloud — THE CLOUD'S OWN BIRTH INTENT. Root authors this once, into its own
|
|
@@ -892,7 +891,7 @@ export const retireWorkspace = directive("retireWorkspace")
|
|
|
892
891
|
|
|
893
892
|
/**
|
|
894
893
|
* retireOwnWorkspace — SELF-SERVICE retirement: the principal who CREATED a workspace may retire it (you
|
|
895
|
-
* made it, you can withdraw it — e.g.
|
|
894
|
+
* made it, you can withdraw it — e.g. an operator removing an obsolete sandbox). Same
|
|
896
895
|
* effect as {@link retireWorkspace} (status → retired, custody untouched) but gated by `creator` (the birth
|
|
897
896
|
* authority) and SCOPED by {@link retireOwnOnly} to records THIS principal created — so a creator can only
|
|
898
897
|
* retire its OWN workspaces, never another's. Admins keep the unrestricted {@link retireWorkspace}.
|
|
@@ -1205,11 +1204,11 @@ export const rebirthConsentByWorkspace = query("rebirthConsentByWorkspace")
|
|
|
1205
1204
|
.returns(RebirthConsent);
|
|
1206
1205
|
|
|
1207
1206
|
// ─────────────────────────── BIRTH AS A NOMOS PRIMITIVE ──────────────────────────────────────────────
|
|
1208
|
-
// (birthPlatform is GONE
|
|
1207
|
+
// (birthPlatform is GONE; birthChild is the one birth directive.)
|
|
1209
1208
|
// THE RECORD IS THE OBLIGATION (architecture/githolon_host_contract.md "Birth is a capability" +
|
|
1210
1209
|
// provisioning_as_obligations.md). Birth is an IMPURE capability: the parent ORDERS (the law-gated
|
|
1211
1210
|
// birthChild mints a CloudWorkspace at status:requested carrying foundingAdminHash + the LAW-AUTHORED
|
|
1212
|
-
// genesisChain
|
|
1211
|
+
// genesisChain under the parent's creation posture), the BIRTH CAPABILITY PROVIDER fulfils it (materialize:
|
|
1213
1212
|
// install the parent's law + REPLAY the genesisChain opaquely — the child's OWN kernel admitting each), and
|
|
1214
1213
|
// authors provisionWorkspace (the receipt) the parent TRUSTS. No separate obligation aggregate — the minted
|
|
1215
1214
|
// CloudWorkspace IS the order. The provider owns only the impure boot + replay; the LAW owns the recipe (P1)
|
|
@@ -1219,13 +1218,13 @@ export const rebirthConsentByWorkspace = query("rebirthConsentByWorkspace")
|
|
|
1219
1218
|
// ─────────────────────────── birthChild — THE ONE BIRTH DIRECTIVE (the `kind`-collapse) ───────────────
|
|
1220
1219
|
// "There are only workspaces, all equal until a domain is installed." birthChild is the SINGLE directive
|
|
1221
1220
|
// that births any child — a plain leaf OR a platform; the difference is the GENESIS DATA the payload carries,
|
|
1222
|
-
// never a node-class.
|
|
1223
|
-
//
|
|
1224
|
-
//
|
|
1221
|
+
// never a node-class. The same creation posture governs leaf and platform births: open/unset admits any
|
|
1222
|
+
// proven author, invite requires `#creator`, and closed requires `#admin`. The prior `#platformCreator`
|
|
1223
|
+
// second tier is retired.
|
|
1225
1224
|
// • genesis ABSENT → a plain lineage record (no foundingAdminHash); boot-on-touch, never drained.
|
|
1226
1225
|
// • genesis PRESENT → carries {foundingAdminHash, pool}: the birth capability provider drains it
|
|
1227
1226
|
// (the order carries a foundingAdminHash) → materialize + seed genesis. A platform.
|
|
1228
|
-
// Self-relative: the
|
|
1227
|
+
// Self-relative: the posture reads `workspace:self`, so the SAME directive on root births a platform and
|
|
1229
1228
|
// on a platform births a child — no per-level host code (the recursion is the law). Supersedes
|
|
1230
1229
|
// createWorkspace + birthPlatform (kept as compat aliases during migration). See
|
|
1231
1230
|
// architecture/provisioning_as_obligations.md "The `kind`-collapse".
|
|
@@ -1311,8 +1310,7 @@ export const birthChild = directive("birthChild")
|
|
|
1311
1310
|
return [];
|
|
1312
1311
|
})
|
|
1313
1312
|
.reads(workspacesByPrincipal, quotaByPrincipal, cloudPolicy) // arm the WorkspaceQuota captured-read gate (S2)
|
|
1314
|
-
.births() // BIRTH IS A DECLARED KERNEL PRIMITIVE (G3):
|
|
1315
|
-
.requires("creator"); // THE ONE birth authority — a #creator may birth a leaf OR a platform (tier collapsed)
|
|
1313
|
+
.births(); // BIRTH IS A DECLARED KERNEL PRIMITIVE (G3): the kernel folds the carried recipe; posture is invariant law
|
|
1316
1314
|
|
|
1317
1315
|
/** workspacesByStatus — the birth provider drains `requested` CloudWorkspaces that CARRY A foundingAdminHash
|
|
1318
1316
|
* (a genesis order, no ownerGrantHash) → materialize → provisionWorkspace. Plain requested records (no
|
|
@@ -1751,15 +1749,13 @@ export const capabilityBindingByKeyHash = query("capabilityBindingByKeyHash")
|
|
|
1751
1749
|
export { relationTuplesByObject } from "./relations.js";
|
|
1752
1750
|
export { RelationTuple };
|
|
1753
1751
|
|
|
1754
|
-
// ── GOVERNANCE WRITE-AUTHZ
|
|
1755
|
-
//
|
|
1756
|
-
//
|
|
1757
|
-
//
|
|
1758
|
-
//
|
|
1759
|
-
//
|
|
1760
|
-
//
|
|
1761
|
-
// principal (`key:<sha256(bearer)>` for admin/creator, `user:<uid>` for platformCreator), pinned to the
|
|
1762
|
-
// sealed Attribution.actor on every peer — so "who may grant" is a LAW fact, never a host pre-check. The
|
|
1752
|
+
// ── GOVERNANCE WRITE-AUTHZ: NATIVE ROLES + DYNAMIC CREATION POSTURE ───────────────────────────────────
|
|
1753
|
+
// Static governance requirements use the full-native relation gate: a directive carrying
|
|
1754
|
+
// `.requires("<relation>")` is checked by the ONE wasm gate over PRE-APPLY tuples on every door (author /
|
|
1755
|
+
// edge admission / verify_chain replay). Workspace creation is the intentional exception because it is a
|
|
1756
|
+
// three-posture policy, not one static role: the invariant below admits a proven author under open/unset,
|
|
1757
|
+
// checks `#creator` under invite, and checks `#admin` under closed. The captured author principal is pinned
|
|
1758
|
+
// to the sealed Attribution.actor on every peer — so "who may grant or create" is LAW, never a host pre-check. The
|
|
1763
1759
|
// pre-apply roster means `grantAdmin(@key:self)` cannot self-grant (the not-yet-written subject is absent
|
|
1764
1760
|
// in the pre-apply state). SELF-RELATIVE: custody guarantees a workspace only folds its OWN tuples, so the
|
|
1765
1761
|
// SAME law governs root and every born platform. The host's `_adminOk`/`_creationLawOk`/`_platformCheck`
|
|
@@ -1859,6 +1855,76 @@ export const reclaimRequiresRetired = workspaceInvariant("ReclaimRequiresRetired
|
|
|
1859
1855
|
});
|
|
1860
1856
|
|
|
1861
1857
|
// ── THE WORKSPACE-QUOTA GATE (S2 — THE QUOTA LAW, ENFORCED) ────────────────────────────────────────────
|
|
1858
|
+
// The native `.requires("creator")` replacement accidentally collapsed a THREE-POSTURE law into one
|
|
1859
|
+
// permanently invite-only door. Creation is not a static role check: open/unset admits any PROVEN author;
|
|
1860
|
+
// invite admits #creator; closed admits #admin. Keep that policy in an invariant over the committed prefix,
|
|
1861
|
+
// while the native relation evaluator remains the one meaning of "holds #creator/#admin" everywhere else.
|
|
1862
|
+
//
|
|
1863
|
+
// The same invariant binds quota/lineage identity to the proven author. Without it, an open caller could
|
|
1864
|
+
// charge births to a victim principal (or evade its own quota) by changing two payload strings. A historical
|
|
1865
|
+
// bare uid and its canonical `user:<uid>` subject are the same identity; a self-authenticating `key:<hash>`
|
|
1866
|
+
// must match byte-for-byte.
|
|
1867
|
+
const CREATOR_TUPLE_ID = "pre:" + relationTupleInstanceId(SELF_ADMIN_OBJECT, "creator");
|
|
1868
|
+
const ADMIN_TUPLE_ID = "pre:" + relationTupleInstanceId(SELF_ADMIN_OBJECT, "admin");
|
|
1869
|
+
const CREATION_POLICY_ID = "pre:cloud-policy:cloud";
|
|
1870
|
+
|
|
1871
|
+
function canonicalBirthIdentity(value: unknown): string {
|
|
1872
|
+
const s = String(value ?? "").trim();
|
|
1873
|
+
return s.startsWith("user:") ? s.slice("user:".length) : s;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
function activeRelationSubject(row: unknown, subject: string): boolean {
|
|
1877
|
+
let leaf = ((row as { subjects?: Record<string, unknown> } | undefined)?.subjects ?? {})[subject];
|
|
1878
|
+
if (typeof leaf === "string") {
|
|
1879
|
+
try { leaf = JSON.parse(leaf) as unknown; } catch { return false; }
|
|
1880
|
+
}
|
|
1881
|
+
return !!leaf && (leaf as { status?: string }).status === "active";
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
function creationPostureVerdict(
|
|
1885
|
+
snapshots: Record<string, Record<string, unknown>>,
|
|
1886
|
+
ctx?: { intent: Record<string, unknown>; principal?: string },
|
|
1887
|
+
): WorkspaceInvariantVerdict {
|
|
1888
|
+
const actor = String(ctx?.principal ?? "").trim();
|
|
1889
|
+
const claimed = canonicalBirthIdentity(ctx?.intent?.principal);
|
|
1890
|
+
const requestedBy = canonicalBirthIdentity(ctx?.intent?.requestedBy);
|
|
1891
|
+
const proven = canonicalBirthIdentity(actor);
|
|
1892
|
+
if (actor === "" || claimed === "" || requestedBy === "" || claimed !== proven || requestedBy !== proven) {
|
|
1893
|
+
return { reject: "creation.identity-mismatch" };
|
|
1894
|
+
}
|
|
1895
|
+
const posture = String((snapshots.policy as { creationPosture?: string } | undefined)?.creationPosture ?? "open");
|
|
1896
|
+
if (posture === "open" || posture === "") return { accept: true };
|
|
1897
|
+
if (posture === "invite") {
|
|
1898
|
+
return activeRelationSubject(snapshots.creators, actor)
|
|
1899
|
+
? { accept: true }
|
|
1900
|
+
: { reject: "creation.not-invited" };
|
|
1901
|
+
}
|
|
1902
|
+
if (posture === "closed") {
|
|
1903
|
+
return activeRelationSubject(snapshots.admins, actor)
|
|
1904
|
+
? { accept: true }
|
|
1905
|
+
: { reject: "creation.closed" };
|
|
1906
|
+
}
|
|
1907
|
+
return { reject: "creation.invalid-posture" };
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
const creationPostureReads = () => [
|
|
1911
|
+
refAs("policy", "CloudPolicy", CREATION_POLICY_ID),
|
|
1912
|
+
refAs("creators", "RelationTuple", CREATOR_TUPLE_ID),
|
|
1913
|
+
refAs("admins", "RelationTuple", ADMIN_TUPLE_ID),
|
|
1914
|
+
];
|
|
1915
|
+
|
|
1916
|
+
/** Compatibility create: open/unset ⇒ proven author; invite ⇒ creator; closed ⇒ admin. */
|
|
1917
|
+
export const creationPostureCreate = workspaceInvariant("CreationPostureCreate")
|
|
1918
|
+
.on("createWorkspace")
|
|
1919
|
+
.reads(creationPostureReads)
|
|
1920
|
+
.assert(creationPostureVerdict);
|
|
1921
|
+
|
|
1922
|
+
/** Unified child birth: exactly the same posture and identity law as compatibility create. */
|
|
1923
|
+
export const creationPostureBirth = workspaceInvariant("CreationPostureBirth")
|
|
1924
|
+
.on("birthChild")
|
|
1925
|
+
.reads(creationPostureReads)
|
|
1926
|
+
.assert(creationPostureVerdict);
|
|
1927
|
+
|
|
1862
1928
|
// The CloudPolicy/QuotaGrant law-state used to be enforced by NOBODY — `createWorkspace`/`birthChild` were
|
|
1863
1929
|
// gated only by `.requires("creator")`, so a single #creator could mint unbounded workspaces. NOW the
|
|
1864
1930
|
// COUNT quota is REAL law at the gate: each birth directive CAPTURES (via the #58 captured-read lane,
|