@serfab/cadre-core 0.8.1 → 0.10.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 (197) hide show
  1. package/README.md +6 -4
  2. package/dist/bootstrap-peer-store-file.d.ts +13 -0
  3. package/dist/bootstrap-peer-store-file.js +42 -0
  4. package/dist/bootstrap-peer-store-file.js.map +1 -0
  5. package/dist/bootstrap-peer-store.d.ts +73 -0
  6. package/dist/bootstrap-peer-store.js +128 -0
  7. package/dist/bootstrap-peer-store.js.map +1 -0
  8. package/dist/cadre-node.d.ts +1006 -101
  9. package/dist/cadre-node.js +1977 -283
  10. package/dist/cadre-node.js.map +1 -1
  11. package/dist/control-authorization.d.ts +93 -0
  12. package/dist/control-authorization.js +57 -0
  13. package/dist/control-authorization.js.map +1 -0
  14. package/dist/control-cohort.d.ts +15 -15
  15. package/dist/control-cohort.js +14 -14
  16. package/dist/control-cohort.js.map +1 -1
  17. package/dist/control-database.d.ts +1033 -76
  18. package/dist/control-database.js +1520 -153
  19. package/dist/control-database.js.map +1 -1
  20. package/dist/control-formation-recorder.d.ts +91 -12
  21. package/dist/control-formation-recorder.js +131 -16
  22. package/dist/control-formation-recorder.js.map +1 -1
  23. package/dist/control-schema.d.ts +1 -1
  24. package/dist/control-schema.js +774 -186
  25. package/dist/control-schema.js.map +1 -1
  26. package/dist/control-stream.d.ts +38 -4
  27. package/dist/control-stream.js +70 -4
  28. package/dist/control-stream.js.map +1 -1
  29. package/dist/control-write-retry.d.ts +151 -0
  30. package/dist/control-write-retry.js +411 -0
  31. package/dist/control-write-retry.js.map +1 -0
  32. package/dist/delegate-admission.d.ts +134 -0
  33. package/dist/delegate-admission.js +244 -0
  34. package/dist/delegate-admission.js.map +1 -0
  35. package/dist/device-token.d.ts +12 -12
  36. package/dist/device-token.js +15 -14
  37. package/dist/device-token.js.map +1 -1
  38. package/dist/ed25519-key.d.ts +68 -0
  39. package/dist/ed25519-key.js +106 -0
  40. package/dist/ed25519-key.js.map +1 -0
  41. package/dist/file-durable-slot.d.ts +20 -0
  42. package/dist/file-durable-slot.js +53 -0
  43. package/dist/file-durable-slot.js.map +1 -0
  44. package/dist/formation-approval.d.ts +157 -0
  45. package/dist/formation-approval.js +375 -0
  46. package/dist/formation-approval.js.map +1 -0
  47. package/dist/fs-atomic.d.ts +38 -0
  48. package/dist/fs-atomic.js +97 -0
  49. package/dist/fs-atomic.js.map +1 -0
  50. package/dist/identity-key.d.ts +49 -0
  51. package/dist/identity-key.js +81 -0
  52. package/dist/identity-key.js.map +1 -0
  53. package/dist/index.d.ts +16 -6
  54. package/dist/index.js +44 -10
  55. package/dist/index.js.map +1 -1
  56. package/dist/key-store-file.d.ts +2 -24
  57. package/dist/key-store-file.js +10 -77
  58. package/dist/key-store-file.js.map +1 -1
  59. package/dist/key-store.d.ts +1 -1
  60. package/dist/key-store.js +1 -1
  61. package/dist/membership-connection-gater.d.ts +146 -0
  62. package/dist/membership-connection-gater.js +180 -0
  63. package/dist/membership-connection-gater.js.map +1 -0
  64. package/dist/node-local-snapshot.d.ts +109 -0
  65. package/dist/node-local-snapshot.js +182 -0
  66. package/dist/node-local-snapshot.js.map +1 -0
  67. package/dist/peer-authorization.d.ts +150 -11
  68. package/dist/peer-authorization.js +170 -13
  69. package/dist/peer-authorization.js.map +1 -1
  70. package/dist/peer-record.d.ts +24 -11
  71. package/dist/peer-record.js +29 -13
  72. package/dist/peer-record.js.map +1 -1
  73. package/dist/push-node.d.ts +38 -0
  74. package/dist/push-node.js +48 -0
  75. package/dist/push-node.js.map +1 -0
  76. package/dist/push-notifier-apns.d.ts +10 -2
  77. package/dist/push-notifier-apns.js +14 -10
  78. package/dist/push-notifier-apns.js.map +1 -1
  79. package/dist/push-notifier-fcm.d.ts +7 -1
  80. package/dist/push-notifier-fcm.js +9 -10
  81. package/dist/push-notifier-fcm.js.map +1 -1
  82. package/dist/push-notifier.d.ts +16 -35
  83. package/dist/push-notifier.js +16 -44
  84. package/dist/push-notifier.js.map +1 -1
  85. package/dist/relay-addrs.d.ts +45 -0
  86. package/dist/relay-addrs.js +103 -0
  87. package/dist/relay-addrs.js.map +1 -0
  88. package/dist/relay-reservation.d.ts +266 -0
  89. package/dist/relay-reservation.js +613 -0
  90. package/dist/relay-reservation.js.map +1 -0
  91. package/dist/seed-bootstrap.d.ts +263 -79
  92. package/dist/seed-bootstrap.js +413 -211
  93. package/dist/seed-bootstrap.js.map +1 -1
  94. package/dist/seed-trust-policy.d.ts +50 -24
  95. package/dist/seed-trust-policy.js +28 -19
  96. package/dist/seed-trust-policy.js.map +1 -1
  97. package/dist/strand-addr-protocol.d.ts +42 -13
  98. package/dist/strand-addr-protocol.js +58 -52
  99. package/dist/strand-addr-protocol.js.map +1 -1
  100. package/dist/strand-backfill.d.ts +109 -0
  101. package/dist/strand-backfill.js +279 -0
  102. package/dist/strand-backfill.js.map +1 -0
  103. package/dist/strand-database.d.ts +5 -5
  104. package/dist/strand-database.js +4 -4
  105. package/dist/strand-database.js.map +1 -1
  106. package/dist/strand-formation-manager.d.ts +38 -9
  107. package/dist/strand-formation-manager.js +120 -17
  108. package/dist/strand-formation-manager.js.map +1 -1
  109. package/dist/strand-formation-protocol.d.ts +104 -5
  110. package/dist/strand-formation-protocol.js +233 -5
  111. package/dist/strand-formation-protocol.js.map +1 -1
  112. package/dist/strand-instance-manager.d.ts +40 -2
  113. package/dist/strand-instance-manager.js +93 -15
  114. package/dist/strand-instance-manager.js.map +1 -1
  115. package/dist/strand-member-key.d.ts +6 -6
  116. package/dist/strand-member-key.js +6 -6
  117. package/dist/strand-member-key.js.map +1 -1
  118. package/dist/strand-member-registry.d.ts +12 -12
  119. package/dist/strand-member-registry.js +22 -14
  120. package/dist/strand-member-registry.js.map +1 -1
  121. package/dist/strand-membership-writer.d.ts +532 -138
  122. package/dist/strand-membership-writer.js +800 -179
  123. package/dist/strand-membership-writer.js.map +1 -1
  124. package/dist/strand-solicitation.d.ts +99 -33
  125. package/dist/strand-solicitation.js +100 -52
  126. package/dist/strand-solicitation.js.map +1 -1
  127. package/dist/strand-transport-key.d.ts +40 -0
  128. package/dist/strand-transport-key.js +56 -0
  129. package/dist/strand-transport-key.js.map +1 -0
  130. package/dist/strand-wake-protocol.d.ts +9 -7
  131. package/dist/strand-wake-protocol.js +22 -46
  132. package/dist/strand-wake-protocol.js.map +1 -1
  133. package/dist/strand-watcher.d.ts +17 -2
  134. package/dist/strand-watcher.js +47 -3
  135. package/dist/strand-watcher.js.map +1 -1
  136. package/dist/trusted-owner-store-file.d.ts +12 -0
  137. package/dist/trusted-owner-store-file.js +39 -0
  138. package/dist/trusted-owner-store-file.js.map +1 -0
  139. package/dist/trusted-owner-store.d.ts +64 -0
  140. package/dist/trusted-owner-store.js +126 -0
  141. package/dist/trusted-owner-store.js.map +1 -0
  142. package/dist/types.d.ts +325 -50
  143. package/dist/types.js +13 -0
  144. package/dist/types.js.map +1 -1
  145. package/package.json +23 -8
  146. package/src/bootstrap-peer-store-file.ts +44 -0
  147. package/src/bootstrap-peer-store.ts +172 -0
  148. package/src/cadre-node.ts +2129 -295
  149. package/src/control-authorization.ts +114 -0
  150. package/src/control-cohort.ts +17 -17
  151. package/src/control-database.ts +1766 -191
  152. package/src/control-formation-recorder.ts +174 -21
  153. package/src/control-schema.ts +786 -198
  154. package/src/control-stream.ts +81 -4
  155. package/src/control-write-retry.ts +473 -0
  156. package/src/delegate-admission.ts +302 -0
  157. package/src/device-token.ts +19 -14
  158. package/src/ed25519-key.ts +134 -0
  159. package/src/file-durable-slot.ts +61 -0
  160. package/src/formation-approval.ts +604 -0
  161. package/src/fs-atomic.ts +107 -0
  162. package/src/identity-key.ts +109 -0
  163. package/src/index.ts +157 -24
  164. package/src/key-store-file.ts +10 -81
  165. package/src/key-store.ts +1 -1
  166. package/src/membership-connection-gater.ts +204 -0
  167. package/src/node-local-snapshot.ts +260 -0
  168. package/src/peer-authorization.ts +219 -13
  169. package/src/peer-record.ts +31 -13
  170. package/src/push-node.ts +64 -0
  171. package/src/push-notifier-apns.ts +16 -12
  172. package/src/push-notifier-fcm.ts +9 -10
  173. package/src/push-notifier.ts +16 -57
  174. package/src/relay-addrs.ts +111 -0
  175. package/src/relay-reservation.ts +823 -0
  176. package/src/seed-bootstrap.ts +490 -226
  177. package/src/seed-trust-policy.ts +65 -32
  178. package/src/strand-addr-protocol.ts +84 -53
  179. package/src/strand-backfill.ts +366 -0
  180. package/src/strand-database.ts +6 -6
  181. package/src/strand-formation-manager.ts +145 -19
  182. package/src/strand-formation-protocol.ts +310 -9
  183. package/src/strand-instance-manager.ts +113 -16
  184. package/src/strand-member-key.ts +7 -7
  185. package/src/strand-member-registry.ts +28 -23
  186. package/src/strand-membership-writer.ts +1430 -640
  187. package/src/strand-solicitation.ts +155 -90
  188. package/src/strand-transport-key.ts +58 -0
  189. package/src/strand-wake-protocol.ts +29 -46
  190. package/src/strand-watcher.ts +66 -3
  191. package/src/trusted-owner-store-file.ts +41 -0
  192. package/src/trusted-owner-store.ts +171 -0
  193. package/src/types.ts +342 -51
  194. package/dist/authority-key.d.ts +0 -41
  195. package/dist/authority-key.js +0 -52
  196. package/dist/authority-key.js.map +0 -1
  197. package/src/authority-key.ts +0 -68
@@ -1,26 +1,165 @@
1
+ import type { RevocableTable } from './control-authorization.js';
1
2
  /**
2
- * Canonical digest an authority signs to authorize a peer.
3
+ * Canonical digest an owner signs to vouch a peer's ENROLLMENT — the offline
4
+ * credential `cadre enroll register` verifies. No table checks this digest, so it
5
+ * carries its own `'Cadre.Enrollment'` domain tag to stay disjoint from every
6
+ * CadreControl table rule (pre-tag it collided with the `DeviceToken` owner digests,
7
+ * so an enrollment vouch doubled as a push-token delete approval).
3
8
  *
4
- * This is the exact byte construction {@link SeedBootstrapService.authorizePeer}
5
- * signs over when it inserts an authorized `CadrePeer` row:
6
- * `digest([peerId], 'sha256', 'base64url')`. Factored into one place so the
7
- * producer (authority signing) and the verifier (the offline `cadre enroll
8
- * register` check) can never drift apart — change the digest here and both move
9
- * together. The SQL mirror is the bare single-field `digest(coalesce(new.PeerId, old.PeerId))`.
9
+ * Factored into one place so the producer (owner signing) and the verifier (the
10
+ * offline `cadre enroll register` check) can never drift apart — change the digest
11
+ * here and both move together.
10
12
  */
11
13
  export declare function peerAuthorizationDigest(peerId: string): string;
12
14
  /**
13
- * Verify that `signature` is a valid authority ed25519 signature over `peerId`'s
14
- * authorization digest, using `authorityPublicKey` (base64url).
15
+ * The `CadreControl.DeviceToken` columns {@link deviceTokenAddDigest} binds, in the
16
+ * schema's order. A whole-row struct rather than six positional strings, so a caller
17
+ * cannot silently transpose `platform` and `token` (both opaque strings) and mint a
18
+ * signature over a row it did not mean to approve.
19
+ *
20
+ * Structurally satisfied by a `DeviceTokenRecord` spread with the row's `stampId`;
21
+ * `types.ts:DeviceTokenRecord` deliberately does NOT carry the stamp — it is the
22
+ * SELF-signed record shape, and the peer's own `Sig` does not cover `StampId`.
23
+ * `updatedAt` / `sig` are nullable because the columns are: both sign as `''` when
24
+ * absent, mirroring the schema's `coalesce(...)`.
25
+ */
26
+ export interface DeviceTokenAuthorizedRow {
27
+ peerId: string;
28
+ platform: string;
29
+ token: string;
30
+ updatedAt: number | null;
31
+ sig: string | null;
32
+ stampId: string;
33
+ }
34
+ /**
35
+ * Canonical digest an owner signs to authorize a `DeviceToken` INSERT — the WHOLE row,
36
+ * ending in its single-use `StampId` nonce. SQL mirror:
37
+ * `digest('CadreControl.DeviceToken', 'add', new.PeerId, new.Platform, new.Token,
38
+ * coalesce(cast(new.UpdatedAt as text), ''), coalesce(new.Sig, ''), new.StampId)` in
39
+ * `DeviceToken.AuthorizedInsert`.
40
+ *
41
+ * Binding every column means a captured approval can only ever reproduce the exact row
42
+ * it approved — never one carrying attacker-chosen `Platform`/`Token`/`UpdatedAt` — and
43
+ * binding the stamp makes it single-use: while the row lives the `unique` column blocks
44
+ * a replay, and after a clear the stamp is retired permanently into
45
+ * `CadreControl.Revocation` (`DeviceToken.NotRevoked`). This matters more here than for
46
+ * `CadrePeer`: a resurrected push token has NO freshness ceiling to retire it
47
+ * (`CadreNode.resolveDeviceToken` defaults `maxAgeMs` to infinity by design), so stamp
48
+ * retirement is the only thing that sticks.
49
+ *
50
+ * Distinct from {@link deviceTokenRemoveDigest} so a captured insert approval can never
51
+ * be replayed to delete the token, and vice versa.
52
+ */
53
+ export declare function deviceTokenAddDigest(row: DeviceTokenAuthorizedRow): string;
54
+ /**
55
+ * Canonical digest an owner signs to authorize a `DeviceToken` DELETE, bound to the
56
+ * STORED row's (PeerId, StampId). SQL mirror:
57
+ * `digest('CadreControl.DeviceToken', 'remove', old.PeerId, old.StampId)` in
58
+ * `DeviceToken.AuthorizedDelete`.
59
+ *
60
+ * A narrower vector than {@link deviceTokenAddDigest} on purpose (the same split
61
+ * `CadrePeer` makes): the clear approval names only which row is being retired, so it
62
+ * cannot be re-cut into an insert approval, and it is dead the moment the stamp it
63
+ * names is tombstoned.
64
+ */
65
+ export declare function deviceTokenRemoveDigest(peerId: string, stampId: string): string;
66
+ /**
67
+ * Canonical digest an owner signs to VOUCH a `CadrePeer` membership row (insert
68
+ * and the owner re-touch update — same semantics, deliberately the same digest).
69
+ * Binds the peer id to the row's single-use `StampId` nonce, so a captured signed
70
+ * insert cannot be replayed — while the row lives the `unique` column blocks it, and
71
+ * after a removal the stamp is retired permanently into `CadreControl.Revocation`
72
+ * (`CadrePeer.NotRevoked`) — and, because {@link cadrePeerRemoveDigest} scopes a
73
+ * DIFFERENT payload, the stored voucher (`VouchSig`) cannot be replayed to authorize
74
+ * a delete. The domain tag keeps the stored, replicated `VouchSig` useless against
75
+ * every OTHER table's rules.
76
+ *
77
+ * SQL mirror: `digest('CadreControl.CadrePeer', 'vouch', new.PeerId, new.StampId)`.
78
+ */
79
+ export declare function cadrePeerVoucherDigest(peerId: string, stampId: string): string;
80
+ /**
81
+ * Canonical digest an owner signs to REMOVE a `CadrePeer` row. Deliberately a
82
+ * distinct payload from {@link cadrePeerVoucherDigest} (the `'remove'` action tag)
83
+ * so the row's stored voucher — a signature over the voucher digest — can never
84
+ * satisfy this delete check. The signature is supplied in write context and never
85
+ * stored, so no reader can replay it; a captured remove is also dead after the
86
+ * delete lands, because a re-added row carries a FRESH `StampId` (the removed row's
87
+ * stamp is retired into `CadreControl.Revocation` and never reused).
88
+ *
89
+ * SQL mirror: `digest('CadreControl.CadrePeer', 'remove', old.PeerId, old.StampId)`.
90
+ */
91
+ export declare function cadrePeerRemoveDigest(peerId: string, stampId: string): string;
92
+ /**
93
+ * Canonical digest an owner signs to APPEND a `CadreControl.Revocation` tombstone —
94
+ * the row retiring `stampId` for the named guarded table, and recording `rowKey`
95
+ * (the removed row's primary key: OwnerKey.Key / ValidationKey.Key /
96
+ * CadrePeer.PeerId / DeviceToken.PeerId / Strand.Id) as which row was retired.
97
+ * SQL mirror:
98
+ * `digest('CadreControl.Revocation', 'remove', new.TableName, new.RowKey, new.StampId)`
99
+ * in `Revocation.Authorized`.
100
+ *
101
+ * Its own `'CadreControl.Revocation'` domain tag makes it disjoint from the
102
+ * `'CadreControl.CadrePeer'` (or `OwnerKey` / `ValidationKey` / `Strand`)
103
+ * `'remove'` digest the same owner signs in the SAME transaction for the delete
104
+ * this tombstone accompanies — a removal signature is not a retirement
105
+ * signature and cannot be replayed as one.
106
+ */
107
+ export declare function revocationDigest(tableName: RevocableTable, rowKey: string, stampId: string): string;
108
+ /**
109
+ * Verify that `signature` is a valid owner ed25519 signature over `peerId`'s
110
+ * authorization digest, using `ownerPublicKey` (base64url).
15
111
  *
16
112
  * This is the mirror of the signing done in
17
113
  * {@link SeedBootstrapService.authorizePeer}: it checks the signature against
18
114
  * {@link peerAuthorizationDigest}. A `true` result means the holder of the
19
- * authority private key vouched for this peer ID — it does NOT mean the peer is
115
+ * owner private key vouched for this peer ID — it does NOT mean the peer is
20
116
  * registered anywhere.
21
117
  *
22
118
  * Returns a boolean and never throws: malformed base64url, a bad/garbage key, or
23
119
  * any crypto failure resolves to `false` (callers want a verdict, not an
24
120
  * exception). The catch is logged at debug.
25
121
  */
26
- export declare function verifyPeerAuthorization(peerId: string, authorityPublicKey: string, signature: string): boolean;
122
+ export declare function verifyPeerAuthorization(peerId: string, ownerPublicKey: string, signature: string): boolean;
123
+ /**
124
+ * 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
127
+ * `VouchOwner`/`VouchSig` (see {@link cadrePeerVoucherDigest}).
128
+ *
129
+ * 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
131
+ * nothing about whether that owner key is itself trustworthy — the caller must
132
+ * separately check the key against the node-local trusted-owner anchor
133
+ * (`TrustedOwnerStore`), never the replicated `OwnerKey` table.
134
+ *
135
+ * Returns a boolean and never throws (same contract as
136
+ * {@link verifyPeerAuthorization}): malformed input or any crypto failure
137
+ * resolves to `false`, logged at debug.
138
+ */
139
+ export declare function verifyCadrePeerVoucher(peerId: string, stampId: string, ownerPublicKey: string, signature: string): boolean;
140
+ /**
141
+ * Canonical digest a JOINING peer signs to consent to ONE `FormationUsage`
142
+ * redemption — the read-side mirror of `formationConsentMessage` in
143
+ * 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.
147
+ */
148
+ export declare function formationConsentDigest(token: string, usageStampId: string, peerKey: string, disclosure: string): string;
149
+ /**
150
+ * Verify that `row.peerSig` is a valid ed25519 signature over the joining peer's
151
+ * consent digest (see {@link formationConsentDigest}) — the row's OWN `peerKey`.
152
+ *
153
+ * Unlike {@link verifyCadrePeerVoucher} there is no separate enrolled/owner row to
154
+ * look up: the identity IS the key carried on the row, so a forged consent would
155
+ * need that joiner's own private key. Returns a boolean and never throws (same
156
+ * contract as the siblings above): malformed input or any crypto failure resolves
157
+ * to `false`, logged at debug.
158
+ */
159
+ export declare function verifyFormationConsent(row: {
160
+ token: string;
161
+ usageStampId: string;
162
+ peerKey: string;
163
+ disclosure: string;
164
+ peerSig: string;
165
+ }): boolean;
@@ -1,40 +1,197 @@
1
1
  import debug from 'debug';
2
2
  import { digest, verify } from '@optimystic/quereus-plugin-crypto';
3
+ import { controlAuthorizationFields } from './control-authorization.js';
3
4
  const log = debug('sereus:cadre:peer-authorization');
4
5
  /**
5
- * Canonical digest an authority signs to authorize a peer.
6
+ * base64url SHA-256 digest over the shared domain-tagged field vector (see
7
+ * control-authorization.ts). The base64url twin of
8
+ * control-database.ts:buildAuthorizationMessage (which returns the same digest as raw
9
+ * bytes): sign either encoding with input encoding to match and the signed bytes agree.
10
+ */
11
+ function taggedDigest(domain, action, rowFields) {
12
+ return digest(controlAuthorizationFields(domain, action, rowFields), 'sha256', 'base64url');
13
+ }
14
+ /**
15
+ * Canonical digest an owner signs to vouch a peer's ENROLLMENT — the offline
16
+ * credential `cadre enroll register` verifies. No table checks this digest, so it
17
+ * carries its own `'Cadre.Enrollment'` domain tag to stay disjoint from every
18
+ * CadreControl table rule (pre-tag it collided with the `DeviceToken` owner digests,
19
+ * so an enrollment vouch doubled as a push-token delete approval).
6
20
  *
7
- * This is the exact byte construction {@link SeedBootstrapService.authorizePeer}
8
- * signs over when it inserts an authorized `CadrePeer` row:
9
- * `digest([peerId], 'sha256', 'base64url')`. Factored into one place so the
10
- * producer (authority signing) and the verifier (the offline `cadre enroll
11
- * register` check) can never drift apart — change the digest here and both move
12
- * together. The SQL mirror is the bare single-field `digest(coalesce(new.PeerId, old.PeerId))`.
21
+ * Factored into one place so the producer (owner signing) and the verifier (the
22
+ * offline `cadre enroll register` check) can never drift apart — change the digest
23
+ * here and both move together.
13
24
  */
14
25
  export function peerAuthorizationDigest(peerId) {
15
- return digest([peerId], 'sha256', 'base64url');
26
+ return taggedDigest('Cadre.Enrollment', 'vouch', [peerId]);
27
+ }
28
+ /**
29
+ * Canonical digest an owner signs to authorize a `DeviceToken` INSERT — the WHOLE row,
30
+ * ending in its single-use `StampId` nonce. SQL mirror:
31
+ * `digest('CadreControl.DeviceToken', 'add', new.PeerId, new.Platform, new.Token,
32
+ * coalesce(cast(new.UpdatedAt as text), ''), coalesce(new.Sig, ''), new.StampId)` in
33
+ * `DeviceToken.AuthorizedInsert`.
34
+ *
35
+ * Binding every column means a captured approval can only ever reproduce the exact row
36
+ * it approved — never one carrying attacker-chosen `Platform`/`Token`/`UpdatedAt` — and
37
+ * binding the stamp makes it single-use: while the row lives the `unique` column blocks
38
+ * a replay, and after a clear the stamp is retired permanently into
39
+ * `CadreControl.Revocation` (`DeviceToken.NotRevoked`). This matters more here than for
40
+ * `CadrePeer`: a resurrected push token has NO freshness ceiling to retire it
41
+ * (`CadreNode.resolveDeviceToken` defaults `maxAgeMs` to infinity by design), so stamp
42
+ * retirement is the only thing that sticks.
43
+ *
44
+ * Distinct from {@link deviceTokenRemoveDigest} so a captured insert approval can never
45
+ * be replayed to delete the token, and vice versa.
46
+ */
47
+ export function deviceTokenAddDigest(row) {
48
+ return taggedDigest('CadreControl.DeviceToken', 'add', [
49
+ row.peerId,
50
+ row.platform,
51
+ row.token,
52
+ row.updatedAt === null ? '' : String(row.updatedAt),
53
+ row.sig ?? '',
54
+ row.stampId,
55
+ ]);
56
+ }
57
+ /**
58
+ * Canonical digest an owner signs to authorize a `DeviceToken` DELETE, bound to the
59
+ * STORED row's (PeerId, StampId). SQL mirror:
60
+ * `digest('CadreControl.DeviceToken', 'remove', old.PeerId, old.StampId)` in
61
+ * `DeviceToken.AuthorizedDelete`.
62
+ *
63
+ * A narrower vector than {@link deviceTokenAddDigest} on purpose (the same split
64
+ * `CadrePeer` makes): the clear approval names only which row is being retired, so it
65
+ * cannot be re-cut into an insert approval, and it is dead the moment the stamp it
66
+ * names is tombstoned.
67
+ */
68
+ export function deviceTokenRemoveDigest(peerId, stampId) {
69
+ return taggedDigest('CadreControl.DeviceToken', 'remove', [peerId, stampId]);
70
+ }
71
+ /**
72
+ * Canonical digest an owner signs to VOUCH a `CadrePeer` membership row (insert
73
+ * and the owner re-touch update — same semantics, deliberately the same digest).
74
+ * Binds the peer id to the row's single-use `StampId` nonce, so a captured signed
75
+ * insert cannot be replayed — while the row lives the `unique` column blocks it, and
76
+ * after a removal the stamp is retired permanently into `CadreControl.Revocation`
77
+ * (`CadrePeer.NotRevoked`) — and, because {@link cadrePeerRemoveDigest} scopes a
78
+ * DIFFERENT payload, the stored voucher (`VouchSig`) cannot be replayed to authorize
79
+ * a delete. The domain tag keeps the stored, replicated `VouchSig` useless against
80
+ * every OTHER table's rules.
81
+ *
82
+ * SQL mirror: `digest('CadreControl.CadrePeer', 'vouch', new.PeerId, new.StampId)`.
83
+ */
84
+ export function cadrePeerVoucherDigest(peerId, stampId) {
85
+ return taggedDigest('CadreControl.CadrePeer', 'vouch', [peerId, stampId]);
86
+ }
87
+ /**
88
+ * Canonical digest an owner signs to REMOVE a `CadrePeer` row. Deliberately a
89
+ * distinct payload from {@link cadrePeerVoucherDigest} (the `'remove'` action tag)
90
+ * so the row's stored voucher — a signature over the voucher digest — can never
91
+ * satisfy this delete check. The signature is supplied in write context and never
92
+ * stored, so no reader can replay it; a captured remove is also dead after the
93
+ * delete lands, because a re-added row carries a FRESH `StampId` (the removed row's
94
+ * stamp is retired into `CadreControl.Revocation` and never reused).
95
+ *
96
+ * SQL mirror: `digest('CadreControl.CadrePeer', 'remove', old.PeerId, old.StampId)`.
97
+ */
98
+ export function cadrePeerRemoveDigest(peerId, stampId) {
99
+ return taggedDigest('CadreControl.CadrePeer', 'remove', [peerId, stampId]);
100
+ }
101
+ /**
102
+ * Canonical digest an owner signs to APPEND a `CadreControl.Revocation` tombstone —
103
+ * the row retiring `stampId` for the named guarded table, and recording `rowKey`
104
+ * (the removed row's primary key: OwnerKey.Key / ValidationKey.Key /
105
+ * CadrePeer.PeerId / DeviceToken.PeerId / Strand.Id) as which row was retired.
106
+ * SQL mirror:
107
+ * `digest('CadreControl.Revocation', 'remove', new.TableName, new.RowKey, new.StampId)`
108
+ * in `Revocation.Authorized`.
109
+ *
110
+ * Its own `'CadreControl.Revocation'` domain tag makes it disjoint from the
111
+ * `'CadreControl.CadrePeer'` (or `OwnerKey` / `ValidationKey` / `Strand`)
112
+ * `'remove'` digest the same owner signs in the SAME transaction for the delete
113
+ * this tombstone accompanies — a removal signature is not a retirement
114
+ * signature and cannot be replayed as one.
115
+ */
116
+ export function revocationDigest(tableName, rowKey, stampId) {
117
+ return taggedDigest('CadreControl.Revocation', 'remove', [tableName, rowKey, stampId]);
16
118
  }
17
119
  /**
18
- * Verify that `signature` is a valid authority ed25519 signature over `peerId`'s
19
- * authorization digest, using `authorityPublicKey` (base64url).
120
+ * Verify that `signature` is a valid owner ed25519 signature over `peerId`'s
121
+ * authorization digest, using `ownerPublicKey` (base64url).
20
122
  *
21
123
  * This is the mirror of the signing done in
22
124
  * {@link SeedBootstrapService.authorizePeer}: it checks the signature against
23
125
  * {@link peerAuthorizationDigest}. A `true` result means the holder of the
24
- * authority private key vouched for this peer ID — it does NOT mean the peer is
126
+ * owner private key vouched for this peer ID — it does NOT mean the peer is
25
127
  * registered anywhere.
26
128
  *
27
129
  * Returns a boolean and never throws: malformed base64url, a bad/garbage key, or
28
130
  * any crypto failure resolves to `false` (callers want a verdict, not an
29
131
  * exception). The catch is logged at debug.
30
132
  */
31
- export function verifyPeerAuthorization(peerId, authorityPublicKey, signature) {
133
+ export function verifyPeerAuthorization(peerId, ownerPublicKey, signature) {
32
134
  try {
33
- return verify(peerAuthorizationDigest(peerId), signature, authorityPublicKey, 'ed25519', 'base64url', 'base64url', 'base64url');
135
+ return verify(peerAuthorizationDigest(peerId), signature, ownerPublicKey, 'ed25519', 'base64url', 'base64url', 'base64url');
34
136
  }
35
137
  catch (error) {
36
138
  log('verifyPeerAuthorization failed: %o', error);
37
139
  return false;
38
140
  }
39
141
  }
142
+ /**
143
+ * Verify that `signature` is a valid owner ed25519 signature over the
144
+ * `CadrePeer` voucher digest for (`peerId`, `stampId`) — the read-side mirror
145
+ * of the voucher {@link ControlDatabase.insertCadrePeer} signs and persists into
146
+ * `VouchOwner`/`VouchSig` (see {@link cadrePeerVoucherDigest}).
147
+ *
148
+ * A `true` result means the holder of `ownerPublicKey` vouched THIS membership
149
+ * row (the peer id bound to the row's single-use `StampId` nonce). It says
150
+ * nothing about whether that owner key is itself trustworthy — the caller must
151
+ * separately check the key against the node-local trusted-owner anchor
152
+ * (`TrustedOwnerStore`), never the replicated `OwnerKey` table.
153
+ *
154
+ * Returns a boolean and never throws (same contract as
155
+ * {@link verifyPeerAuthorization}): malformed input or any crypto failure
156
+ * resolves to `false`, logged at debug.
157
+ */
158
+ export function verifyCadrePeerVoucher(peerId, stampId, ownerPublicKey, signature) {
159
+ try {
160
+ return verify(cadrePeerVoucherDigest(peerId, stampId), signature, ownerPublicKey, 'ed25519', 'base64url', 'base64url', 'base64url');
161
+ }
162
+ catch (error) {
163
+ log('verifyCadrePeerVoucher failed: %o', error);
164
+ return false;
165
+ }
166
+ }
167
+ /**
168
+ * Canonical digest a JOINING peer signs to consent to ONE `FormationUsage`
169
+ * redemption — the read-side mirror of `formationConsentMessage` in
170
+ * control-database.ts, base64url-encoded instead of raw bytes (see
171
+ * {@link taggedDigest}). That doc comment carries the field-vector rationale
172
+ * (notably why `strandId` is not signed); the two vectors must not drift, which
173
+ * peer-authorization.spec.ts pins by signing one form and verifying the other.
174
+ */
175
+ export function formationConsentDigest(token, usageStampId, peerKey, disclosure) {
176
+ return taggedDigest('CadreControl.FormationUsage', 'consent', [token, usageStampId, peerKey, disclosure]);
177
+ }
178
+ /**
179
+ * Verify that `row.peerSig` is a valid ed25519 signature over the joining peer's
180
+ * consent digest (see {@link formationConsentDigest}) — the row's OWN `peerKey`.
181
+ *
182
+ * Unlike {@link verifyCadrePeerVoucher} there is no separate enrolled/owner row to
183
+ * look up: the identity IS the key carried on the row, so a forged consent would
184
+ * need that joiner's own private key. Returns a boolean and never throws (same
185
+ * contract as the siblings above): malformed input or any crypto failure resolves
186
+ * to `false`, logged at debug.
187
+ */
188
+ export function verifyFormationConsent(row) {
189
+ try {
190
+ return verify(formationConsentDigest(row.token, row.usageStampId, row.peerKey, row.disclosure), row.peerSig, row.peerKey, 'ed25519', 'base64url', 'base64url', 'base64url');
191
+ }
192
+ catch (error) {
193
+ log('verifyFormationConsent failed: %o', error);
194
+ return false;
195
+ }
196
+ }
40
197
  //# sourceMappingURL=peer-authorization.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"peer-authorization.js","sourceRoot":"","sources":["../src/peer-authorization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAC;AAEnE,MAAM,GAAG,GAAG,KAAK,CAAC,iCAAiC,CAAC,CAAC;AAErD;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAc;IACpD,OAAO,MAAM,CAAC,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,WAAW,CAAW,CAAC;AAC3D,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,uBAAuB,CACrC,MAAc,EACd,kBAA0B,EAC1B,SAAiB;IAEjB,IAAI,CAAC;QACH,OAAO,MAAM,CACX,uBAAuB,CAAC,MAAM,CAAC,EAC/B,SAAS,EACT,kBAAkB,EAClB,SAAS,EACT,WAAW,EACX,WAAW,EACX,WAAW,CACZ,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,oCAAoC,EAAE,KAAK,CAAC,CAAC;QACjD,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"peer-authorization.js","sourceRoot":"","sources":["../src/peer-authorization.ts"],"names":[],"mappings":"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"}
@@ -3,22 +3,26 @@
3
3
  * signs when publishing its own `CadrePeer` row and re-verifies when resolving
4
4
  * another peer's row.
5
5
  *
6
- * The signed payload is a single SHA-256 digest of a delimited string of the
7
- * authenticated fields. It is intentionally NOT JSON-canonicalized: a delimited
8
- * digest is trivially deterministic across node/browser/RN (plain string concat
9
- * + sha256, no key ordering) and — crucially — is reconstructable inside the
10
- * `CadrePeer.AuthorizedUpdate` SQL constraint from the row's own columns:
6
+ * The signed payload is a single SHA-256 digest over the shared domain-tagged
7
+ * field vector (see control-authorization.ts) — the crypto plugin's injective
8
+ * multi-field encoding, so no field split is ambiguous. It is intentionally NOT
9
+ * JSON-canonicalized (deterministic across node/browser/RN, no key ordering)
10
+ * and — crucially — is reconstructable inside the `CadrePeer.AuthorizedUpdate`
11
+ * SQL constraint from the row's own columns:
11
12
  *
12
- * digest(new.PeerId || '|' || new.Multiaddr || '|' || cast(new.UpdatedAt as text))
13
+ * digest('CadreControl.CadrePeer', 'publish',
14
+ * new.PeerId, new.Multiaddr, cast(new.UpdatedAt as text))
13
15
  *
14
16
  * Keep {@link peerRecordSignedPayload} and that constraint byte-for-byte in
15
- * sync. The `'|'` delimiter is safe: a base58btc PeerId, a multiaddr (which uses
16
- * `/`), and a base-10 integer never contain it.
17
+ * sync. The `'publish'` action tag marks this as a peer's SELF-signed record
18
+ * (signed with the peer's own key, not an owner key) and keeps it disjoint from
19
+ * every owner-signed digest.
17
20
  *
18
21
  * `publicKey` is deliberately excluded from the payload — it is the key the
19
22
  * signature is verified *with* (so a signature already commits to exactly one
20
23
  * key), and its binding to `peerId` is checked separately at resolve time.
21
24
  */
25
+ import type { Multiaddr } from '@multiformats/multiaddr';
22
26
  import type { PeerAddressRecord, PeerResolveTrustPolicy } from './types.js';
23
27
  /**
24
28
  * Default freshness ceiling for resolved peer-address records (15 minutes).
@@ -49,7 +53,7 @@ export declare function peerRecordSignedPayload(peerId: string, multiaddr: strin
49
53
  * preserved and is the order signed over).
50
54
  *
51
55
  * @param fields - the record fields to sign (signaling addr first by convention)
52
- * @param privateKeyB64 - base64url ed25519 seed (see `authorityKeyFromLibp2p`)
56
+ * @param privateKeyB64 - base64url ed25519 seed (see `ed25519KeyPairFromLibp2p`)
53
57
  */
54
58
  export declare function signPeerRecord(fields: {
55
59
  peerId: string;
@@ -71,6 +75,15 @@ export declare function verifyPeerRecordSignature(record: PeerAddressRecord): bo
71
75
  export declare function isPeerRecordFresh(updatedAt: number, maxAgeMs: number, now: number): boolean;
72
76
  /** True if `addr` is a `/p2p-circuit` signaling/relay multiaddr. */
73
77
  export declare function isSignalingAddr(addr: string): boolean;
78
+ /**
79
+ * The peer id a multiaddr terminates in, or `null` when it names none.
80
+ *
81
+ * The LAST `/p2p/` component is the dial target: a circuit address
82
+ * (`…/p2p/<relay>/p2p-circuit/p2p/<target>`) names the relay first and the peer
83
+ * being reached last. Replaces the deprecated `Multiaddr.getPeerId()`, which
84
+ * cannot express that distinction.
85
+ */
86
+ export declare function trailingPeerId(addr: Multiaddr): string | null;
74
87
  /**
75
88
  * Return `addrs` with signaling (`/p2p-circuit`) addrs first, otherwise stable.
76
89
  * Used to present the WebRTC dial input ahead of direct addrs. Does NOT mutate
@@ -81,8 +94,8 @@ export declare function orderSignalingFirst(addrs: string[]): string[];
81
94
  /**
82
95
  * Default resolve trust gate: trust any peer that has a `CadrePeer` row.
83
96
  *
84
- * A row only exists because an authority signed its `AuthorizedInsert`, so row
85
- * presence already means "authority-vouched member". This is the seam where a
97
+ * A row only exists because an owner signed its `AuthorizedInsert`, so row
98
+ * presence already means "owner-vouched member". This is the seam where a
86
99
  * stricter policy (trust circle, pinned keys) plugs in — see
87
100
  * {@link PeerResolveTrustPolicy}.
88
101
  */
@@ -3,23 +3,27 @@
3
3
  * signs when publishing its own `CadrePeer` row and re-verifies when resolving
4
4
  * another peer's row.
5
5
  *
6
- * The signed payload is a single SHA-256 digest of a delimited string of the
7
- * authenticated fields. It is intentionally NOT JSON-canonicalized: a delimited
8
- * digest is trivially deterministic across node/browser/RN (plain string concat
9
- * + sha256, no key ordering) and — crucially — is reconstructable inside the
10
- * `CadrePeer.AuthorizedUpdate` SQL constraint from the row's own columns:
6
+ * The signed payload is a single SHA-256 digest over the shared domain-tagged
7
+ * field vector (see control-authorization.ts) — the crypto plugin's injective
8
+ * multi-field encoding, so no field split is ambiguous. It is intentionally NOT
9
+ * JSON-canonicalized (deterministic across node/browser/RN, no key ordering)
10
+ * and — crucially — is reconstructable inside the `CadrePeer.AuthorizedUpdate`
11
+ * SQL constraint from the row's own columns:
11
12
  *
12
- * digest(new.PeerId || '|' || new.Multiaddr || '|' || cast(new.UpdatedAt as text))
13
+ * digest('CadreControl.CadrePeer', 'publish',
14
+ * new.PeerId, new.Multiaddr, cast(new.UpdatedAt as text))
13
15
  *
14
16
  * Keep {@link peerRecordSignedPayload} and that constraint byte-for-byte in
15
- * sync. The `'|'` delimiter is safe: a base58btc PeerId, a multiaddr (which uses
16
- * `/`), and a base-10 integer never contain it.
17
+ * sync. The `'publish'` action tag marks this as a peer's SELF-signed record
18
+ * (signed with the peer's own key, not an owner key) and keeps it disjoint from
19
+ * every owner-signed digest.
17
20
  *
18
21
  * `publicKey` is deliberately excluded from the payload — it is the key the
19
22
  * signature is verified *with* (so a signature already commits to exactly one
20
23
  * key), and its binding to `peerId` is checked separately at resolve time.
21
24
  */
22
25
  import { digest, sign, verify } from '@optimystic/quereus-plugin-crypto';
26
+ import { controlAuthorizationFields } from './control-authorization.js';
23
27
  /**
24
28
  * Default freshness ceiling for resolved peer-address records (15 minutes).
25
29
  * A record older than this is treated as stale and filtered out, so a resolver
@@ -45,8 +49,8 @@ const SIGNALING_PREFIX = '/p2p-circuit';
45
49
  * @param updatedAt - epoch-ms freshness stamp
46
50
  */
47
51
  export function peerRecordSignedPayload(peerId, multiaddr, updatedAt) {
48
- const joined = `${peerId}|${multiaddr}|${updatedAt}`;
49
- return digest([joined], 'sha256', 'base64url');
52
+ const fields = controlAuthorizationFields('CadreControl.CadrePeer', 'publish', [peerId, multiaddr, String(updatedAt)]);
53
+ return digest(fields, 'sha256', 'base64url');
50
54
  }
51
55
  /**
52
56
  * Sign a peer-address record with the ed25519 private key behind its `peerId`.
@@ -54,7 +58,7 @@ export function peerRecordSignedPayload(peerId, multiaddr, updatedAt) {
54
58
  * preserved and is the order signed over).
55
59
  *
56
60
  * @param fields - the record fields to sign (signaling addr first by convention)
57
- * @param privateKeyB64 - base64url ed25519 seed (see `authorityKeyFromLibp2p`)
61
+ * @param privateKeyB64 - base64url ed25519 seed (see `ed25519KeyPairFromLibp2p`)
58
62
  */
59
63
  export function signPeerRecord(fields, privateKeyB64) {
60
64
  const payloadDigest = peerRecordSignedPayload(fields.peerId, fields.addrs.join(','), fields.updatedAt);
@@ -88,6 +92,18 @@ export function isPeerRecordFresh(updatedAt, maxAgeMs, now) {
88
92
  export function isSignalingAddr(addr) {
89
93
  return addr.includes(SIGNALING_PREFIX);
90
94
  }
95
+ /**
96
+ * The peer id a multiaddr terminates in, or `null` when it names none.
97
+ *
98
+ * The LAST `/p2p/` component is the dial target: a circuit address
99
+ * (`…/p2p/<relay>/p2p-circuit/p2p/<target>`) names the relay first and the peer
100
+ * being reached last. Replaces the deprecated `Multiaddr.getPeerId()`, which
101
+ * cannot express that distinction.
102
+ */
103
+ export function trailingPeerId(addr) {
104
+ const p2p = addr.getComponents().filter((c) => c.name === 'p2p');
105
+ return p2p.length > 0 ? (p2p[p2p.length - 1].value ?? null) : null;
106
+ }
91
107
  /**
92
108
  * Return `addrs` with signaling (`/p2p-circuit`) addrs first, otherwise stable.
93
109
  * Used to present the WebRTC dial input ahead of direct addrs. Does NOT mutate
@@ -102,8 +118,8 @@ export function orderSignalingFirst(addrs) {
102
118
  /**
103
119
  * Default resolve trust gate: trust any peer that has a `CadrePeer` row.
104
120
  *
105
- * A row only exists because an authority signed its `AuthorizedInsert`, so row
106
- * presence already means "authority-vouched member". This is the seam where a
121
+ * A row only exists because an owner signed its `AuthorizedInsert`, so row
122
+ * presence already means "owner-vouched member". This is the seam where a
107
123
  * stricter policy (trust circle, pinned keys) plugs in — see
108
124
  * {@link PeerResolveTrustPolicy}.
109
125
  */
@@ -1 +1 @@
1
- {"version":3,"file":"peer-record.js","sourceRoot":"","sources":["../src/peer-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAC;AAGzE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAE7D;;;GAGG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,8BAA8B,GAAG,CAAC,CAAC;AAEnF,+EAA+E;AAC/E,MAAM,gBAAgB,GAAG,cAAc,CAAC;AAExC;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAc,EAAE,SAAiB,EAAE,SAAiB;IAC1F,MAAM,MAAM,GAAG,GAAG,MAAM,IAAI,SAAS,IAAI,SAAS,EAAE,CAAC;IACrD,OAAO,MAAM,CAAC,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,WAAW,CAAW,CAAC;AAC3D,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAC5B,MAAiF,EACjF,aAAqB;IAErB,MAAM,aAAa,GAAG,uBAAuB,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;IACvG,MAAM,GAAG,GAAG,IAAI,CACd,aAAa,EACb,aAAa,EACb,SAAS,EACT,WAAW,EACX,WAAW,EACX,WAAW,CACF,CAAC;IACZ,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC;AAC5H,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,MAAyB;IACjE,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;QACrC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,aAAa,GAAG,uBAAuB,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;IACvG,OAAO,MAAM,CACX,aAAa,EACb,MAAM,CAAC,GAAG,EACV,MAAM,CAAC,SAAS,EAChB,SAAS,EACT,WAAW,EACX,WAAW,EACX,WAAW,CACZ,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAAiB,EAAE,QAAgB,EAAE,GAAW;IAChF,IAAI,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,EAAE,CAAC;QACrB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,GAAG,GAAG,SAAS,IAAI,QAAQ,CAAC;AACrC,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAC;AACzC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAe;IACjD,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IAChD,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,OAAO,CAAC,GAAG,SAAS,EAAE,GAAG,IAAI,CAAC,CAAC;AACjC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB;IACtC,OAAO;QACL,QAAQ;YACN,OAAO,IAAI,CAAC;QACd,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"peer-record.js","sourceRoot":"","sources":["../src/peer-record.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * push-node.ts — Node-only subpath entry for platform push **construction**.
3
+ *
4
+ * Import from the dedicated subpath, never the package root:
5
+ *
6
+ * ```ts
7
+ * import { createPushNotifier } from '@serfab/cadre-core/push-node';
8
+ * ```
9
+ *
10
+ * The FCM/APNs implementations reach for `node:crypto` / `node:http2`, so they
11
+ * are quarantined here — unreachable from the cross-platform `./index.js` graph
12
+ * (same isolation pattern as `./key-store-file`). A React Native / browser entry
13
+ * never resolves this path, so the Node builtins never reach a bundler that
14
+ * cannot satisfy them. The core references only the {@link PushNotifier}
15
+ * *interface* (`push-notifier.ts`) and a Node host injects the instance built
16
+ * here into `CadreNodeConfig.push.notifier`.
17
+ */
18
+ import type { PushCredentials } from './types.js';
19
+ import type { PushNotifier } from './push-notifier.js';
20
+ import { createFcmPushNotifier, type FcmPushDeps } from './push-notifier-fcm.js';
21
+ import { createApnsPushNotifier, type ApnsPushDeps } from './push-notifier-apns.js';
22
+ /**
23
+ * Per-platform transport/clock/log seams, injected by tests. Defaults are the
24
+ * real implementations (global `fetch` for FCM, a `node:http2` session for APNs).
25
+ */
26
+ export interface PushNotifierDeps {
27
+ fcm?: FcmPushDeps;
28
+ apns?: ApnsPushDeps;
29
+ }
30
+ /**
31
+ * Build a {@link PushNotifier} router over the configured credentials. Only the
32
+ * platforms whose credentials are present get a backing implementation; a `send`
33
+ * for an unconfigured platform returns a best-effort `no <platform> credentials`
34
+ * failure rather than throwing.
35
+ */
36
+ export declare function createPushNotifier(creds: PushCredentials, deps?: PushNotifierDeps): PushNotifier;
37
+ export { createFcmPushNotifier, createApnsPushNotifier };
38
+ export type { FcmPushDeps, ApnsPushDeps };