@serfab/cadre-core 0.8.1 → 0.9.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/README.md +2 -2
- package/dist/cadre-node.d.ts +101 -61
- package/dist/cadre-node.js +165 -124
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-cohort.d.ts +15 -15
- package/dist/control-cohort.js +14 -14
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +51 -34
- package/dist/control-database.js +72 -56
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +2 -2
- package/dist/control-formation-recorder.js +2 -2
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +57 -33
- package/dist/control-schema.js.map +1 -1
- package/dist/device-token.d.ts +1 -1
- package/dist/device-token.js +1 -1
- package/dist/{authority-key.d.ts → ed25519-key.d.ts} +11 -10
- package/dist/{authority-key.js → ed25519-key.js} +9 -10
- package/dist/ed25519-key.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +8 -8
- package/dist/index.js.map +1 -1
- package/dist/key-store.d.ts +1 -1
- package/dist/key-store.js +1 -1
- package/dist/peer-authorization.d.ts +28 -6
- package/dist/peer-authorization.js +33 -7
- package/dist/peer-authorization.js.map +1 -1
- package/dist/peer-record.d.ts +3 -3
- package/dist/peer-record.js +3 -3
- package/dist/push-node.d.ts +38 -0
- package/dist/push-node.js +48 -0
- package/dist/push-node.js.map +1 -0
- package/dist/push-notifier-apns.d.ts +10 -2
- package/dist/push-notifier-apns.js +14 -10
- package/dist/push-notifier-apns.js.map +1 -1
- package/dist/push-notifier-fcm.d.ts +7 -1
- package/dist/push-notifier-fcm.js +9 -10
- package/dist/push-notifier-fcm.js.map +1 -1
- package/dist/push-notifier.d.ts +16 -35
- package/dist/push-notifier.js +16 -44
- package/dist/push-notifier.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +70 -51
- package/dist/seed-bootstrap.js +168 -106
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.d.ts +11 -11
- package/dist/seed-trust-policy.js +11 -11
- package/dist/seed-trust-policy.js.map +1 -1
- package/dist/strand-database.d.ts +5 -5
- package/dist/strand-database.js +4 -4
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +1 -1
- package/dist/strand-formation-protocol.js +1 -1
- package/dist/strand-instance-manager.d.ts +1 -1
- package/dist/strand-instance-manager.js +1 -1
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-member-key.d.ts +6 -6
- package/dist/strand-member-key.js +6 -6
- package/dist/strand-member-key.js.map +1 -1
- package/dist/strand-member-registry.d.ts +7 -7
- package/dist/strand-member-registry.js +5 -5
- package/dist/strand-member-registry.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +84 -84
- package/dist/strand-membership-writer.js +90 -90
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/types.d.ts +52 -32
- package/dist/types.js.map +1 -1
- package/package.json +7 -3
- package/src/cadre-node.ts +170 -126
- package/src/control-cohort.ts +17 -17
- package/src/control-database.ts +76 -59
- package/src/control-formation-recorder.ts +2 -2
- package/src/control-schema.ts +57 -33
- package/src/device-token.ts +1 -1
- package/src/{authority-key.ts → ed25519-key.ts} +12 -11
- package/src/index.ts +21 -19
- package/src/key-store.ts +1 -1
- package/src/peer-authorization.ts +35 -7
- package/src/peer-record.ts +3 -3
- package/src/push-node.ts +64 -0
- package/src/push-notifier-apns.ts +16 -12
- package/src/push-notifier-fcm.ts +9 -10
- package/src/push-notifier.ts +16 -57
- package/src/seed-bootstrap.ts +179 -115
- package/src/seed-trust-policy.ts +18 -18
- package/src/strand-database.ts +6 -6
- package/src/strand-formation-protocol.ts +1 -1
- package/src/strand-instance-manager.ts +2 -2
- package/src/strand-member-key.ts +7 -7
- package/src/strand-member-registry.ts +10 -10
- package/src/strand-membership-writer.ts +127 -127
- package/src/types.ts +52 -32
- package/dist/authority-key.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Database } from '@quereus/quereus';
|
|
2
2
|
import type { SAppConfig } from './types.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { Ed25519KeyPair } from './ed25519-key.js';
|
|
4
4
|
/**
|
|
5
5
|
* The engine (rules-logic system) assumed to manage every strand. Persisted in
|
|
6
6
|
* `Strand.Header.Engine`.
|
|
@@ -23,7 +23,7 @@ export declare const STRAND_ENGINE_VERSION = "0.1.0";
|
|
|
23
23
|
*
|
|
24
24
|
* `schemas/strand.qsql` signs a single SHA-256 digest over a `'|'`-joined payload
|
|
25
25
|
* (e.g. `Member.Authorized` verifies
|
|
26
|
-
* `verify(digest(new.Key), context.
|
|
26
|
+
* `verify(digest(new.Key), context.ManagerSignature, A.MemberKey, 'ed25519')`).
|
|
27
27
|
* So the signer hashes the payload to raw bytes and ed25519-signs *those bytes*:
|
|
28
28
|
* SQL `digest(...)`'s default output is base64url and `verify(...)`'s default
|
|
29
29
|
* `inputEncoding` is base64url, so signer and verifier operate on identical bytes.
|
|
@@ -61,16 +61,16 @@ export declare function verifyStrandPayload(payload: string, signatureB64: strin
|
|
|
61
61
|
export interface FounderBootstrapParams {
|
|
62
62
|
/** The strand id — written to `Header.Id`. */
|
|
63
63
|
strandId: string;
|
|
64
|
-
/** Strand type: `'o'` (open, Header only) or `'c'` (closed, Header+Member+
|
|
64
|
+
/** Strand type: `'o'` (open, Header only) or `'c'` (closed, Header+Member+Manager). */
|
|
65
65
|
type: 'o' | 'c';
|
|
66
66
|
/** The sApp config whose id/version/schema/signature populate the Header. */
|
|
67
67
|
sApp: SAppConfig;
|
|
68
68
|
/**
|
|
69
69
|
* The founder's derived strand keypair (from {@link strandMemberKeyPair}). Its
|
|
70
|
-
* `publicKeyB64` becomes the founding `Member.Key` and `
|
|
70
|
+
* `publicKeyB64` becomes the founding `Member.Key` and `Manager.MemberKey`.
|
|
71
71
|
* Required for a closed strand; ignored for an open strand.
|
|
72
72
|
*/
|
|
73
|
-
founderKeyPair?:
|
|
73
|
+
founderKeyPair?: Ed25519KeyPair;
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
76
|
* Founder-only one-time bootstrap of a strand's `Strand.*` membership/RBAC rows.
|
|
@@ -81,16 +81,16 @@ export interface FounderBootstrapParams {
|
|
|
81
81
|
* restart / founder re-`addStrand` is a no-op and never double-inserts.
|
|
82
82
|
*
|
|
83
83
|
* Behavior by strand type:
|
|
84
|
-
* - **Open (`'o'`)**: insert `Header` only. `Member`/`
|
|
84
|
+
* - **Open (`'o'`)**: insert `Header` only. `Member`/`Manager`/`Invite` are
|
|
85
85
|
* `OnlyClosed` and would trip that constraint — they are skipped entirely.
|
|
86
86
|
* - **Closed (`'c'`)**: insert `Header(Type='c')`, then the founding `Member`
|
|
87
|
-
* (`Key = founderKeyPair.publicKeyB64`), then the founding `
|
|
87
|
+
* (`Key = founderKeyPair.publicKeyB64`), then the founding `Manager`
|
|
88
88
|
* (`MemberKey = founderKeyPair.publicKeyB64`). Insert order matters: the
|
|
89
|
-
* Header must commit before the deferred `OnlyClosed` checks on Member/
|
|
89
|
+
* Header must commit before the deferred `OnlyClosed` checks on Member/Manager
|
|
90
90
|
* evaluate at commit.
|
|
91
91
|
*
|
|
92
92
|
* A closed strand with no `founderKeyPair` throws: a closed strand with no founding
|
|
93
|
-
*
|
|
93
|
+
* Manager could never admit anyone, so failing loudly here (which propagates out
|
|
94
94
|
* of `StrandDatabase.initialize()` and triggers the runtime's rollback) is correct.
|
|
95
95
|
*
|
|
96
96
|
* @param db - The strand's Quereus database (schema already applied).
|
|
@@ -102,10 +102,10 @@ export declare function bootstrapFounderMembership(db: Database, params: Founder
|
|
|
102
102
|
/** Parameters for {@link issueInvite}. */
|
|
103
103
|
export interface IssueInviteParams {
|
|
104
104
|
/**
|
|
105
|
-
* The issuing
|
|
106
|
-
* `Strand.
|
|
105
|
+
* The issuing manager's strand keypair. Its `publicKeyB64` must already be a
|
|
106
|
+
* `Strand.Manager` row (the `InviteValid` constraint rejects a non-manager).
|
|
107
107
|
*/
|
|
108
|
-
|
|
108
|
+
managerKeyPair: Ed25519KeyPair;
|
|
109
109
|
/**
|
|
110
110
|
* Optional invite expiry as epoch milliseconds. When set, it is canonicalised
|
|
111
111
|
* via {@link canonicalDatetime} so the signed payload segment byte-matches the
|
|
@@ -131,10 +131,10 @@ export interface IssuedInvite {
|
|
|
131
131
|
*
|
|
132
132
|
* Generates a fresh invite ed25519 keypair (the public key becomes `Invite.Key`),
|
|
133
133
|
* builds the constraint's payload (`Key || '|' || coalesce(Expiration, '')`), and
|
|
134
|
-
* signs it TWICE: with the
|
|
135
|
-
*
|
|
134
|
+
* signs it TWICE: with the manager private key (→ `ManagerSignature`, proving
|
|
135
|
+
* a manager issued it) and with the invite private key (→ `InviteSignature`,
|
|
136
136
|
* proving the issuer actually holds the invite secret). Both signatures plus the
|
|
137
|
-
*
|
|
137
|
+
* manager public key are bound as constraint context for the `Invite` insert.
|
|
138
138
|
*
|
|
139
139
|
* The returned `invitePrivateKey` is the only secret the invitee needs to redeem
|
|
140
140
|
* the invite — it is NOT stored in the strand (only `Invite.Key`, the public half,
|
|
@@ -142,7 +142,7 @@ export interface IssuedInvite {
|
|
|
142
142
|
* is `InviteKey`, so a given invite can be consumed at most once.
|
|
143
143
|
*
|
|
144
144
|
* @param db - The closed strand's database (founder already bootstrapped).
|
|
145
|
-
* @param params - The issuing
|
|
145
|
+
* @param params - The issuing manager keypair and optional expiry.
|
|
146
146
|
* @returns The invite public key and the out-of-band private seed.
|
|
147
147
|
*/
|
|
148
148
|
export declare function issueInvite(db: Database, params: IssueInviteParams): Promise<IssuedInvite>;
|
|
@@ -204,44 +204,44 @@ export interface ConsumeInviteParams {
|
|
|
204
204
|
* `ConsumedInvite` row survives).
|
|
205
205
|
*/
|
|
206
206
|
export declare function consumeInvite(db: Database, params: ConsumeInviteParams): Promise<void>;
|
|
207
|
-
/** Parameters for {@link
|
|
208
|
-
export interface
|
|
207
|
+
/** Parameters for {@link addMemberByManager}. */
|
|
208
|
+
export interface AddMemberByManagerParams {
|
|
209
209
|
/**
|
|
210
|
-
* The admitting
|
|
211
|
-
* `Strand.
|
|
210
|
+
* The admitting manager's strand keypair. Its `publicKeyB64` must be a
|
|
211
|
+
* `Strand.Manager` row; it signs the new member key directly.
|
|
212
212
|
*/
|
|
213
|
-
|
|
213
|
+
managerKeyPair: Ed25519KeyPair;
|
|
214
214
|
/** The joining member's ed25519 PUBLIC key (base64url) — the new `Member.Key`. */
|
|
215
215
|
memberKey: string;
|
|
216
216
|
}
|
|
217
217
|
/**
|
|
218
|
-
* Admit a `Member` directly by
|
|
219
|
-
* path on `Member.Authorized`'s direct-
|
|
218
|
+
* Admit a `Member` directly by manager signature — the sibling of the invite
|
|
219
|
+
* path on `Member.Authorized`'s direct-manager branch.
|
|
220
220
|
*
|
|
221
|
-
* The constraint verifies `digest(new.Key)` against
|
|
222
|
-
* `context.
|
|
223
|
-
* No `ConsumedInvite` is involved: this is the path
|
|
224
|
-
* member it already trusts (e.g.
|
|
221
|
+
* The constraint verifies `digest(new.Key)` against a `Manager` row matching
|
|
222
|
+
* `context.ManagerKey`, so the payload is just the member key (no `'|'` join).
|
|
223
|
+
* No `ConsumedInvite` is involved: this is the path a manager uses to seat a
|
|
224
|
+
* member it already trusts (e.g. a manager-side enrollment that admits a
|
|
225
225
|
* party already authorised out-of-band).
|
|
226
226
|
*
|
|
227
227
|
* @param db - The closed strand's database.
|
|
228
|
-
* @param params - The admitting
|
|
229
|
-
* @throws If `Member.Authorized` rejects (e.g. a non-
|
|
228
|
+
* @param params - The admitting manager keypair and the new member key.
|
|
229
|
+
* @throws If `Member.Authorized` rejects (e.g. a non-manager key); the insert
|
|
230
230
|
* rolls back, leaving no `Member` row.
|
|
231
231
|
*/
|
|
232
|
-
export declare function
|
|
232
|
+
export declare function addMemberByManager(db: Database, params: AddMemberByManagerParams): Promise<void>;
|
|
233
233
|
/** Parameters for {@link registerMemberPeer}. */
|
|
234
234
|
export interface RegisterMemberPeerParams {
|
|
235
235
|
/**
|
|
236
236
|
* The member's OWN strand keypair (`{ privateKeyB64, publicKeyB64 }`). Its
|
|
237
237
|
* `publicKeyB64` becomes `MemberPeer.MemberKey` and it self-signs the binding —
|
|
238
238
|
* `MemberPeer.Authorized` verifies the signature against `MemberKey` itself, so a
|
|
239
|
-
* peer can only be registered by the very member it belongs to (no
|
|
239
|
+
* peer can only be registered by the very member it belongs to (no manager
|
|
240
240
|
* involved). The founder passes its `strandMemberKeyPair`; an invited member
|
|
241
|
-
* passes its own keypair. Typed as {@link
|
|
242
|
-
* base64url keypair shape — no
|
|
241
|
+
* passes its own keypair. Typed as {@link Ed25519KeyPair} only for the shared
|
|
242
|
+
* base64url keypair shape — no manager privilege is implied.
|
|
243
243
|
*/
|
|
244
|
-
memberKeyPair:
|
|
244
|
+
memberKeyPair: Ed25519KeyPair;
|
|
245
245
|
/** The peer/node id (libp2p peer id string) to associate with the member. */
|
|
246
246
|
peerId: string;
|
|
247
247
|
}
|
|
@@ -272,90 +272,90 @@ export interface RegisterMemberPeerParams {
|
|
|
272
272
|
* matching `Member` row); the insert rolls back, leaving no `MemberPeer` row.
|
|
273
273
|
*/
|
|
274
274
|
export declare function registerMemberPeer(db: Database, params: RegisterMemberPeerParams): Promise<void>;
|
|
275
|
-
/** Parameters for {@link
|
|
276
|
-
export interface
|
|
275
|
+
/** Parameters for {@link addManager}. */
|
|
276
|
+
export interface AddManagerParams {
|
|
277
277
|
/**
|
|
278
|
-
* An EXISTING
|
|
279
|
-
* `Strand.
|
|
280
|
-
* rejects a non-
|
|
281
|
-
*
|
|
278
|
+
* An EXISTING manager's strand keypair. Its `publicKeyB64` must already be a
|
|
279
|
+
* `Strand.Manager` row (the existing-manager branch of `Manager.Authorized`
|
|
280
|
+
* rejects a non-manager once the founder manager exists); it signs the new
|
|
281
|
+
* manager key and is bound as `context.ManagerKey`.
|
|
282
282
|
*/
|
|
283
|
-
|
|
284
|
-
/** The member key to promote — the new `
|
|
285
|
-
|
|
283
|
+
byManagerKeyPair: Ed25519KeyPair;
|
|
284
|
+
/** The member key to promote — the new `Manager.MemberKey` row. */
|
|
285
|
+
newManagerKey: string;
|
|
286
286
|
}
|
|
287
287
|
/**
|
|
288
|
-
* Promote a member to `
|
|
288
|
+
* Promote a member to `Manager` on the signature of an existing manager.
|
|
289
289
|
*
|
|
290
|
-
* The existing-
|
|
290
|
+
* The existing-manager branch of `Manager.Authorized` verifies
|
|
291
291
|
* `digest(coalesce(new.MemberKey, old.MemberKey))` — `new.MemberKey` =
|
|
292
|
-
* `
|
|
293
|
-
* `context.
|
|
294
|
-
* `'|'` join) and binds itself as `context.
|
|
292
|
+
* `newManagerKey` on insert — against a `Manager` row matching
|
|
293
|
+
* `context.ManagerKey`. So the signer signs the new manager key directly (no
|
|
294
|
+
* `'|'` join) and binds itself as `context.ManagerKey`.
|
|
295
295
|
*
|
|
296
|
-
* Once the founder
|
|
296
|
+
* Once the founder manager exists, the schema's `(select count(1) from Manager)
|
|
297
297
|
* <= 1` bootstrap shortcut no longer applies to a second add (at commit the count
|
|
298
298
|
* includes the new row, so it is ≥ 2), so this genuinely exercises signature
|
|
299
|
-
* verification — a non-
|
|
299
|
+
* verification — a non-manager signer, or a signature over the wrong key, is
|
|
300
300
|
* rejected.
|
|
301
301
|
*
|
|
302
|
-
* @param db - The closed strand's database (founder
|
|
303
|
-
* @param params - The authorizing
|
|
304
|
-
* @throws If `
|
|
302
|
+
* @param db - The closed strand's database (founder manager already seated).
|
|
303
|
+
* @param params - The authorizing manager's keypair and the new manager key.
|
|
304
|
+
* @throws If `Manager.Authorized` rejects; the insert rolls back.
|
|
305
305
|
*/
|
|
306
|
-
export declare function
|
|
307
|
-
/** Parameters for {@link
|
|
308
|
-
export interface
|
|
306
|
+
export declare function addManager(db: Database, params: AddManagerParams): Promise<void>;
|
|
307
|
+
/** Parameters for {@link removeManager}. */
|
|
308
|
+
export interface RemoveManagerParams {
|
|
309
309
|
/**
|
|
310
|
-
* The keypair authorizing the removal, bound as `context.
|
|
311
|
-
* signing `digest(
|
|
312
|
-
* existing
|
|
310
|
+
* The keypair authorizing the removal, bound as `context.ManagerKey` and
|
|
311
|
+
* signing `digest(targetManagerKey)`. For an ADMIN removal this is a DIFFERENT
|
|
312
|
+
* existing manager (satisfying the existing-manager branch); for a
|
|
313
313
|
* SELF-resignation it is the target's OWN keypair (`publicKeyB64 ===
|
|
314
|
-
*
|
|
314
|
+
* targetManagerKey`, satisfying the former-manager self branch). The same
|
|
315
315
|
* context construction satisfies whichever branch applies — see
|
|
316
|
-
* {@link
|
|
316
|
+
* {@link removeManager}.
|
|
317
317
|
*/
|
|
318
|
-
|
|
319
|
-
/** The `
|
|
320
|
-
|
|
318
|
+
byManagerKeyPair: Ed25519KeyPair;
|
|
319
|
+
/** The `Manager.MemberKey` row to delete. */
|
|
320
|
+
targetManagerKey: string;
|
|
321
321
|
}
|
|
322
322
|
/**
|
|
323
|
-
* Remove
|
|
324
|
-
*
|
|
323
|
+
* Remove a `Manager` row — either an admin removing a different manager or a
|
|
324
|
+
* manager resigning itself.
|
|
325
325
|
*
|
|
326
326
|
* On a DELETE, `coalesce(new.MemberKey, old.MemberKey)` binds `old.MemberKey =
|
|
327
|
-
*
|
|
328
|
-
* key for BOTH accepting branches of `
|
|
327
|
+
* targetManagerKey` (there is no `new` row), so the signed payload is the target
|
|
328
|
+
* key for BOTH accepting branches of `Manager.Authorized`, and a single context
|
|
329
329
|
* construction serves both:
|
|
330
|
-
* - **Admin removal**: `
|
|
331
|
-
* existing-
|
|
332
|
-
* context.
|
|
333
|
-
* - **Self-resignation**: `
|
|
334
|
-
* equals `
|
|
335
|
-
* `old.MemberKey = context.
|
|
330
|
+
* - **Admin removal**: `byManagerKeyPair` is a different existing manager; the
|
|
331
|
+
* existing-manager branch verifies the signature against `A.MemberKey =
|
|
332
|
+
* context.ManagerKey`.
|
|
333
|
+
* - **Self-resignation**: `byManagerKeyPair` IS the target (its `publicKeyB64`
|
|
334
|
+
* equals `targetManagerKey`); the former-manager self branch verifies
|
|
335
|
+
* `old.MemberKey = context.ManagerKey` and the self-signature over `old.MemberKey`.
|
|
336
336
|
*
|
|
337
337
|
* The caller selects the case purely by which keypair it passes — no branching here.
|
|
338
338
|
*
|
|
339
339
|
* KNOWN PLATFORM GAP (not fixable here): the optimystic bootstrap-mode transactor
|
|
340
340
|
* evaluates deferred (subquery-bearing) CHECK constraints only on INSERT, not on
|
|
341
|
-
* DELETE. `
|
|
342
|
-
* `
|
|
341
|
+
* DELETE. `Manager.Authorized` is deferred, so the platform currently accepts ANY
|
|
342
|
+
* `Manager` delete regardless of signature — `removeManager`'s authorization is
|
|
343
343
|
* effectively unenforced at runtime until that gap is closed (filed as
|
|
344
344
|
* `optimystic-deferred-check-not-enforced-on-delete`). This writer still builds the
|
|
345
345
|
* correct, signed delete so it works unchanged once enforcement lands — exactly as
|
|
346
346
|
* the invite path issues correct inserts despite the open PK-uniqueness gap.
|
|
347
347
|
*
|
|
348
|
-
* KNOWN SCHEMA HAZARD (not guarded here): even with enforcement, `
|
|
349
|
-
* Authorized`'s `(select count(1) from
|
|
348
|
+
* KNOWN SCHEMA HAZARD (not guarded here): even with enforcement, `Manager.
|
|
349
|
+
* Authorized`'s `(select count(1) from Manager) <= 1` bootstrap branch is true at
|
|
350
350
|
* commit whenever a delete drops the count to ≤ 1, so removing the strand's LAST (or
|
|
351
|
-
* second-to-last)
|
|
351
|
+
* second-to-last) manager would be accepted regardless of signature — and removing
|
|
352
352
|
* the last one orphans the strand (no one can ever add another). The schema does not
|
|
353
|
-
* prevent this; a "min-one-
|
|
353
|
+
* prevent this; a "min-one-manager" invariant is deferred to a future schema change
|
|
354
354
|
* rather than grown into this writer.
|
|
355
355
|
*
|
|
356
356
|
* @param db - The closed strand's database.
|
|
357
|
-
* @param params - The authorizing keypair and the target
|
|
358
|
-
* @throws If `
|
|
359
|
-
* while > 2
|
|
357
|
+
* @param params - The authorizing keypair and the target manager key.
|
|
358
|
+
* @throws If `Manager.Authorized` rejects (e.g. a non-manager admin removal
|
|
359
|
+
* while > 2 managers remain); the delete rolls back.
|
|
360
360
|
*/
|
|
361
|
-
export declare function
|
|
361
|
+
export declare function removeManager(db: Database, params: RemoveManagerParams): Promise<void>;
|
|
@@ -24,7 +24,7 @@ export const STRAND_ENGINE_VERSION = '0.1.0';
|
|
|
24
24
|
*
|
|
25
25
|
* `schemas/strand.qsql` signs a single SHA-256 digest over a `'|'`-joined payload
|
|
26
26
|
* (e.g. `Member.Authorized` verifies
|
|
27
|
-
* `verify(digest(new.Key), context.
|
|
27
|
+
* `verify(digest(new.Key), context.ManagerSignature, A.MemberKey, 'ed25519')`).
|
|
28
28
|
* So the signer hashes the payload to raw bytes and ed25519-signs *those bytes*:
|
|
29
29
|
* SQL `digest(...)`'s default output is base64url and `verify(...)`'s default
|
|
30
30
|
* `inputEncoding` is base64url, so signer and verifier operate on identical bytes.
|
|
@@ -109,7 +109,7 @@ async function insertHeaderIfAbsent(db, params) {
|
|
|
109
109
|
* Insert the founding `Strand.Member` if no member exists yet.
|
|
110
110
|
*
|
|
111
111
|
* The empty-table state satisfies the `count(1) from Member <= 1` bootstrap branch
|
|
112
|
-
* of `Member.Authorized`, so no
|
|
112
|
+
* of `Member.Authorized`, so no manager signature is needed — the context fields
|
|
113
113
|
* are explicit nulls. Guarding on the count makes this idempotent: a re-run (the
|
|
114
114
|
* founder re-`addStrand`/`resumeStrand`) finds the member present and skips.
|
|
115
115
|
*/
|
|
@@ -119,28 +119,28 @@ async function insertFounderMemberIfAbsent(db, memberKey, strandId) {
|
|
|
119
119
|
return;
|
|
120
120
|
}
|
|
121
121
|
await db.exec(`insert into Strand.Member (Key)
|
|
122
|
-
with context
|
|
122
|
+
with context ManagerKey = null, ManagerSignature = null
|
|
123
123
|
values (?)`, [memberKey]);
|
|
124
124
|
log('Inserted founding Member for strand %s', strandId);
|
|
125
125
|
}
|
|
126
126
|
/**
|
|
127
|
-
* Insert the founding `Strand.
|
|
127
|
+
* Insert the founding `Strand.Manager` if no manager exists yet.
|
|
128
128
|
*
|
|
129
|
-
* The empty-table state satisfies the `count(1) from
|
|
130
|
-
* branch of `
|
|
131
|
-
* same count guard. Must run AFTER the Header insert: `
|
|
129
|
+
* The empty-table state satisfies the `count(1) from Manager <= 1` bootstrap
|
|
130
|
+
* branch of `Manager.Authorized`, so no signature is needed. Idempotent via the
|
|
131
|
+
* same count guard. Must run AFTER the Header insert: `Manager.OnlyClosed` is a
|
|
132
132
|
* deferred (subquery) check evaluated at commit, and the sequential auto-commit
|
|
133
|
-
* Header→Member→
|
|
133
|
+
* Header→Member→Manager order ensures the closed `Header` is committed first.
|
|
134
134
|
*/
|
|
135
|
-
async function
|
|
136
|
-
if (await strandTableCount(db, '
|
|
137
|
-
log('
|
|
135
|
+
async function insertFounderManagerIfAbsent(db, memberKey, strandId) {
|
|
136
|
+
if (await strandTableCount(db, 'Manager') > 0) {
|
|
137
|
+
log('Manager already present for strand %s; skipping founder Manager', strandId);
|
|
138
138
|
return;
|
|
139
139
|
}
|
|
140
|
-
await db.exec(`insert into Strand.
|
|
141
|
-
with context
|
|
140
|
+
await db.exec(`insert into Strand.Manager (MemberKey)
|
|
141
|
+
with context ManagerKey = null, Signature = null
|
|
142
142
|
values (?)`, [memberKey]);
|
|
143
|
-
log('Inserted founding
|
|
143
|
+
log('Inserted founding Manager for strand %s', strandId);
|
|
144
144
|
}
|
|
145
145
|
/**
|
|
146
146
|
* Founder-only one-time bootstrap of a strand's `Strand.*` membership/RBAC rows.
|
|
@@ -151,16 +151,16 @@ async function insertFounderAuthorityIfAbsent(db, memberKey, strandId) {
|
|
|
151
151
|
* restart / founder re-`addStrand` is a no-op and never double-inserts.
|
|
152
152
|
*
|
|
153
153
|
* Behavior by strand type:
|
|
154
|
-
* - **Open (`'o'`)**: insert `Header` only. `Member`/`
|
|
154
|
+
* - **Open (`'o'`)**: insert `Header` only. `Member`/`Manager`/`Invite` are
|
|
155
155
|
* `OnlyClosed` and would trip that constraint — they are skipped entirely.
|
|
156
156
|
* - **Closed (`'c'`)**: insert `Header(Type='c')`, then the founding `Member`
|
|
157
|
-
* (`Key = founderKeyPair.publicKeyB64`), then the founding `
|
|
157
|
+
* (`Key = founderKeyPair.publicKeyB64`), then the founding `Manager`
|
|
158
158
|
* (`MemberKey = founderKeyPair.publicKeyB64`). Insert order matters: the
|
|
159
|
-
* Header must commit before the deferred `OnlyClosed` checks on Member/
|
|
159
|
+
* Header must commit before the deferred `OnlyClosed` checks on Member/Manager
|
|
160
160
|
* evaluate at commit.
|
|
161
161
|
*
|
|
162
162
|
* A closed strand with no `founderKeyPair` throws: a closed strand with no founding
|
|
163
|
-
*
|
|
163
|
+
* Manager could never admit anyone, so failing loudly here (which propagates out
|
|
164
164
|
* of `StrandDatabase.initialize()` and triggers the runtime's rollback) is correct.
|
|
165
165
|
*
|
|
166
166
|
* @param db - The strand's Quereus database (schema already applied).
|
|
@@ -172,21 +172,21 @@ export async function bootstrapFounderMembership(db, params) {
|
|
|
172
172
|
const { strandId, type, founderKeyPair } = params;
|
|
173
173
|
log('Founder bootstrap for strand %s (type %s)', strandId, type);
|
|
174
174
|
// Validate BEFORE writing anything: a closed strand with no founder key must
|
|
175
|
-
// fail without leaving a closed `Header` that has no founding Member/
|
|
175
|
+
// fail without leaving a closed `Header` that has no founding Member/Manager
|
|
176
176
|
// (such a strand could never admit anyone).
|
|
177
177
|
if (type === 'c' && !founderKeyPair) {
|
|
178
178
|
throw new Error(`Cannot bootstrap closed strand ${strandId}: no founder key pair derived from MemberPrivateKey. ` +
|
|
179
|
-
'A closed strand needs a founding Member +
|
|
179
|
+
'A closed strand needs a founding Member + Manager or it can never admit members.');
|
|
180
180
|
}
|
|
181
|
-
// Header is written for every strand; Member/
|
|
181
|
+
// Header is written for every strand; Member/Manager are closed-only.
|
|
182
182
|
await insertHeaderIfAbsent(db, params);
|
|
183
183
|
if (type === 'o') {
|
|
184
|
-
// Open strand: Member/
|
|
184
|
+
// Open strand: Member/Manager/Invite are OnlyClosed — nothing else to write.
|
|
185
185
|
return;
|
|
186
186
|
}
|
|
187
187
|
const memberKey = founderKeyPair.publicKeyB64;
|
|
188
188
|
await insertFounderMemberIfAbsent(db, memberKey, strandId);
|
|
189
|
-
await
|
|
189
|
+
await insertFounderManagerIfAbsent(db, memberKey, strandId);
|
|
190
190
|
log('Founder bootstrap complete for closed strand %s', strandId);
|
|
191
191
|
}
|
|
192
192
|
/**
|
|
@@ -194,10 +194,10 @@ export async function bootstrapFounderMembership(db, params) {
|
|
|
194
194
|
*
|
|
195
195
|
* Generates a fresh invite ed25519 keypair (the public key becomes `Invite.Key`),
|
|
196
196
|
* builds the constraint's payload (`Key || '|' || coalesce(Expiration, '')`), and
|
|
197
|
-
* signs it TWICE: with the
|
|
198
|
-
*
|
|
197
|
+
* signs it TWICE: with the manager private key (→ `ManagerSignature`, proving
|
|
198
|
+
* a manager issued it) and with the invite private key (→ `InviteSignature`,
|
|
199
199
|
* proving the issuer actually holds the invite secret). Both signatures plus the
|
|
200
|
-
*
|
|
200
|
+
* manager public key are bound as constraint context for the `Invite` insert.
|
|
201
201
|
*
|
|
202
202
|
* The returned `invitePrivateKey` is the only secret the invitee needs to redeem
|
|
203
203
|
* the invite — it is NOT stored in the strand (only `Invite.Key`, the public half,
|
|
@@ -205,11 +205,11 @@ export async function bootstrapFounderMembership(db, params) {
|
|
|
205
205
|
* is `InviteKey`, so a given invite can be consumed at most once.
|
|
206
206
|
*
|
|
207
207
|
* @param db - The closed strand's database (founder already bootstrapped).
|
|
208
|
-
* @param params - The issuing
|
|
208
|
+
* @param params - The issuing manager keypair and optional expiry.
|
|
209
209
|
* @returns The invite public key and the out-of-band private seed.
|
|
210
210
|
*/
|
|
211
211
|
export async function issueInvite(db, params) {
|
|
212
|
-
const {
|
|
212
|
+
const { managerKeyPair, expiration } = params;
|
|
213
213
|
const invitePrivateKey = generatePrivateKey('ed25519', 'base64url');
|
|
214
214
|
const inviteKey = getPublicKey(invitePrivateKey, 'ed25519', 'base64url', 'base64url');
|
|
215
215
|
// Canonicalise the expiry to the engine's stored `datetime` form so the signed
|
|
@@ -217,11 +217,11 @@ export async function issueInvite(db, params) {
|
|
|
217
217
|
const expirationSegment = expiration == null ? '' : await canonicalDatetime(db, expiration);
|
|
218
218
|
const expirationColumn = expiration == null ? null : expirationSegment;
|
|
219
219
|
const payload = `${inviteKey}|${expirationSegment}`;
|
|
220
|
-
const
|
|
220
|
+
const managerSignature = signStrandPayload(payload, managerKeyPair.privateKeyB64);
|
|
221
221
|
const inviteSignature = signStrandPayload(payload, invitePrivateKey);
|
|
222
222
|
await db.exec(`insert into Strand.Invite (Key, Expiration)
|
|
223
|
-
with context
|
|
224
|
-
values (?, ?)`, [
|
|
223
|
+
with context ManagerKey = ?, ManagerSignature = ?, InviteSignature = ?
|
|
224
|
+
values (?, ?)`, [managerKeyPair.publicKeyB64, managerSignature, inviteSignature, inviteKey, expirationColumn]);
|
|
225
225
|
log('Issued invite %s (expires=%s)', inviteKey, expirationColumn ?? 'never');
|
|
226
226
|
return { inviteKey, invitePrivateKey };
|
|
227
227
|
}
|
|
@@ -278,9 +278,9 @@ export async function consumeInvite(db, params) {
|
|
|
278
278
|
await db.beginTransaction();
|
|
279
279
|
try {
|
|
280
280
|
// 1. Member — admitted by the deferred invite branch (the matching
|
|
281
|
-
// ConsumedInvite below), so no
|
|
281
|
+
// ConsumedInvite below), so no manager signature is supplied.
|
|
282
282
|
await db.exec(`insert into Strand.Member (Key)
|
|
283
|
-
with context
|
|
283
|
+
with context ManagerKey = null, ManagerSignature = null
|
|
284
284
|
values (?)`, [memberKey]);
|
|
285
285
|
// 2. ConsumedInvite — proves possession of the invite private key and that the
|
|
286
286
|
// invite has not expired (NotExpired gate against the canonical Now).
|
|
@@ -303,27 +303,27 @@ export async function consumeInvite(db, params) {
|
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
/**
|
|
306
|
-
* Admit a `Member` directly by
|
|
307
|
-
* path on `Member.Authorized`'s direct-
|
|
306
|
+
* Admit a `Member` directly by manager signature — the sibling of the invite
|
|
307
|
+
* path on `Member.Authorized`'s direct-manager branch.
|
|
308
308
|
*
|
|
309
|
-
* The constraint verifies `digest(new.Key)` against
|
|
310
|
-
* `context.
|
|
311
|
-
* No `ConsumedInvite` is involved: this is the path
|
|
312
|
-
* member it already trusts (e.g.
|
|
309
|
+
* The constraint verifies `digest(new.Key)` against a `Manager` row matching
|
|
310
|
+
* `context.ManagerKey`, so the payload is just the member key (no `'|'` join).
|
|
311
|
+
* No `ConsumedInvite` is involved: this is the path a manager uses to seat a
|
|
312
|
+
* member it already trusts (e.g. a manager-side enrollment that admits a
|
|
313
313
|
* party already authorised out-of-band).
|
|
314
314
|
*
|
|
315
315
|
* @param db - The closed strand's database.
|
|
316
|
-
* @param params - The admitting
|
|
317
|
-
* @throws If `Member.Authorized` rejects (e.g. a non-
|
|
316
|
+
* @param params - The admitting manager keypair and the new member key.
|
|
317
|
+
* @throws If `Member.Authorized` rejects (e.g. a non-manager key); the insert
|
|
318
318
|
* rolls back, leaving no `Member` row.
|
|
319
319
|
*/
|
|
320
|
-
export async function
|
|
321
|
-
const {
|
|
322
|
-
const signature = signStrandPayload(memberKey,
|
|
320
|
+
export async function addMemberByManager(db, params) {
|
|
321
|
+
const { managerKeyPair, memberKey } = params;
|
|
322
|
+
const signature = signStrandPayload(memberKey, managerKeyPair.privateKeyB64);
|
|
323
323
|
await db.exec(`insert into Strand.Member (Key)
|
|
324
|
-
with context
|
|
325
|
-
values (?)`, [
|
|
326
|
-
log('Admitted member %s by
|
|
324
|
+
with context ManagerKey = ?, ManagerSignature = ?
|
|
325
|
+
values (?)`, [managerKeyPair.publicKeyB64, signature, memberKey]);
|
|
326
|
+
log('Admitted member %s by manager %s', memberKey, managerKeyPair.publicKeyB64);
|
|
327
327
|
}
|
|
328
328
|
/**
|
|
329
329
|
* Register a network node (`PeerId`) as acting on behalf of a member.
|
|
@@ -373,77 +373,77 @@ async function memberPeerExists(db, memberKey, peerId) {
|
|
|
373
373
|
return false;
|
|
374
374
|
}
|
|
375
375
|
/**
|
|
376
|
-
* Promote a member to `
|
|
376
|
+
* Promote a member to `Manager` on the signature of an existing manager.
|
|
377
377
|
*
|
|
378
|
-
* The existing-
|
|
378
|
+
* The existing-manager branch of `Manager.Authorized` verifies
|
|
379
379
|
* `digest(coalesce(new.MemberKey, old.MemberKey))` — `new.MemberKey` =
|
|
380
|
-
* `
|
|
381
|
-
* `context.
|
|
382
|
-
* `'|'` join) and binds itself as `context.
|
|
380
|
+
* `newManagerKey` on insert — against a `Manager` row matching
|
|
381
|
+
* `context.ManagerKey`. So the signer signs the new manager key directly (no
|
|
382
|
+
* `'|'` join) and binds itself as `context.ManagerKey`.
|
|
383
383
|
*
|
|
384
|
-
* Once the founder
|
|
384
|
+
* Once the founder manager exists, the schema's `(select count(1) from Manager)
|
|
385
385
|
* <= 1` bootstrap shortcut no longer applies to a second add (at commit the count
|
|
386
386
|
* includes the new row, so it is ≥ 2), so this genuinely exercises signature
|
|
387
|
-
* verification — a non-
|
|
387
|
+
* verification — a non-manager signer, or a signature over the wrong key, is
|
|
388
388
|
* rejected.
|
|
389
389
|
*
|
|
390
|
-
* @param db - The closed strand's database (founder
|
|
391
|
-
* @param params - The authorizing
|
|
392
|
-
* @throws If `
|
|
390
|
+
* @param db - The closed strand's database (founder manager already seated).
|
|
391
|
+
* @param params - The authorizing manager's keypair and the new manager key.
|
|
392
|
+
* @throws If `Manager.Authorized` rejects; the insert rolls back.
|
|
393
393
|
*/
|
|
394
|
-
export async function
|
|
395
|
-
const {
|
|
396
|
-
const signature = signStrandPayload(
|
|
397
|
-
await db.exec(`insert into Strand.
|
|
398
|
-
with context
|
|
399
|
-
values (?)`, [
|
|
400
|
-
log('Added
|
|
394
|
+
export async function addManager(db, params) {
|
|
395
|
+
const { byManagerKeyPair, newManagerKey } = params;
|
|
396
|
+
const signature = signStrandPayload(newManagerKey, byManagerKeyPair.privateKeyB64);
|
|
397
|
+
await db.exec(`insert into Strand.Manager (MemberKey)
|
|
398
|
+
with context ManagerKey = ?, Signature = ?
|
|
399
|
+
values (?)`, [byManagerKeyPair.publicKeyB64, signature, newManagerKey]);
|
|
400
|
+
log('Added manager %s by %s', newManagerKey, byManagerKeyPair.publicKeyB64);
|
|
401
401
|
}
|
|
402
402
|
/**
|
|
403
|
-
* Remove
|
|
404
|
-
*
|
|
403
|
+
* Remove a `Manager` row — either an admin removing a different manager or a
|
|
404
|
+
* manager resigning itself.
|
|
405
405
|
*
|
|
406
406
|
* On a DELETE, `coalesce(new.MemberKey, old.MemberKey)` binds `old.MemberKey =
|
|
407
|
-
*
|
|
408
|
-
* key for BOTH accepting branches of `
|
|
407
|
+
* targetManagerKey` (there is no `new` row), so the signed payload is the target
|
|
408
|
+
* key for BOTH accepting branches of `Manager.Authorized`, and a single context
|
|
409
409
|
* construction serves both:
|
|
410
|
-
* - **Admin removal**: `
|
|
411
|
-
* existing-
|
|
412
|
-
* context.
|
|
413
|
-
* - **Self-resignation**: `
|
|
414
|
-
* equals `
|
|
415
|
-
* `old.MemberKey = context.
|
|
410
|
+
* - **Admin removal**: `byManagerKeyPair` is a different existing manager; the
|
|
411
|
+
* existing-manager branch verifies the signature against `A.MemberKey =
|
|
412
|
+
* context.ManagerKey`.
|
|
413
|
+
* - **Self-resignation**: `byManagerKeyPair` IS the target (its `publicKeyB64`
|
|
414
|
+
* equals `targetManagerKey`); the former-manager self branch verifies
|
|
415
|
+
* `old.MemberKey = context.ManagerKey` and the self-signature over `old.MemberKey`.
|
|
416
416
|
*
|
|
417
417
|
* The caller selects the case purely by which keypair it passes — no branching here.
|
|
418
418
|
*
|
|
419
419
|
* KNOWN PLATFORM GAP (not fixable here): the optimystic bootstrap-mode transactor
|
|
420
420
|
* evaluates deferred (subquery-bearing) CHECK constraints only on INSERT, not on
|
|
421
|
-
* DELETE. `
|
|
422
|
-
* `
|
|
421
|
+
* DELETE. `Manager.Authorized` is deferred, so the platform currently accepts ANY
|
|
422
|
+
* `Manager` delete regardless of signature — `removeManager`'s authorization is
|
|
423
423
|
* effectively unenforced at runtime until that gap is closed (filed as
|
|
424
424
|
* `optimystic-deferred-check-not-enforced-on-delete`). This writer still builds the
|
|
425
425
|
* correct, signed delete so it works unchanged once enforcement lands — exactly as
|
|
426
426
|
* the invite path issues correct inserts despite the open PK-uniqueness gap.
|
|
427
427
|
*
|
|
428
|
-
* KNOWN SCHEMA HAZARD (not guarded here): even with enforcement, `
|
|
429
|
-
* Authorized`'s `(select count(1) from
|
|
428
|
+
* KNOWN SCHEMA HAZARD (not guarded here): even with enforcement, `Manager.
|
|
429
|
+
* Authorized`'s `(select count(1) from Manager) <= 1` bootstrap branch is true at
|
|
430
430
|
* commit whenever a delete drops the count to ≤ 1, so removing the strand's LAST (or
|
|
431
|
-
* second-to-last)
|
|
431
|
+
* second-to-last) manager would be accepted regardless of signature — and removing
|
|
432
432
|
* the last one orphans the strand (no one can ever add another). The schema does not
|
|
433
|
-
* prevent this; a "min-one-
|
|
433
|
+
* prevent this; a "min-one-manager" invariant is deferred to a future schema change
|
|
434
434
|
* rather than grown into this writer.
|
|
435
435
|
*
|
|
436
436
|
* @param db - The closed strand's database.
|
|
437
|
-
* @param params - The authorizing keypair and the target
|
|
438
|
-
* @throws If `
|
|
439
|
-
* while > 2
|
|
437
|
+
* @param params - The authorizing keypair and the target manager key.
|
|
438
|
+
* @throws If `Manager.Authorized` rejects (e.g. a non-manager admin removal
|
|
439
|
+
* while > 2 managers remain); the delete rolls back.
|
|
440
440
|
*/
|
|
441
|
-
export async function
|
|
442
|
-
const {
|
|
443
|
-
const signature = signStrandPayload(
|
|
444
|
-
await db.exec(`delete from Strand.
|
|
445
|
-
with context
|
|
446
|
-
where MemberKey = ?`, [
|
|
447
|
-
log('Removed
|
|
441
|
+
export async function removeManager(db, params) {
|
|
442
|
+
const { byManagerKeyPair, targetManagerKey } = params;
|
|
443
|
+
const signature = signStrandPayload(targetManagerKey, byManagerKeyPair.privateKeyB64);
|
|
444
|
+
await db.exec(`delete from Strand.Manager
|
|
445
|
+
with context ManagerKey = ?, Signature = ?
|
|
446
|
+
where MemberKey = ?`, [byManagerKeyPair.publicKeyB64, signature, targetManagerKey]);
|
|
447
|
+
log('Removed manager %s by %s', targetManagerKey, byManagerKeyPair.publicKeyB64);
|
|
448
448
|
}
|
|
449
449
|
//# sourceMappingURL=strand-membership-writer.js.map
|