@serfab/cadre-core 1.13.0 → 1.14.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.
Files changed (120) hide show
  1. package/dist/cadre-invite-protocol.d.ts +348 -0
  2. package/dist/cadre-invite-protocol.js +605 -0
  3. package/dist/cadre-invite-protocol.js.map +1 -0
  4. package/dist/cadre-node.d.ts +523 -180
  5. package/dist/cadre-node.js +921 -293
  6. package/dist/cadre-node.js.map +1 -1
  7. package/dist/claim-proof.d.ts +45 -0
  8. package/dist/claim-proof.js +132 -0
  9. package/dist/claim-proof.js.map +1 -0
  10. package/dist/control-authorization.d.ts +80 -19
  11. package/dist/control-authorization.js +55 -13
  12. package/dist/control-authorization.js.map +1 -1
  13. package/dist/control-cohort.js +1 -1
  14. package/dist/control-cohort.js.map +1 -1
  15. package/dist/control-database.d.ts +514 -97
  16. package/dist/control-database.js +1033 -195
  17. package/dist/control-database.js.map +1 -1
  18. package/dist/control-protocol-guard.d.ts +142 -0
  19. package/dist/control-protocol-guard.js +293 -0
  20. package/dist/control-protocol-guard.js.map +1 -0
  21. package/dist/control-schema.d.ts +1 -1
  22. package/dist/control-schema.js +801 -1067
  23. package/dist/control-schema.js.map +1 -1
  24. package/dist/control-stream.d.ts +4 -0
  25. package/dist/control-stream.js +22 -7
  26. package/dist/control-stream.js.map +1 -1
  27. package/dist/delegate-admission.d.ts +1 -1
  28. package/dist/delegate-admission.js +1 -1
  29. package/dist/device-token.d.ts +11 -9
  30. package/dist/device-token.js +14 -12
  31. package/dist/device-token.js.map +1 -1
  32. package/dist/ed25519-key.d.ts +15 -0
  33. package/dist/ed25519-key.js +28 -1
  34. package/dist/ed25519-key.js.map +1 -1
  35. package/dist/index.d.ts +14 -9
  36. package/dist/index.js +27 -9
  37. package/dist/index.js.map +1 -1
  38. package/dist/link-budget.d.ts +16 -29
  39. package/dist/link-budget.js +16 -31
  40. package/dist/link-budget.js.map +1 -1
  41. package/dist/membership-connection-gater.d.ts +190 -151
  42. package/dist/membership-connection-gater.js +235 -173
  43. package/dist/membership-connection-gater.js.map +1 -1
  44. package/dist/node-claim-payload.d.ts +19 -0
  45. package/dist/node-claim-payload.js +120 -0
  46. package/dist/node-claim-payload.js.map +1 -0
  47. package/dist/owner-anchor-sync.d.ts +22 -0
  48. package/dist/owner-anchor-sync.js +143 -0
  49. package/dist/owner-anchor-sync.js.map +1 -0
  50. package/dist/peer-authorization.d.ts +113 -18
  51. package/dist/peer-authorization.js +225 -29
  52. package/dist/peer-authorization.js.map +1 -1
  53. package/dist/peer-dial.d.ts +19 -0
  54. package/dist/peer-dial.js +22 -1
  55. package/dist/peer-dial.js.map +1 -1
  56. package/dist/peer-join-backfill.d.ts +26 -2
  57. package/dist/peer-join-backfill.js +54 -12
  58. package/dist/peer-join-backfill.js.map +1 -1
  59. package/dist/peer-record.d.ts +13 -9
  60. package/dist/peer-record.js +16 -12
  61. package/dist/peer-record.js.map +1 -1
  62. package/dist/pending-join-runner.d.ts +66 -58
  63. package/dist/pending-join-runner.js +136 -145
  64. package/dist/pending-join-runner.js.map +1 -1
  65. package/dist/push-fanout.d.ts +8 -2
  66. package/dist/push-fanout.js +8 -2
  67. package/dist/push-fanout.js.map +1 -1
  68. package/dist/seed-bootstrap.d.ts +147 -122
  69. package/dist/seed-bootstrap.js +284 -269
  70. package/dist/seed-bootstrap.js.map +1 -1
  71. package/dist/seed-trust-policy.d.ts +98 -37
  72. package/dist/seed-trust-policy.js +188 -2
  73. package/dist/seed-trust-policy.js.map +1 -1
  74. package/dist/strand-addr-protocol.d.ts +24 -26
  75. package/dist/strand-addr-protocol.js +19 -26
  76. package/dist/strand-addr-protocol.js.map +1 -1
  77. package/dist/strand-formation-protocol.js +1 -1
  78. package/dist/strand-formation-protocol.js.map +1 -1
  79. package/dist/strand-revocation-enforcer.d.ts +11 -11
  80. package/dist/strand-revocation-enforcer.js +11 -11
  81. package/dist/strand-wake-protocol.d.ts +13 -16
  82. package/dist/strand-wake-protocol.js +13 -20
  83. package/dist/strand-wake-protocol.js.map +1 -1
  84. package/dist/trusted-owner-store.d.ts +35 -5
  85. package/dist/trusted-owner-store.js +50 -8
  86. package/dist/trusted-owner-store.js.map +1 -1
  87. package/dist/types.d.ts +302 -87
  88. package/dist/types.js.map +1 -1
  89. package/package.json +8 -7
  90. package/src/cadre-invite-protocol.ts +864 -0
  91. package/src/cadre-node.ts +9381 -8686
  92. package/src/claim-proof.ts +147 -0
  93. package/src/control-authorization.ts +203 -117
  94. package/src/control-cohort.ts +1 -1
  95. package/src/control-database.ts +1216 -218
  96. package/src/control-protocol-guard.ts +348 -0
  97. package/src/control-schema.ts +812 -1078
  98. package/src/control-stream.ts +26 -7
  99. package/src/delegate-admission.ts +1 -1
  100. package/src/device-token.ts +14 -10
  101. package/src/ed25519-key.ts +28 -1
  102. package/src/index.ts +84 -10
  103. package/src/link-budget.ts +16 -32
  104. package/src/membership-connection-gater.ts +637 -570
  105. package/src/node-claim-payload.ts +125 -0
  106. package/src/owner-anchor-sync.ts +171 -0
  107. package/src/peer-authorization.ts +258 -29
  108. package/src/peer-dial.ts +22 -1
  109. package/src/peer-join-backfill.ts +56 -11
  110. package/src/peer-record.ts +16 -11
  111. package/src/pending-join-runner.ts +729 -735
  112. package/src/push-fanout.ts +8 -2
  113. package/src/seed-bootstrap.ts +344 -321
  114. package/src/seed-trust-policy.ts +353 -140
  115. package/src/strand-addr-protocol.ts +26 -35
  116. package/src/strand-formation-protocol.ts +1 -1
  117. package/src/strand-revocation-enforcer.ts +11 -11
  118. package/src/strand-wake-protocol.ts +13 -23
  119. package/src/trusted-owner-store.ts +242 -171
  120. package/src/types.ts +2471 -2242
@@ -0,0 +1,19 @@
1
+ /** Version 1 of the payload; the only one either side reads. */
2
+ export declare const NODE_CLAIM_PAYLOAD_PREFIX = "sereus-join:1.";
3
+ export interface NodeClaimPayload {
4
+ /** The node's peer id. */
5
+ peerId: string;
6
+ /** Addresses the phone may dial the node at, each ending in `/p2p/<peerId>`. */
7
+ multiaddrs: string[];
8
+ /** The node's claim secret, base64url (`parseClaimSecret`). */
9
+ secret: string;
10
+ }
11
+ /** Encode a payload as the text the host shows. Refuses what {@link decodeNodeClaimPayload} would. */
12
+ export declare function encodeNodeClaimPayload(payload: NodeClaimPayload): string;
13
+ /**
14
+ * Decode the text a phone scanned or pasted. Refuses, naming the problem: any version but
15
+ * 1, a body that is not base64url JSON of the right shape, a peer id that does not parse,
16
+ * an empty address list, an address that does not parse or whose last `/p2p/` is not that
17
+ * peer id, and a secret {@link parseClaimSecret} refuses. The secret is never echoed.
18
+ */
19
+ export declare function decodeNodeClaimPayload(text: string): NodeClaimPayload;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * The claim payload: what a host shows as a QR code (and as text) for a node that is
3
+ * waiting to be claimed, and what the owner's phone scans to claim it.
4
+ *
5
+ * `sereus-join:1.<base64url of canonical JSON>` where the JSON is
6
+ * `{ multiaddrs, peerId, secret }`: the node's peer id, every address the phone may
7
+ * dial it at (each ending in `/p2p/<peerId>`), and the node's one-time claim secret
8
+ * (`claim-proof.ts`). The phone hands all three to `CadreNode.claimNode`. The payload
9
+ * carries no party id: the claim supplies it, and the node records it.
10
+ *
11
+ * The decoder lives here, beside the encoder, because the phone and the host both need
12
+ * it and neither depends on the other. Imports only `@multiformats/multiaddr`,
13
+ * `@libp2p/peer-id` and `uint8arrays`, so it loads on React Native and in the browser.
14
+ */
15
+ import { peerIdFromString } from '@libp2p/peer-id';
16
+ import { multiaddr, CODE_P2P, CODE_P2P_CIRCUIT } from '@multiformats/multiaddr';
17
+ import { fromString as uint8ArrayFromString, toString as uint8ArrayToString } from 'uint8arrays';
18
+ import { canonicalJson } from './canonical-json.js';
19
+ import { parseClaimSecret } from './claim-proof.js';
20
+ /** Version 1 of the payload; the only one either side reads. */
21
+ export const NODE_CLAIM_PAYLOAD_PREFIX = 'sereus-join:1.';
22
+ /** Encode a payload as the text the host shows. Refuses what {@link decodeNodeClaimPayload} would. */
23
+ export function encodeNodeClaimPayload(payload) {
24
+ const checked = requireNodeClaimPayload(payload);
25
+ const json = canonicalJson({ multiaddrs: checked.multiaddrs, peerId: checked.peerId, secret: checked.secret });
26
+ return `${NODE_CLAIM_PAYLOAD_PREFIX}${uint8ArrayToString(new TextEncoder().encode(json), 'base64url')}`;
27
+ }
28
+ /**
29
+ * Decode the text a phone scanned or pasted. Refuses, naming the problem: any version but
30
+ * 1, a body that is not base64url JSON of the right shape, a peer id that does not parse,
31
+ * an empty address list, an address that does not parse or whose last `/p2p/` is not that
32
+ * peer id, and a secret {@link parseClaimSecret} refuses. The secret is never echoed.
33
+ */
34
+ export function decodeNodeClaimPayload(text) {
35
+ const trimmed = text.trim();
36
+ if (!trimmed.startsWith(NODE_CLAIM_PAYLOAD_PREFIX)) {
37
+ throw new Error(`A claim payload must start with "${NODE_CLAIM_PAYLOAD_PREFIX}" (this one starts with "${trimmed.slice(0, NODE_CLAIM_PAYLOAD_PREFIX.length)}")`);
38
+ }
39
+ let parsed;
40
+ try {
41
+ const json = new TextDecoder().decode(uint8ArrayFromString(trimmed.slice(NODE_CLAIM_PAYLOAD_PREFIX.length), 'base64url'));
42
+ parsed = JSON.parse(json);
43
+ }
44
+ catch (error) {
45
+ throw new Error('A claim payload must carry base64url-encoded JSON after its prefix', { cause: error });
46
+ }
47
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
48
+ throw new Error('A claim payload must decode to a JSON object');
49
+ }
50
+ const { peerId, multiaddrs, secret } = parsed;
51
+ if (typeof peerId !== 'string')
52
+ throw new Error('A claim payload must name the node\'s peer id');
53
+ if (!Array.isArray(multiaddrs) || !multiaddrs.every((a) => typeof a === 'string')) {
54
+ throw new Error('A claim payload must carry the node\'s addresses as a list of strings');
55
+ }
56
+ if (typeof secret !== 'string')
57
+ throw new Error('A claim payload must carry the claim secret');
58
+ return requireNodeClaimPayload({ peerId, multiaddrs, secret });
59
+ }
60
+ /** The shape rules both directions apply. Returns the payload with its fields trimmed. */
61
+ function requireNodeClaimPayload(payload) {
62
+ const peerId = parsePeerId(payload.peerId);
63
+ const multiaddrs = payload.multiaddrs.map((a) => a.trim());
64
+ if (multiaddrs.length === 0)
65
+ throw new Error('A claim payload must carry at least one address for the node');
66
+ for (const addr of multiaddrs)
67
+ requireAddressOf(addr, peerId);
68
+ // The secret is checked, never kept in a message: see `parseClaimSecret`.
69
+ parseClaimSecret(payload.secret);
70
+ return { peerId: peerId.toString(), multiaddrs, secret: payload.secret.trim() };
71
+ }
72
+ function parsePeerId(text) {
73
+ try {
74
+ return peerIdFromString(text.trim());
75
+ }
76
+ catch (error) {
77
+ throw new Error(`A claim payload's peer id does not parse ("${text}")`, { cause: error });
78
+ }
79
+ }
80
+ /** `addr` parses, and its destination (the last `/p2p/`, past any relay hop) is `peerId`. */
81
+ function requireAddressOf(addr, peerId) {
82
+ let destination;
83
+ try {
84
+ destination = destinationPeerIdOf(addr);
85
+ }
86
+ catch (error) {
87
+ throw new Error(`A claim payload address does not parse ("${addr}")`, { cause: error });
88
+ }
89
+ if (destination === null) {
90
+ throw new Error(`A claim payload address must end in /p2p/${peerId.toString()} ("${addr}" names no peer)`);
91
+ }
92
+ let named;
93
+ try {
94
+ named = peerIdFromString(destination);
95
+ }
96
+ catch (error) {
97
+ throw new Error(`A claim payload address names a peer id that does not parse ("${addr}")`, { cause: error });
98
+ }
99
+ if (!named.equals(peerId)) {
100
+ throw new Error(`A claim payload address names another peer ("${addr}" is not an address of ${peerId.toString()})`);
101
+ }
102
+ }
103
+ /**
104
+ * The peer an address reaches: its last `/p2p/` component. A relayed address
105
+ * (`…/p2p/<relay>/p2p-circuit/p2p/<destination>`) names its destination after the
106
+ * circuit hop; one that ends at the hop names nobody. Same rule as
107
+ * `groupAddrsByPeerId` in `peer-addr-book.ts`.
108
+ */
109
+ function destinationPeerIdOf(addr) {
110
+ const components = multiaddr(addr).getComponents();
111
+ for (let i = components.length - 1; i >= 0; i--) {
112
+ const component = components[i];
113
+ if (component.code === CODE_P2P_CIRCUIT)
114
+ return null;
115
+ if (component.code === CODE_P2P && component.value)
116
+ return component.value;
117
+ }
118
+ return null;
119
+ }
120
+ //# sourceMappingURL=node-claim-payload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-claim-payload.js","sourceRoot":"","sources":["../src/node-claim-payload.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { TrustSource } from './trusted-owner-store.js';
2
+ import type { InvitationChain, OwnerKeyRow, RevocationRow } from './types.js';
3
+ export interface OwnerAnchorInputs {
4
+ /** The party whose table is being read; every stored proof is judged for it, so a voucher or tombstone signed for another party neither seats nor removes a key. */
5
+ partyId: string;
6
+ /** The anchor at the start of the pass, every key with its provenance (`TrustedOwnerStore.sources`). */
7
+ anchor: ReadonlyMap<string, TrustSource>;
8
+ /** `OwnerKey` rows as read; rows whose stamp a tombstone in `tombstones` retires are dropped here too. */
9
+ rows: readonly OwnerKeyRow[];
10
+ /** `Revocation` rows; only those with `tableName === 'OwnerKey'` are consulted. */
11
+ tombstones: readonly RevocationRow[];
12
+ /** The usage and invitation rows, when some owner row is invitation-admitted; null skips those rows. */
13
+ chain: InvitationChain | null;
14
+ }
15
+ export interface OwnerAnchorDerivation {
16
+ /** The keys the anchor should hold after this pass. */
17
+ target: ReadonlySet<string>;
18
+ /** Keys whose verifiable removal was NOT applied because the anchor would have emptied. */
19
+ refusedRemovals: readonly string[];
20
+ }
21
+ /** The rule above, as one pure function. */
22
+ export declare function deriveOwnerAnchor({ partyId, anchor, rows, tombstones, chain }: OwnerAnchorInputs): OwnerAnchorDerivation;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * How the node-local trusted-owner anchor follows the replicated `OwnerKey` table along
3
+ * VERIFIABLE chains — the pure rule, with no I/O, so it can be tested without a database.
4
+ * `CadreNode.syncOwnerAnchor` reads the table, the tombstones and the invitation chain,
5
+ * calls {@link deriveOwnerAnchor}, and applies the difference to the store.
6
+ *
7
+ * Why not simply trust the table: an empty local copy can seat any key through the
8
+ * schema's genesis branch and that row replicates (`docs/architecture.md` → "Node-local
9
+ * trusted-owner anchor"). So every node grows its anchor only through rows whose stored
10
+ * proof verifies against a key it already trusts, on every membership refresh. The terms
11
+ * used throughout:
12
+ *
13
+ * - **base**: the anchor's entries whose provenance is not `chain` (trusted out of band).
14
+ * - **live row**: an `OwnerKey` row whose stamp no tombstone retires.
15
+ * - **kept**: a `chain` entry that still has a live row. A derived entry is state, not
16
+ * recomputed from the base each pass: it entered through a proof that verified then, and
17
+ * it stays until its own row is retired or a verifiable tombstone removes it. This is what
18
+ * lets an owner rotate (A adds A', A' removes A): A' keeps its place after A's row is
19
+ * retired, although nothing could re-derive it from the remaining base.
20
+ * - **derivable from S**: a live row whose stored proof verifies against S — owner-signed
21
+ * (`vouchSig` set): `vouchOwner` is in S and `verifyOwnerKeyVoucher` holds;
22
+ * invitation-admitted (`vouchSig` null, `vouchUsage` set): `verifyInvitationOwnerAdmission`
23
+ * holds with S as the anchor. The founding row (all three null) is never derivable:
24
+ * founders enter anchors only out of band.
25
+ * - **derived**: the least fixpoint from base ∪ kept — add every key whose live row is
26
+ * derivable from what is already in the set, until nothing changes. **chainDerived** is the
27
+ * keys whose live row was derivable in this pass, base keys included.
28
+ * - **removed**: a key K in derived is removed when an `OwnerKey` tombstone names K, its
29
+ * signer is in derived and is not K, its stored signature verifies
30
+ * (`verifyRevocationSigner`), AND K is not in chainDerived. The last condition is what
31
+ * makes a re-add win over an older tombstone: a removed owner re-added under a fresh
32
+ * stamp has a live derivable row again, and the old stamp's tombstone no longer applies.
33
+ * - **target** = derived minus removed. If that would be empty, no removal is applied this
34
+ * pass (target = derived) and the refused removals are reported for the caller to log.
35
+ *
36
+ * Every tombstone is judged against the pass-start set (derived) at once, so the outcome
37
+ * does not depend on the order they are read in: a signer that is itself removed in this
38
+ * pass still counts, and a mutual removal of the last two owners is refused rather than
39
+ * decided by read order.
40
+ *
41
+ * An owner C that a removed owner B had added therefore stays where it was already derived
42
+ * (its row is live) and cannot be derived on a machine that never held it (the chain from
43
+ * that machine's base runs through B's retired row) until a remaining owner re-adds it or
44
+ * an invitation pins it there. The rotation case above is the same shape with C = A', so the
45
+ * two cannot be told apart from replicated state; keeping is the only rule under which a
46
+ * sole owner can rotate at all.
47
+ */
48
+ import { verifyInvitationOwnerAdmission, verifyOwnerKeyVoucher, verifyRevocationSigner } from './peer-authorization.js';
49
+ /** The rule above, as one pure function. */
50
+ export function deriveOwnerAnchor({ partyId, anchor, rows, tombstones, chain }) {
51
+ const ownerTombstones = tombstones.filter(tombstone => tombstone.tableName === 'OwnerKey');
52
+ // NOTE: a stamp is retired by any tombstone that names it, verified or not — the same
53
+ // reading `queryCadrePeers` and `getOwnerKeys` give the table. A forged tombstone can
54
+ // therefore prune a derived entry (its row goes) but never seat or keep one; if tombstone
55
+ // pollution is ever closed at the write side, judge retirement by the verified signer too.
56
+ const retired = new Set(ownerTombstones.map(tombstone => tombstone.stampId));
57
+ const liveRows = rows.filter(row => !retired.has(row.stampId));
58
+ const derived = passStartSet(anchor, liveRows);
59
+ const chainDerived = deriveFixpoint(partyId, derived, liveRows, chain);
60
+ const removed = removedKeys(partyId, derived, chainDerived, ownerTombstones);
61
+ if (removed.size > 0 && removed.size === derived.size) {
62
+ return { target: derived, refusedRemovals: Array.from(removed) };
63
+ }
64
+ const target = new Set(Array.from(derived).filter(key => !removed.has(key)));
65
+ return { target, refusedRemovals: [] };
66
+ }
67
+ /** base ∪ kept: every out-of-band entry, and every `chain` entry whose row is still live. */
68
+ function passStartSet(anchor, liveRows) {
69
+ const liveKeys = new Set(liveRows.map(row => row.key));
70
+ const start = new Set();
71
+ for (const [key, source] of anchor) {
72
+ if (source !== 'chain' || liveKeys.has(key)) {
73
+ start.add(key);
74
+ }
75
+ }
76
+ return start;
77
+ }
78
+ /**
79
+ * Grow `derived` to the least fixpoint and return chainDerived: every key whose live row was
80
+ * derivable. A row is judged once its voucher is in the set, and settled either way then:
81
+ * trust only grows within a pass, and a proof that failed against a trusted voucher cannot
82
+ * pass later, so no row is verified twice.
83
+ */
84
+ function deriveFixpoint(partyId, derived, liveRows, chain) {
85
+ const chainDerived = new Set();
86
+ const pending = new Set(liveRows);
87
+ let progress = true;
88
+ while (progress) {
89
+ progress = false;
90
+ for (const row of pending) {
91
+ if (row.vouchOwner === null || !derived.has(row.vouchOwner)) {
92
+ continue;
93
+ }
94
+ pending.delete(row);
95
+ if (proofVerifies(partyId, row, derived, chain)) {
96
+ chainDerived.add(row.key);
97
+ derived.add(row.key);
98
+ progress = true;
99
+ }
100
+ }
101
+ }
102
+ return chainDerived;
103
+ }
104
+ /** Does `row`'s stored proof verify against `trusted`, for `partyId`, by the kind of proof it carries (see the module doc)? */
105
+ function proofVerifies(partyId, row, trusted, chain) {
106
+ if (row.vouchOwner === null) {
107
+ return false;
108
+ }
109
+ if (row.vouchSig !== null) {
110
+ return verifyOwnerKeyVoucher(partyId, row.key, row.stampId, row.vouchOwner, row.vouchSig);
111
+ }
112
+ if (row.vouchUsage === null) {
113
+ return false;
114
+ }
115
+ const usage = chain?.usages.get(row.vouchUsage);
116
+ const invite = usage === undefined ? undefined : chain?.invites.get(usage.inviteKey);
117
+ if (usage === undefined || invite === undefined) {
118
+ return false;
119
+ }
120
+ return verifyInvitationOwnerAdmission(partyId, row, usage, invite, key => trusted.has(key));
121
+ }
122
+ /**
123
+ * The keys of `derived` that a verifiable tombstone removes (see the module doc), every
124
+ * tombstone judged against the same pass-start set. A tombstone for a key outside `derived`
125
+ * changes nothing and is skipped before any signature work.
126
+ */
127
+ function removedKeys(partyId, derived, chainDerived, ownerTombstones) {
128
+ const removed = new Set();
129
+ for (const tombstone of ownerTombstones) {
130
+ const key = tombstone.rowKey;
131
+ if (!derived.has(key) || chainDerived.has(key) || removed.has(key)) {
132
+ continue;
133
+ }
134
+ if (tombstone.signerKey === key || !derived.has(tombstone.signerKey)) {
135
+ continue;
136
+ }
137
+ if (verifyRevocationSigner(partyId, tombstone)) {
138
+ removed.add(key);
139
+ }
140
+ }
141
+ return removed;
142
+ }
143
+ //# sourceMappingURL=owner-anchor-sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owner-anchor-sync.js","sourceRoot":"","sources":["../src/owner-anchor-sync.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
- import type { RevocableTable } from './control-authorization.js';
1
+ import type { CadreInviteSignedFields, RevocableTable } from './control-authorization.js';
2
+ import type { CadreInviteRow, CadreInviteUsageRow, CadrePeerVoucherFields, OwnerKeyRow, RevocationRow } from './types.js';
2
3
  /**
3
4
  * Canonical digest an owner signs to vouch a peer's ENROLLMENT — the offline
4
5
  * credential `cadre enroll register` verifies. No table checks this digest, so it
@@ -34,7 +35,7 @@ export interface DeviceTokenAuthorizedRow {
34
35
  /**
35
36
  * Canonical digest an owner signs to authorize a `DeviceToken` INSERT — the WHOLE row,
36
37
  * ending in its single-use `StampId` nonce. SQL mirror:
37
- * `digest('CadreControl.DeviceToken', 'add', new.PeerId, new.Platform, new.Token,
38
+ * `digest('CadreControl.DeviceToken', 'add', party_id(), new.PeerId, new.Platform, new.Token,
38
39
  * coalesce(cast(new.UpdatedAt as text), ''), coalesce(new.Sig, ''), new.StampId)` in
39
40
  * `DeviceToken.AuthorizedInsert`.
40
41
  *
@@ -50,11 +51,11 @@ export interface DeviceTokenAuthorizedRow {
50
51
  * Distinct from {@link deviceTokenRemoveDigest} so a captured insert approval can never
51
52
  * be replayed to delete the token, and vice versa.
52
53
  */
53
- export declare function deviceTokenAddDigest(row: DeviceTokenAuthorizedRow): string;
54
+ export declare function deviceTokenAddDigest(partyId: string, row: DeviceTokenAuthorizedRow): string;
54
55
  /**
55
56
  * Canonical digest an owner signs to authorize a `DeviceToken` DELETE, bound to the
56
57
  * STORED row's (PeerId, StampId). SQL mirror:
57
- * `digest('CadreControl.DeviceToken', 'remove', old.PeerId, old.StampId)` in
58
+ * `digest('CadreControl.DeviceToken', 'remove', party_id(), old.PeerId, old.StampId)` in
58
59
  * `DeviceToken.AuthorizedDelete`.
59
60
  *
60
61
  * A narrower vector than {@link deviceTokenAddDigest} on purpose (the same split
@@ -62,7 +63,7 @@ export declare function deviceTokenAddDigest(row: DeviceTokenAuthorizedRow): str
62
63
  * cannot be re-cut into an insert approval, and it is dead the moment the stamp it
63
64
  * names is tombstoned.
64
65
  */
65
- export declare function deviceTokenRemoveDigest(peerId: string, stampId: string): string;
66
+ export declare function deviceTokenRemoveDigest(partyId: string, peerId: string, stampId: string): string;
66
67
  /**
67
68
  * Canonical digest an owner signs to VOUCH a `CadrePeer` membership row (insert
68
69
  * and the owner re-touch update — same semantics, deliberately the same digest).
@@ -74,9 +75,9 @@ export declare function deviceTokenRemoveDigest(peerId: string, stampId: string)
74
75
  * a delete. The domain tag keeps the stored, replicated `VouchSig` useless against
75
76
  * every OTHER table's rules.
76
77
  *
77
- * SQL mirror: `digest('CadreControl.CadrePeer', 'vouch', new.PeerId, new.StampId)`.
78
+ * SQL mirror: `digest('CadreControl.CadrePeer', 'vouch', party_id(), new.PeerId, new.StampId)`.
78
79
  */
79
- export declare function cadrePeerVoucherDigest(peerId: string, stampId: string): string;
80
+ export declare function cadrePeerVoucherDigest(partyId: string, peerId: string, stampId: string): string;
80
81
  /**
81
82
  * Canonical digest an owner signs to REMOVE a `CadrePeer` row. Deliberately a
82
83
  * distinct payload from {@link cadrePeerVoucherDigest} (the `'remove'` action tag)
@@ -86,16 +87,16 @@ export declare function cadrePeerVoucherDigest(peerId: string, stampId: string):
86
87
  * delete lands, because a re-added row carries a FRESH `StampId` (the removed row's
87
88
  * stamp is retired into `CadreControl.Revocation` and never reused).
88
89
  *
89
- * SQL mirror: `digest('CadreControl.CadrePeer', 'remove', old.PeerId, old.StampId)`.
90
+ * SQL mirror: `digest('CadreControl.CadrePeer', 'remove', party_id(), old.PeerId, old.StampId)`.
90
91
  */
91
- export declare function cadrePeerRemoveDigest(peerId: string, stampId: string): string;
92
+ export declare function cadrePeerRemoveDigest(partyId: string, peerId: string, stampId: string): string;
92
93
  /**
93
94
  * Canonical digest an owner signs to APPEND a `CadreControl.Revocation` tombstone —
94
95
  * the row retiring `stampId` for the named guarded table, and recording `rowKey`
95
96
  * (the removed row's primary key: OwnerKey.Key / ValidationKey.Key /
96
97
  * CadrePeer.PeerId / DeviceToken.PeerId / Strand.Id) as which row was retired.
97
98
  * SQL mirror:
98
- * `digest('CadreControl.Revocation', 'remove', new.TableName, new.RowKey, new.StampId)`
99
+ * `digest('CadreControl.Revocation', 'remove', party_id(), new.TableName, new.RowKey, new.StampId)`
99
100
  * in `Revocation.Authorized`.
100
101
  *
101
102
  * Its own `'CadreControl.Revocation'` domain tag makes it disjoint from the
@@ -104,7 +105,28 @@ export declare function cadrePeerRemoveDigest(peerId: string, stampId: string):
104
105
  * this tombstone accompanies — a removal signature is not a retirement
105
106
  * signature and cannot be replayed as one.
106
107
  */
107
- export declare function revocationDigest(tableName: RevocableTable, rowKey: string, stampId: string): string;
108
+ export declare function revocationDigest(partyId: string, tableName: RevocableTable, rowKey: string, stampId: string): string;
109
+ /**
110
+ * Does a `Revocation` row's stored signer pair (`SignerKey` / `SignerSig`) verify over the
111
+ * tombstone's own triple, for `partyId`? The read-side mirror of `Revocation.Authorized`,
112
+ * for a node that received the row by replication and must decide whether to honour it.
113
+ * Whether `row.signerKey` is an owner this node trusts is the caller's question, as for
114
+ * {@link verifyCadrePeerVoucher}. Never throws: a malformed row verifies as `false`.
115
+ */
116
+ export declare function verifyRevocationSigner(partyId: string, row: RevocationRow): boolean;
117
+ /**
118
+ * Canonical digest an owner signs to seat a further `OwnerKey` row — the signed-add branch
119
+ * of `OwnerKey.Authorized`, which stores the pair as `VouchOwner` / `VouchSig`. SQL mirror:
120
+ * `digest('CadreControl.OwnerKey', 'add', party_id(), new.Key, new.StampId)`. The removal
121
+ * digest needs no builder here: it is the generic guarded `'remove'` digest over (Key, StampId).
122
+ */
123
+ export declare function ownerKeyAddDigest(partyId: string, key: string, stampId: string): string;
124
+ /**
125
+ * Does an owner-signed `OwnerKey` row's stored voucher verify — `vouchSig` by `vouchOwner`
126
+ * over {@link ownerKeyAddDigest} for (`partyId`, `key`, `stampId`)? Whether `vouchOwner` is
127
+ * anchored is the caller's question, as for {@link verifyCadrePeerVoucher}. Never throws.
128
+ */
129
+ export declare function verifyOwnerKeyVoucher(partyId: string, key: string, stampId: string, vouchOwner: string, vouchSig: string): boolean;
108
130
  /**
109
131
  * Verify that `signature` is a valid owner ed25519 signature over `peerId`'s
110
132
  * authorization digest, using `ownerPublicKey` (base64url).
@@ -122,12 +144,13 @@ export declare function revocationDigest(tableName: RevocableTable, rowKey: stri
122
144
  export declare function verifyPeerAuthorization(peerId: string, ownerPublicKey: string, signature: string): boolean;
123
145
  /**
124
146
  * Verify that `signature` is a valid owner ed25519 signature over the
125
- * `CadrePeer` voucher digest for (`peerId`, `stampId`) — the read-side mirror
126
- * of the voucher {@link ControlDatabase.insertCadrePeer} signs and persists into
147
+ * `CadrePeer` voucher digest for (`partyId`, `peerId`, `stampId`) — the read-side
148
+ * mirror of the voucher {@link ControlDatabase.insertCadrePeer} signs and persists into
127
149
  * `VouchOwner`/`VouchSig` (see {@link cadrePeerVoucherDigest}).
128
150
  *
129
151
  * A `true` result means the holder of `ownerPublicKey` vouched THIS membership
130
- * row (the peer id bound to the row's single-use `StampId` nonce). It says
152
+ * row (the peer id bound to the row's single-use `StampId` nonce) in THIS party: a
153
+ * voucher copied from another party's table that shares the owner key fails. It says
131
154
  * nothing about whether that owner key is itself trustworthy — the caller must
132
155
  * separately check the key against the node-local trusted-owner anchor
133
156
  * (`TrustedOwnerStore`), never the replicated `OwnerKey` table.
@@ -136,14 +159,14 @@ export declare function verifyPeerAuthorization(peerId: string, ownerPublicKey:
136
159
  * {@link verifyPeerAuthorization}): malformed input or any crypto failure
137
160
  * resolves to `false`, logged at debug.
138
161
  */
139
- export declare function verifyCadrePeerVoucher(peerId: string, stampId: string, ownerPublicKey: string, signature: string): boolean;
162
+ export declare function verifyCadrePeerVoucher(partyId: string, peerId: string, stampId: string, ownerPublicKey: string, signature: string): boolean;
140
163
  /**
141
164
  * Canonical digest a JOINING peer signs to consent to ONE `FormationUsage`
142
165
  * redemption — the read-side mirror of `formationConsentMessage` in
143
166
  * control-database.ts, base64url-encoded instead of raw bytes (see
144
- * {@link taggedDigest}). That doc comment carries the field-vector rationale
145
- * (notably why `strandId` is not signed); the two vectors must not drift, which
146
- * peer-authorization.spec.ts pins by signing one form and verifying the other.
167
+ * {@link unboundTaggedDigest}). That doc comment carries the field-vector rationale
168
+ * (notably why neither `strandId` nor the party id is signed); the two vectors must not
169
+ * drift, which peer-authorization.spec.ts pins by signing one form and verifying the other.
147
170
  */
148
171
  export declare function formationConsentDigest(token: string, usageStampId: string, peerKey: string, disclosure: string): string;
149
172
  /**
@@ -163,3 +186,75 @@ export declare function verifyFormationConsent(row: {
163
186
  disclosure: string;
164
187
  peerSig: string;
165
188
  }): boolean;
189
+ /**
190
+ * Canonical digest an owner signs to seat a `CadreInvite` row — the read-side mirror
191
+ * of `cadreInviteAddMessage` in control-database.ts, base64url instead of raw bytes
192
+ * (see {@link taggedDigest}). The whole row is bound, nullable columns as `''` and
193
+ * `ExpiresAt` in its stored canonical form, through the one field builder both sides
194
+ * share (`cadreInviteRowFields`). SQL mirror: `CadreInvite.AuthorizedInsert`.
195
+ */
196
+ export declare function cadreInviteAddDigest(partyId: string, row: CadreInviteSignedFields): string;
197
+ /**
198
+ * Canonical digest the HOLDER of a `CadreInvite`'s private key signs to redeem it once:
199
+ * proof of possession, verified against the invitation key itself
200
+ * (`CadreInviteUsage.InvitePossessed`). Binds the redemption's single-use nonce and the
201
+ * device being admitted, so one redemption cannot be re-presented for another device
202
+ * or another use. Mirror of `cadreInviteRedeemMessage` in control-database.ts.
203
+ */
204
+ export declare function cadreInviteRedeemDigest(partyId: string, inviteKey: string, usageStampId: string, peerKey: string): string;
205
+ /**
206
+ * Canonical digest the DEVICE signs to consent to being admitted by one redemption
207
+ * (`CadreInviteUsage.PeerConsented`), over the same fields as
208
+ * {@link cadreInviteRedeemDigest} under a distinct action tag, so the holder's and
209
+ * the device's signatures are never interchangeable. Mirror of
210
+ * `cadreInviteConsentMessage` in control-database.ts.
211
+ */
212
+ export declare function cadreInviteConsentDigest(partyId: string, inviteKey: string, usageStampId: string, peerKey: string): string;
213
+ /**
214
+ * Does a `CadreInvite` row carry a valid `'add'` signature by its own `issuerKey`, for
215
+ * `partyId`? The redeeming device runs this over the row a member replies with, after
216
+ * checking the issuer against its pinned owner keys and the reply's party against the
217
+ * invitation's, and a member over a bundle's copy it does not hold yet
218
+ * (`ControlDatabase.isCadreInviteLive`); whether that issuer IS an owner is the caller's question.
219
+ * Never throws: a malformed row verifies as `false`.
220
+ */
221
+ export declare function verifyCadreInviteRow(partyId: string, invite: CadreInviteRow): boolean;
222
+ /**
223
+ * The two signatures a redemption carries, checked before any database work: the holder's
224
+ * `'redeem'` signature with the invitation key, and the device's `'consent'` signature with
225
+ * its own key, both over (`inviteKey`, `usageStampId`, `peerKey`). The pre-check twin of
226
+ * `CadreInviteUsage.InvitePossessed` / `PeerConsented`, as `verifyFormationConsent` is for
227
+ * formation: the schema stays the authority, this turns a bad signature into a clean
228
+ * refusal instead of a constraint failure at commit. Never throws.
229
+ */
230
+ export declare function verifyCadreInviteRedemption(partyId: string, fields: {
231
+ inviteKey: string;
232
+ usageStampId: string;
233
+ peerKey: string;
234
+ }, inviteSig: string, peerSig: string): boolean;
235
+ /**
236
+ * Is an invitation-admitted `CadrePeer` row (`vouchSig` null, `vouchUsage` set) a
237
+ * member this node should trust? The read-side mirror of the consent branch of
238
+ * `CadrePeer.AuthorizedInsert`, re-checked against THIS node's anchor because the
239
+ * replicated `OwnerKey` table can be polluted. The usage must name this peer id and this
240
+ * exact row incarnation (`peerStampId`); every other link is {@link verifyAdmissionChain}.
241
+ *
242
+ * Expiry, use count and withdrawal are deliberately NOT re-checked: they were conditions
243
+ * at redemption (`CadreInviteUsage.Authorized`), and membership persists until an owner
244
+ * removes the row — exactly as an owner-vouched row's membership does. Keeping a device
245
+ * out is removing its row AND withdrawing the invitation.
246
+ *
247
+ * Returns a boolean and never throws (the siblings' contract): malformed input or any
248
+ * crypto failure resolves to `false`, logged at debug.
249
+ */
250
+ export declare function verifyInvitationAdmission(partyId: string, row: CadrePeerVoucherFields, usage: CadreInviteUsageRow, invite: CadreInviteRow, isAnchored: (ownerKey: string) => boolean): boolean;
251
+ /**
252
+ * Is an invitation-admitted `OwnerKey` row (`vouchSig` null, `vouchUsage` set) an owner
253
+ * this node should derive into its anchor? The read-side mirror of the consent branch of
254
+ * `OwnerKey.Authorized` (`owner-anchor-sync.ts` → `deriveOwnerAnchor`): the invitation
255
+ * must grant ownership, and the usage must name this key (`peerKey`) and this exact row
256
+ * incarnation (`ownerStampId`); every other link is {@link verifyAdmissionChain}. Same
257
+ * never-throws contract and the same deliberate omissions as
258
+ * {@link verifyInvitationAdmission}.
259
+ */
260
+ export declare function verifyInvitationOwnerAdmission(partyId: string, row: OwnerKeyRow, usage: CadreInviteUsageRow, invite: CadreInviteRow, isAnchored: (ownerKey: string) => boolean): boolean;