@githolon/dsl 0.81.0 → 0.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dart/CHANGELOG.md +10 -0
- package/dart/lib/nomos_client.dart +1 -0
- package/dart/lib/src/webview_bridge.dart +27 -0
- package/dart/lib/src/workspace_name.dart +24 -0
- package/dart/pubspec.yaml +1 -1
- package/dart/test/workspace_name_test.dart +24 -0
- package/package.json +1 -1
- package/src/births.ts +81 -29
- package/src/index.ts +10 -1
package/dart/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## 0.55.1
|
|
2
|
+
|
|
3
|
+
- Add `deriveWorkspaceName(namespace, key)` — re-derive the workspace name a keyed birth
|
|
4
|
+
(`birth.keyed`) produced, so a born workspace is addressable from its key with zero persistence
|
|
5
|
+
(byte-identical to @githolon/dsl + @githolon/client; contract-tested vectors). Replaces hand-rolled
|
|
6
|
+
`<x>WorkspaceFor<Id>` helpers.
|
|
7
|
+
- Expose `bridge.issueDelegationCert({signerSecret, grantorId, relation, subject, epoch, certChain})` —
|
|
8
|
+
issue a role-delegation cert (the `.delegatedRole()` lane) from the public client; deliver via
|
|
9
|
+
`acceptDelegatedRole`. Runner bundle rebuilt.
|
|
10
|
+
|
|
1
11
|
## 0.55.0
|
|
2
12
|
|
|
3
13
|
- **Security: enrollSigner self-binds** (device can only enroll itself; cross-principal forgery closed
|
|
@@ -31,6 +31,7 @@ library;
|
|
|
31
31
|
export 'package:nomos_types/nomos_types.dart';
|
|
32
32
|
// homeNameOf(subject) — the canonical home-workspace name (parity with @githolon/client + the law side).
|
|
33
33
|
export 'src/home.dart';
|
|
34
|
+
export 'src/workspace_name.dart';
|
|
34
35
|
// THE WEBVIEW BRIDGE (app-binding boundary) — the async Dart runtime that drives the REAL githolon running
|
|
35
36
|
// in a WebView, so the generated typed client never needs a `MemoryPersistence` fake.
|
|
36
37
|
export 'src/webview_bridge.dart';
|
|
@@ -1827,6 +1827,33 @@ class NomosBridge {
|
|
|
1827
1827
|
Map<String, Object?> args) async =>
|
|
1828
1828
|
_asMap(await _call('attestationSign', {'args': args}));
|
|
1829
1829
|
|
|
1830
|
+
/// Issue a ROLE-DELEGATION cert (the `.delegatedRole()` lane, kernel `delegationCertSign` op). A platform
|
|
1831
|
+
/// signs — with its grantor secret `signerSecret` (base64) — a cert granting `relation` to `subject`,
|
|
1832
|
+
/// carrying the lineage `certChain` up to K_root. Returns `{ certString }`; deliver it into the target
|
|
1833
|
+
/// home via the tenant's `acceptDelegatedRole` directive (the one gate re-verifies it to K_root on every
|
|
1834
|
+
/// lane). OURS = the op; operating the grantor secret + choosing who to grant is the TENANT.
|
|
1835
|
+
/// final res = await bridge.issueDelegationCert(signerSecret: sP, grantorId: 'co2-platform',
|
|
1836
|
+
/// relation: 'co2EstateCreator', subject: userSubject, epoch: 1, certChain: [rootLink]);
|
|
1837
|
+
/// final certString = res['certString'] as String;
|
|
1838
|
+
Future<Map<String, Object?>> issueDelegationCert({
|
|
1839
|
+
required String signerSecret,
|
|
1840
|
+
required String grantorId,
|
|
1841
|
+
required String relation,
|
|
1842
|
+
required String subject,
|
|
1843
|
+
int epoch = 1,
|
|
1844
|
+
List<Object?>? certChain,
|
|
1845
|
+
}) async =>
|
|
1846
|
+
_asMap(await _call('issueDelegationCert', {
|
|
1847
|
+
'args': {
|
|
1848
|
+
'signerSecret': signerSecret,
|
|
1849
|
+
'grantorId': grantorId,
|
|
1850
|
+
'relation': relation,
|
|
1851
|
+
'subject': subject,
|
|
1852
|
+
'epoch': epoch,
|
|
1853
|
+
if (certChain != null) 'certChain': certChain,
|
|
1854
|
+
},
|
|
1855
|
+
}));
|
|
1856
|
+
|
|
1830
1857
|
/// A declared query's rows from the local projection. `keys` (E2E sealing) injects the reader's held
|
|
1831
1858
|
/// scope keys so encrypted fields AEAD-open in-wasm; absent ⇒ they stay opaque envelopes (today's
|
|
1832
1859
|
/// behavior). `principal` stamps the private-read gate (defaults to the connection's bound principal).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import 'dart:convert';
|
|
2
|
+
|
|
3
|
+
import 'package:crypto/crypto.dart';
|
|
4
|
+
|
|
5
|
+
/// The unit separator (0x1F) that disambiguates `(namespace, key)` in the name hash input.
|
|
6
|
+
final String workspaceNameSep = String.fromCharCode(31);
|
|
7
|
+
|
|
8
|
+
/// Re-derive the workspace name a keyed birth produced, so you can ADDRESS a born workspace from a key you
|
|
9
|
+
/// own — with zero persistence, on any device, any time.
|
|
10
|
+
///
|
|
11
|
+
/// A workspace exists to be used, and using it means finding it again. When a law births a child with
|
|
12
|
+
/// `birth.keyed({ key, namespace })` (the `@githolon/dsl` verb), the workspace name is
|
|
13
|
+
/// `<namespace>-<sha256(namespace + SEP + key)[:40]>`. This function computes the IDENTICAL name — it is the
|
|
14
|
+
/// byte-for-byte mirror of `@githolon/dsl`'s `workspaceNameForKey` and `@githolon/client`'s
|
|
15
|
+
/// `deriveWorkspaceName`, contract-tested against shared vectors. So instead of persisting a minted name or
|
|
16
|
+
/// hand-rolling an `estateWorkspaceForEstateId` helper, you write:
|
|
17
|
+
///
|
|
18
|
+
/// ```dart
|
|
19
|
+
/// final ws = deriveWorkspaceName('estate', estateId); // open / sync / mount it
|
|
20
|
+
/// ```
|
|
21
|
+
String deriveWorkspaceName(String namespace, String key) {
|
|
22
|
+
final digest = sha256.convert(utf8.encode('$namespace$workspaceNameSep$key')).toString();
|
|
23
|
+
return '$namespace-${digest.substring(0, 40)}';
|
|
24
|
+
}
|
package/dart/pubspec.yaml
CHANGED
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
|
|
12
12
|
code-generated per-domain aggregate + directive-payload types. (De)serializes
|
|
13
13
|
the EXACT serde JSON the Rust kernel + projection produce/consume.
|
|
14
|
-
version: 0.55.
|
|
14
|
+
version: 0.55.1
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import 'package:nomos_client/src/workspace_name.dart';
|
|
2
|
+
import 'package:test/test.dart';
|
|
3
|
+
|
|
4
|
+
// CONTRACT VECTORS — shared byte-for-byte with @githolon/dsl `workspaceNameForKey` and @githolon/client
|
|
5
|
+
// `deriveWorkspaceName`. Generated with real sha256 (node crypto). If any of the three derivations drifts,
|
|
6
|
+
// a workspace born by one peer becomes unaddressable by another — so these must never change silently.
|
|
7
|
+
void main() {
|
|
8
|
+
group('deriveWorkspaceName — the shared birth-name derivation', () {
|
|
9
|
+
test('matches the cross-language reference vectors', () {
|
|
10
|
+
expect(deriveWorkspaceName('estate', 'estate-abc123'),
|
|
11
|
+
'estate-83662e729ba18cd04bde52c7a8d854594833d70e');
|
|
12
|
+
expect(deriveWorkspaceName('home', 'user:ada'),
|
|
13
|
+
'home-561af44c94b5b80824f1a9e8cfa1095105f3cb34');
|
|
14
|
+
expect(deriveWorkspaceName('catalogue', 'cat-42'),
|
|
15
|
+
'catalogue-f50bf3b9732dc36961b05b8e6503d96df5f3e88e');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('is deterministic and namespaced (no cross-namespace collision)', () {
|
|
19
|
+
expect(deriveWorkspaceName('estate', 'z'), deriveWorkspaceName('estate', 'z'));
|
|
20
|
+
expect(deriveWorkspaceName('estate', 'z'), isNot(deriveWorkspaceName('catalogue', 'z')));
|
|
21
|
+
expect(deriveWorkspaceName('estate', 'z').startsWith('estate-'), isTrue);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.83.0",
|
|
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
|
@@ -80,14 +80,35 @@ export function birth(recipe: BirthRecipe): void {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
// `nomos.sha256` — the trustless pure hash bound in the sealed engine (like `nomos.mint`), used to derive a
|
|
83
|
-
//
|
|
83
|
+
// keyed child's DETERMINISTIC, ADDRESSABLE name in-law so every peer — and every CLIENT — re-derives the
|
|
84
|
+
// same name from the same key.
|
|
84
85
|
declare const nomos: { sha256(input: string): string };
|
|
85
86
|
|
|
87
|
+
/**
|
|
88
|
+
* THE ONE WORKSPACE-NAME DERIVATION — shared verbatim by the law (this engine), the Dart client, and the TS
|
|
89
|
+
* client, so a workspace born under a key is ADDRESSABLE from that key on every peer with zero persistence.
|
|
90
|
+
*
|
|
91
|
+
* name = `<namespace>-<sha256(namespace + "" + key)[:40]>`
|
|
92
|
+
*
|
|
93
|
+
* A workspace exists to be USED, and using it means finding it again — so a birth's name is NEVER opaque or
|
|
94
|
+
* minted-and-forgotten; it is a pure function of a key the caller already owns (a business id, an owner uid).
|
|
95
|
+
* The `` (unit separator) makes `(ns,key)` unambiguous. `nomos.sha256` returns lowercase hex.
|
|
96
|
+
*
|
|
97
|
+
* The client mirror is `deriveWorkspaceName(namespace, key)` in `nomos_client` (Dart) and `@githolon/client`
|
|
98
|
+
* (TS) — byte-identical output is contract-tested against {@link WORKSPACE_NAME_VECTORS}.
|
|
99
|
+
*/
|
|
100
|
+
export const WORKSPACE_NAME_SEP = "\u001f"; // unit separator disambiguates (namespace,key)
|
|
101
|
+
export function workspaceNameForKey(namespace: string, key: string): string {
|
|
102
|
+
return namespace + "-" + nomos.sha256(String(namespace) + WORKSPACE_NAME_SEP + String(key)).slice(0, 40);
|
|
103
|
+
}
|
|
104
|
+
|
|
86
105
|
/** A genesis chain, or a builder that receives the resolved child workspace name (so a seed step can embed
|
|
87
106
|
* it — e.g. `seedEstate` needs to write its own `estateName`). Passing a builder is the common case. */
|
|
88
|
-
|
|
107
|
+
// An array of genesis steps, OR a builder that receives the resolved child name. Typed as `unknown[]`
|
|
108
|
+
// (not `unknown`) so the union doesn't collapse — the builder's `childWorkspace` param infers as `string`.
|
|
109
|
+
export type GenesisChainOrBuilder = readonly unknown[] | ((childWorkspace: string) => readonly unknown[]);
|
|
89
110
|
function resolveChain(child: string, c: GenesisChainOrBuilder): unknown {
|
|
90
|
-
return typeof c === "function" ?
|
|
111
|
+
return typeof c === "function" ? c(child) : c;
|
|
91
112
|
}
|
|
92
113
|
|
|
93
114
|
/** A `create()`d aggregate reference — the birth's audit record, whose MINTED (captured, replay-stable) id
|
|
@@ -97,30 +118,56 @@ interface MintedRef {
|
|
|
97
118
|
}
|
|
98
119
|
|
|
99
120
|
/**
|
|
100
|
-
* THE
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* • `birth.instance(...)` — MANY children per parent. The child's workspace id is MINTED from `from` (an
|
|
107
|
-
* aggregate you `create()` for the birth) — its id already rides the committed intent (captured), so the
|
|
108
|
-
* child name reproduces byte-identically at replay. Nomos mints the identity; you never hand-roll it.
|
|
121
|
+
* `birth.keyed(...)` — THE way to birth a child workspace. A workspace exists to be USED, and using it means
|
|
122
|
+
* FINDING IT AGAIN, so its name is ALWAYS derived from a `key` the caller owns (a business id, an owner uid)
|
|
123
|
+
* via {@link workspaceNameForKey}. The client re-derives the SAME name with `deriveWorkspaceName(namespace,
|
|
124
|
+
* key)` — so the born workspace is addressable from the key alone, with zero persistence. Same key ⇒ same
|
|
125
|
+
* workspace (re-birth is idempotent). This is the only birth verb you need.
|
|
109
126
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
127
|
+
* ```ts
|
|
128
|
+
* export const birthEstate = directive("birthEstate")
|
|
129
|
+
* .creates(EstateBirth)
|
|
130
|
+
* .payload(z.object({ estateId: z.string(), owner: z.string(), frameworkHash: z.string(), lawHash: z.string(), bornAt: z.string() }))
|
|
131
|
+
* .plan((p) => {
|
|
132
|
+
* birth.keyed({ key: p.estateId, namespace: "estate", genesisChain: (ws) => [
|
|
133
|
+
* { domain: "bootstrap", directiveId: "installDomain", actor: p.owner, payload: { domainHash: p.frameworkHash }, domainHash: p.frameworkHash, domainPackageB64: "" },
|
|
134
|
+
* { domain: "nomos", directiveId: "installDomain", actor: p.owner, payload: { domainHash: p.lawHash }, domainHash: p.lawHash, domainPackageB64: "" },
|
|
135
|
+
* { domain: "estate", directiveId: "seedEstate", actor: p.owner, payload: { estateId: p.estateId, workspaceName: ws, bornAt: p.bornAt } },
|
|
136
|
+
* ] });
|
|
137
|
+
* return [];
|
|
138
|
+
* })
|
|
139
|
+
* .births().requires("estateCreator");
|
|
140
|
+
* // app (Dart / TS): final ws = client.deriveWorkspaceName('estate', estateId); // open it, any time, any device
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export const birthKeyed = ({
|
|
144
|
+
key,
|
|
145
|
+
namespace,
|
|
146
|
+
genesisChain,
|
|
147
|
+
}: {
|
|
148
|
+
/** The id you already own that this workspace is FOR (an estateId, an owner uid). The client addresses the
|
|
149
|
+
* born workspace by re-deriving its name from this same key — so pick a key the client will have in hand. */
|
|
150
|
+
key: string;
|
|
151
|
+
/** The workspace-name namespace (e.g. `"estate"` → `estate-<hash>`). Groups a domain's children. */
|
|
152
|
+
namespace: string;
|
|
153
|
+
genesisChain: GenesisChainOrBuilder;
|
|
154
|
+
}): string => {
|
|
155
|
+
const child = workspaceNameForKey(namespace, String(key));
|
|
156
|
+
footprint.push({ workspace: child, genesisChain: resolveChain(child, genesisChain) });
|
|
157
|
+
return child;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @deprecated Use {@link birthKeyed}. `birth.instance` mints a name the caller THROWS AWAY — so the born
|
|
162
|
+
* workspace can never be found again except via the birth result. A workspace is a custodial thing you birth
|
|
163
|
+
* in order to use; there is no workspace you make and never address later. Birth by a key you own instead.
|
|
115
164
|
*/
|
|
116
165
|
export const birthInstance = ({
|
|
117
166
|
from,
|
|
118
167
|
prefix = "",
|
|
119
168
|
genesisChain,
|
|
120
169
|
}: {
|
|
121
|
-
/** The `create()`d audit record whose minted, captured id names the child (replay-stable uniqueness). */
|
|
122
170
|
from: MintedRef;
|
|
123
|
-
/** Optional human-legible workspace-name prefix (e.g. `"estate-"`). */
|
|
124
171
|
prefix?: string;
|
|
125
172
|
genesisChain: GenesisChainOrBuilder;
|
|
126
173
|
}): string => {
|
|
@@ -129,29 +176,34 @@ export const birthInstance = ({
|
|
|
129
176
|
return child;
|
|
130
177
|
};
|
|
131
178
|
|
|
179
|
+
/**
|
|
180
|
+
* `birth.singleton(...)` — a keyed birth whose key is a NATURAL single key (one home per user: key = owner).
|
|
181
|
+
* Sugar over {@link birthKeyed}: `birth.singleton({ keyedBy: owner, namespace: "home" })`. Same addressing
|
|
182
|
+
* story — the client re-derives with `deriveWorkspaceName(namespace, keyedBy)`.
|
|
183
|
+
*/
|
|
132
184
|
export const birthSingleton = ({
|
|
133
185
|
keyedBy,
|
|
134
186
|
namespace,
|
|
135
187
|
genesisChain,
|
|
136
188
|
}: {
|
|
137
|
-
/** The
|
|
189
|
+
/** The natural single key (e.g. the owner). One workspace per key; re-birth idempotent. */
|
|
138
190
|
keyedBy: string | readonly string[];
|
|
139
|
-
/**
|
|
140
|
-
namespace
|
|
191
|
+
/** The workspace-name namespace (e.g. `"home"`). */
|
|
192
|
+
namespace: string;
|
|
141
193
|
genesisChain: GenesisChainOrBuilder;
|
|
142
194
|
}): string => {
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
footprint.push({ workspace: child, genesisChain: resolveChain(child, genesisChain) });
|
|
146
|
-
return child;
|
|
195
|
+
const key = (Array.isArray(keyedBy) ? keyedBy : [keyedBy]).map(String).join(WORKSPACE_NAME_SEP);
|
|
196
|
+
return birthKeyed({ key, namespace, genesisChain });
|
|
147
197
|
};
|
|
148
198
|
|
|
149
|
-
// Function/namespace declaration merging: attach the
|
|
150
|
-
// `birth.
|
|
199
|
+
// Function/namespace declaration merging: attach the birth verbs to `birth` so the call site reads
|
|
200
|
+
// `birth.keyed(...)` — with full types AND the runtime assignment in one place.
|
|
151
201
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
152
202
|
export namespace birth {
|
|
153
|
-
export const
|
|
203
|
+
export const keyed = birthKeyed;
|
|
154
204
|
export const singleton = birthSingleton;
|
|
205
|
+
/** @deprecated Use `birth.keyed` — see {@link birthInstance}. */
|
|
206
|
+
export const instance = birthInstance;
|
|
155
207
|
}
|
|
156
208
|
|
|
157
209
|
/**
|
package/src/index.ts
CHANGED
|
@@ -198,7 +198,16 @@ export {
|
|
|
198
198
|
// child's genesis recipe via `birth(recipe)` (reuse `buildGenesisChain`); it rides the committed intent as a
|
|
199
199
|
// declared output, and the kernel — reading the `.births` declaration off the law manifest (the
|
|
200
200
|
// `nomosBirthGate` era key, never domain state) — spawns + self-validates the child after the offer admits.
|
|
201
|
-
export {
|
|
201
|
+
export {
|
|
202
|
+
birth,
|
|
203
|
+
workspaceNameForKey,
|
|
204
|
+
WORKSPACE_NAME_SEP,
|
|
205
|
+
birthKeyed,
|
|
206
|
+
birthSingleton,
|
|
207
|
+
birthInstance,
|
|
208
|
+
type BirthRecipe,
|
|
209
|
+
type GenesisChainOrBuilder,
|
|
210
|
+
} from "./births.js";
|
|
202
211
|
// THE PEER-SOVEREIGN ROOT-OF-TRUST (the BirthAttestation cert chain): a home/child law exports
|
|
203
212
|
// `requireParentAttestedBirth` to declare it requires a parent-attested-birth cert chain (the
|
|
204
213
|
// `nomosParentAttestedBirth` era key); the kernel verify doors then verify the travelling chain to K_root.
|