@serfab/cadre-core 1.14.0 → 1.16.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 (171) hide show
  1. package/README.md +293 -274
  2. package/dist/cadre-invite-protocol.d.ts +64 -29
  3. package/dist/cadre-invite-protocol.js +207 -38
  4. package/dist/cadre-invite-protocol.js.map +1 -1
  5. package/dist/cadre-join-request.d.ts +42 -0
  6. package/dist/cadre-join-request.js +144 -0
  7. package/dist/cadre-join-request.js.map +1 -0
  8. package/dist/cadre-node.d.ts +375 -79
  9. package/dist/cadre-node.js +758 -186
  10. package/dist/cadre-node.js.map +1 -1
  11. package/dist/control-database.d.ts +92 -12
  12. package/dist/control-database.js +323 -142
  13. package/dist/control-database.js.map +1 -1
  14. package/dist/control-schema.d.ts +1 -1
  15. package/dist/control-schema.js +19 -3
  16. package/dist/control-schema.js.map +1 -1
  17. package/dist/ed25519-key.d.ts +7 -9
  18. package/dist/ed25519-key.js +20 -10
  19. package/dist/ed25519-key.js.map +1 -1
  20. package/dist/fetch-impl.d.ts +12 -0
  21. package/dist/fetch-impl.js +23 -0
  22. package/dist/fetch-impl.js.map +1 -0
  23. package/dist/file-durable-slot.js +2 -1
  24. package/dist/file-durable-slot.js.map +1 -1
  25. package/dist/file-safe-name.d.ts +25 -0
  26. package/dist/file-safe-name.js +29 -0
  27. package/dist/file-safe-name.js.map +1 -0
  28. package/dist/formation-approval.js +5 -11
  29. package/dist/formation-approval.js.map +1 -1
  30. package/dist/fs-atomic.d.ts +0 -19
  31. package/dist/fs-atomic.js +3 -24
  32. package/dist/fs-atomic.js.map +1 -1
  33. package/dist/index.d.ts +12 -5
  34. package/dist/index.js +31 -6
  35. package/dist/index.js.map +1 -1
  36. package/dist/invitation-bootstrap.d.ts +50 -12
  37. package/dist/invitation-bootstrap.js +76 -13
  38. package/dist/invitation-bootstrap.js.map +1 -1
  39. package/dist/joined-strand-store.d.ts +7 -0
  40. package/dist/joined-strand-store.js +9 -0
  41. package/dist/joined-strand-store.js.map +1 -1
  42. package/dist/key-store-file.js +2 -1
  43. package/dist/key-store-file.js.map +1 -1
  44. package/dist/link-budget.d.ts +9 -4
  45. package/dist/link-budget.js +9 -4
  46. package/dist/link-budget.js.map +1 -1
  47. package/dist/machine-removal.d.ts +78 -0
  48. package/dist/machine-removal.js +166 -0
  49. package/dist/machine-removal.js.map +1 -0
  50. package/dist/membership-connection-gater.d.ts +6 -5
  51. package/dist/membership-connection-gater.js +6 -5
  52. package/dist/membership-connection-gater.js.map +1 -1
  53. package/dist/node-claim-payload.d.ts +55 -0
  54. package/dist/node-claim-payload.js +108 -4
  55. package/dist/node-claim-payload.js.map +1 -1
  56. package/dist/node-default-transports.d.ts +27 -0
  57. package/dist/node-default-transports.js +35 -0
  58. package/dist/node-default-transports.js.map +1 -0
  59. package/dist/node-transports.d.ts +30 -0
  60. package/dist/node-transports.js +40 -0
  61. package/dist/node-transports.js.map +1 -0
  62. package/dist/peer-dial.d.ts +124 -27
  63. package/dist/peer-dial.js +254 -30
  64. package/dist/peer-dial.js.map +1 -1
  65. package/dist/peer-join-backfill.js +6 -0
  66. package/dist/peer-join-backfill.js.map +1 -1
  67. package/dist/peer-record.d.ts +6 -45
  68. package/dist/peer-record.js +8 -50
  69. package/dist/peer-record.js.map +1 -1
  70. package/dist/primary-lan-address.d.ts +8 -0
  71. package/dist/primary-lan-address.js +46 -0
  72. package/dist/primary-lan-address.js.map +1 -0
  73. package/dist/provider/client.d.ts +131 -0
  74. package/dist/provider/client.js +226 -0
  75. package/dist/provider/client.js.map +1 -0
  76. package/dist/provider/descriptor.d.ts +41 -0
  77. package/dist/provider/descriptor.js +103 -0
  78. package/dist/provider/descriptor.js.map +1 -0
  79. package/dist/provider/index.d.ts +9 -0
  80. package/dist/provider/index.js +10 -0
  81. package/dist/provider/index.js.map +1 -0
  82. package/dist/provider/oauth.d.ts +125 -0
  83. package/dist/provider/oauth.js +199 -0
  84. package/dist/provider/oauth.js.map +1 -0
  85. package/dist/provider/rent-node.d.ts +101 -0
  86. package/dist/provider/rent-node.js +220 -0
  87. package/dist/provider/rent-node.js.map +1 -0
  88. package/dist/relay-addrs.d.ts +9 -4
  89. package/dist/relay-addrs.js +9 -4
  90. package/dist/relay-addrs.js.map +1 -1
  91. package/dist/relay-reservation.d.ts +51 -16
  92. package/dist/relay-reservation.js +107 -24
  93. package/dist/relay-reservation.js.map +1 -1
  94. package/dist/seed-bootstrap.d.ts +22 -5
  95. package/dist/seed-bootstrap.js +48 -25
  96. package/dist/seed-bootstrap.js.map +1 -1
  97. package/dist/strand-commit-durability.d.ts +54 -0
  98. package/dist/strand-commit-durability.js +74 -0
  99. package/dist/strand-commit-durability.js.map +1 -0
  100. package/dist/strand-database.d.ts +23 -8
  101. package/dist/strand-database.js +41 -13
  102. package/dist/strand-database.js.map +1 -1
  103. package/dist/strand-first-sync-gate.d.ts +7 -1
  104. package/dist/strand-first-sync-gate.js +7 -1
  105. package/dist/strand-first-sync-gate.js.map +1 -1
  106. package/dist/strand-formation-protocol.js +4 -1
  107. package/dist/strand-formation-protocol.js.map +1 -1
  108. package/dist/strand-fret-addrs.d.ts +19 -1
  109. package/dist/strand-fret-addrs.js +27 -9
  110. package/dist/strand-fret-addrs.js.map +1 -1
  111. package/dist/strand-instance-manager.d.ts +81 -19
  112. package/dist/strand-instance-manager.js +216 -105
  113. package/dist/strand-instance-manager.js.map +1 -1
  114. package/dist/strand-move.d.ts +124 -0
  115. package/dist/strand-move.js +438 -0
  116. package/dist/strand-move.js.map +1 -0
  117. package/dist/strand-network-config.js +7 -6
  118. package/dist/strand-network-config.js.map +1 -1
  119. package/dist/strand-peer-redial.d.ts +113 -0
  120. package/dist/strand-peer-redial.js +236 -0
  121. package/dist/strand-peer-redial.js.map +1 -0
  122. package/dist/strand-wake-protocol.js +10 -7
  123. package/dist/strand-wake-protocol.js.map +1 -1
  124. package/dist/types.d.ts +212 -4
  125. package/dist/types.js +10 -0
  126. package/dist/types.js.map +1 -1
  127. package/package.json +18 -10
  128. package/src/cadre-invite-protocol.ts +1052 -864
  129. package/src/cadre-join-request.ts +167 -0
  130. package/src/cadre-node.ts +10003 -9381
  131. package/src/control-database.ts +4663 -4451
  132. package/src/control-schema.ts +19 -3
  133. package/src/ed25519-key.ts +20 -10
  134. package/src/fetch-impl.ts +23 -0
  135. package/src/file-durable-slot.ts +64 -63
  136. package/src/file-safe-name.ts +32 -0
  137. package/src/formation-approval.ts +604 -610
  138. package/src/fs-atomic.ts +92 -117
  139. package/src/index.ts +71 -4
  140. package/src/invitation-bootstrap.ts +91 -13
  141. package/src/joined-strand-store.ts +508 -498
  142. package/src/key-store-file.ts +142 -141
  143. package/src/link-budget.ts +9 -4
  144. package/src/machine-removal.ts +216 -0
  145. package/src/membership-connection-gater.ts +638 -637
  146. package/src/node-claim-payload.ts +138 -4
  147. package/src/node-default-transports.ts +38 -0
  148. package/src/node-transports.ts +49 -0
  149. package/src/peer-dial.ts +604 -336
  150. package/src/peer-join-backfill.ts +6 -0
  151. package/src/peer-record.ts +159 -202
  152. package/src/primary-lan-address.ts +47 -0
  153. package/src/provider/client.ts +326 -0
  154. package/src/provider/descriptor.ts +129 -0
  155. package/src/provider/index.ts +59 -0
  156. package/src/provider/oauth.ts +285 -0
  157. package/src/provider/rent-node.ts +275 -0
  158. package/src/relay-addrs.ts +9 -4
  159. package/src/relay-reservation.ts +1211 -1121
  160. package/src/seed-bootstrap.ts +1502 -1471
  161. package/src/strand-commit-durability.ts +116 -0
  162. package/src/strand-database.ts +46 -14
  163. package/src/strand-first-sync-gate.ts +7 -1
  164. package/src/strand-formation-protocol.ts +1039 -1036
  165. package/src/strand-fret-addrs.ts +179 -145
  166. package/src/strand-instance-manager.ts +2026 -1883
  167. package/src/strand-move.ts +585 -0
  168. package/src/strand-network-config.ts +7 -6
  169. package/src/strand-peer-redial.ts +307 -0
  170. package/src/strand-wake-protocol.ts +10 -7
  171. package/src/types.ts +220 -4
@@ -0,0 +1,167 @@
1
+ /**
2
+ * The join request: the text an app that wants to join a cadre copies (or shows as a QR code)
3
+ * so the app that manages the cadre can mint an invitation for it alone
4
+ * (`CadreNode.createCadreInvitation({ peerId: request.peerId, grantsOwner })`).
5
+ *
6
+ * `sereus-join-request:1.<base64url of canonical JSON>` where the JSON is
7
+ * `{ appName, peerId, reply?, v }`. Nothing in it is secret or signed: the peer id is public, and
8
+ * the name is only what the requester calls itself, shown in the owner's approval prompt. The
9
+ * safety is in the response: an invitation targeted at that peer id can be redeemed only by
10
+ * the key behind it.
11
+ *
12
+ * `reply` is present only on a request opened as a link from another app on the same phone
13
+ * (`@serfab/cadre-rn/app-link`): the owner app answers by opening `<reply.scheme>://…`, a link it
14
+ * builds itself, so the scheme is restricted to an app's own custom scheme and never a web or
15
+ * system one, which would send the invitation's machine addresses off the phone.
16
+ *
17
+ * Imports only `@libp2p/peer-id` and `uint8arrays`, so it loads on React Native and in the
18
+ * browser.
19
+ */
20
+ import { peerIdFromString } from '@libp2p/peer-id';
21
+ import { fromString as uint8ArrayFromString, toString as uint8ArrayToString } from 'uint8arrays';
22
+ import { canonicalJson } from './canonical-json.js';
23
+
24
+ /** Version 1 of the join request; the only one either side reads. */
25
+ export const CADRE_JOIN_REQUEST_PREFIX = 'sereus-join-request:1.';
26
+
27
+ /**
28
+ * Version 1 of an encoded cadre invitation (`encodeCadreInvitation`). Declared here, beside
29
+ * the request's prefix, so each decoder can name the other kind of text when it is pasted in
30
+ * the wrong place without this codec importing the redemption protocol.
31
+ */
32
+ export const CADRE_INVITATION_PREFIX = 'sereus-invite:1.';
33
+
34
+ /** Longest `appName` a request carries: one line in an approval prompt. */
35
+ export const CADRE_JOIN_REQUEST_MAX_APP_NAME = 64;
36
+
37
+ /**
38
+ * Schemes a reply may not name, though they match {@link CADRE_JOIN_REPLY_SCHEME}: each opens a
39
+ * web page, a file, a system action or another app's content rather than a Sereus app.
40
+ */
41
+ const REFUSED_REPLY_SCHEMES: ReadonlySet<string> = new Set([
42
+ 'http', 'https', 'ws', 'wss', 'ftp', 'file', 'content', 'data', 'blob',
43
+ 'javascript', 'about', 'intent', 'mailto', 'tel', 'sms',
44
+ ]);
45
+
46
+ /**
47
+ * The form of a reply's scheme: lowercase, as an app's custom URL scheme is declared.
48
+ * NOTE: `@serfab/cadre-rn`'s CommonJS config plugin keeps a copy (`APP_SCHEME`) to check extra
49
+ * apps' schemes; change it with this one.
50
+ */
51
+ const CADRE_JOIN_REPLY_SCHEME = /^[a-z][a-z0-9+.-]{0,63}$/;
52
+
53
+ /** The form of a reply's nonce: base64url, long enough that it cannot be guessed. */
54
+ const CADRE_JOIN_REPLY_NONCE = /^[A-Za-z0-9_-]{16,64}$/;
55
+
56
+ /** Where and how the owner app answers a request that arrived as a link. */
57
+ export interface CadreJoinReply {
58
+ /** The requesting app's own custom URL scheme; the owner app opens `<scheme>://cadre-join-response/<response>`. */
59
+ scheme: string;
60
+ /** Echoed in the response, so the requesting app takes only an answer to a request it has open. */
61
+ nonce: string;
62
+ }
63
+
64
+ export interface CadreJoinRequest {
65
+ v: 1;
66
+ /** The requesting node's peer id: an Ed25519 peer id, since the invitation targets that key. */
67
+ peerId: string;
68
+ /** What the requester calls itself, for the owner's approval prompt. Unverified; at most 64 characters, trimmed, non-empty. */
69
+ appName: string;
70
+ /** Present only on a request opened as a link: where and how the owner app answers. */
71
+ reply?: CadreJoinReply;
72
+ }
73
+
74
+ /** Encode a request as the text the requesting app copies. Refuses what {@link decodeCadreJoinRequest} would. */
75
+ export function encodeCadreJoinRequest(request: CadreJoinRequest): string {
76
+ const checked = requireCadreJoinRequest(request);
77
+ // canonicalJson drops an undefined `reply`, so a request without one encodes as it always has.
78
+ const json = canonicalJson({ appName: checked.appName, peerId: checked.peerId, reply: checked.reply, v: checked.v });
79
+ return `${CADRE_JOIN_REQUEST_PREFIX}${uint8ArrayToString(new TextEncoder().encode(json), 'base64url')}`;
80
+ }
81
+
82
+ /**
83
+ * Decode the text the owner's app was given. Refuses, naming the problem: an invitation
84
+ * pasted here, any prefix but version 1's, a body that is not base64url JSON of the request's
85
+ * shape, a peer id that does not parse or is not Ed25519, an `appName` that is empty or
86
+ * longer than {@link CADRE_JOIN_REQUEST_MAX_APP_NAME} characters once trimmed, and a `reply`
87
+ * that {@link requireCadreJoinReply} refuses.
88
+ */
89
+ export function decodeCadreJoinRequest(text: string): CadreJoinRequest {
90
+ const trimmed = text.trim();
91
+ if (trimmed.startsWith(CADRE_INVITATION_PREFIX)) {
92
+ throw new Error('This is a cadre invitation, not a join request; paste it where the app asks for an invitation');
93
+ }
94
+ if (!trimmed.startsWith(CADRE_JOIN_REQUEST_PREFIX)) {
95
+ throw new Error(`A join request must start with "${CADRE_JOIN_REQUEST_PREFIX}" (this one starts with "${trimmed.slice(0, CADRE_JOIN_REQUEST_PREFIX.length)}")`);
96
+ }
97
+ let parsed: unknown;
98
+ try {
99
+ parsed = JSON.parse(new TextDecoder().decode(uint8ArrayFromString(trimmed.slice(CADRE_JOIN_REQUEST_PREFIX.length), 'base64url')));
100
+ } catch (error) {
101
+ throw new Error('A join request must carry base64url-encoded JSON after its prefix', { cause: error });
102
+ }
103
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
104
+ throw new Error('A join request must decode to a JSON object');
105
+ }
106
+ const { v, peerId, appName, reply } = parsed as Record<string, unknown>;
107
+ if (v !== 1) throw new Error(`A join request must be version 1 (this one says ${JSON.stringify(v)})`);
108
+ if (typeof peerId !== 'string') throw new Error('A join request must name the requesting node\'s peer id');
109
+ if (typeof appName !== 'string') throw new Error('A join request must carry the requesting app\'s name');
110
+ return requireCadreJoinRequest({ v, peerId, appName, ...(reply === undefined ? {} : { reply: replyShape(reply) }) });
111
+ }
112
+
113
+ /**
114
+ * Refuses, naming the problem, a reply whose scheme is not a lowercase URL scheme of at most 64
115
+ * characters or is a web or system scheme (`https`, `file`, `intent`, …), and a nonce that is
116
+ * not 16 to 64 base64url characters. The owner app checks it again before it opens a reply.
117
+ */
118
+ export function requireCadreJoinReply(reply: CadreJoinReply): CadreJoinReply {
119
+ const { scheme, nonce } = reply;
120
+ if (!CADRE_JOIN_REPLY_SCHEME.test(scheme)) {
121
+ throw new Error(`A join request's reply scheme must be a lowercase URL scheme of at most 64 characters (this one is ${JSON.stringify(scheme)})`);
122
+ }
123
+ if (REFUSED_REPLY_SCHEMES.has(scheme)) {
124
+ throw new Error(`A join request's reply scheme must be an app's own scheme, not "${scheme}"`);
125
+ }
126
+ if (!CADRE_JOIN_REPLY_NONCE.test(nonce)) {
127
+ throw new Error('A join request\'s reply nonce must be 16 to 64 base64url characters');
128
+ }
129
+ return { scheme, nonce };
130
+ }
131
+
132
+ /** The decoded `reply`'s types, before {@link requireCadreJoinReply} judges its values. */
133
+ function replyShape(reply: unknown): CadreJoinReply {
134
+ if (typeof reply !== 'object' || reply === null || Array.isArray(reply)) {
135
+ throw new Error('A join request\'s reply must be an object');
136
+ }
137
+ const { scheme, nonce } = reply as Record<string, unknown>;
138
+ if (typeof scheme !== 'string' || typeof nonce !== 'string') {
139
+ throw new Error('A join request\'s reply must carry a scheme and a nonce');
140
+ }
141
+ return { scheme, nonce };
142
+ }
143
+
144
+ /** The shape rules both directions apply. Returns the request with its fields trimmed. */
145
+ function requireCadreJoinRequest(request: CadreJoinRequest): CadreJoinRequest {
146
+ const peerId = requireEd25519PeerId(request.peerId);
147
+ const appName = request.appName.trim();
148
+ if (appName.length === 0) throw new Error('A join request must name the requesting app');
149
+ if (appName.length > CADRE_JOIN_REQUEST_MAX_APP_NAME) {
150
+ throw new Error(`A join request's app name must be at most ${CADRE_JOIN_REQUEST_MAX_APP_NAME} characters (this one has ${appName.length})`);
151
+ }
152
+ return { v: 1, peerId, appName, ...(request.reply ? { reply: requireCadreJoinReply(request.reply) } : {}) };
153
+ }
154
+
155
+ /** An invitation targeted at the peer id names its key, so only an Ed25519 id could ever redeem one. */
156
+ function requireEd25519PeerId(text: string): string {
157
+ let parsed: ReturnType<typeof peerIdFromString>;
158
+ try {
159
+ parsed = peerIdFromString(text.trim());
160
+ } catch (error) {
161
+ throw new Error(`A join request's peer id does not parse ("${text}")`, { cause: error });
162
+ }
163
+ if (parsed.type !== 'Ed25519') {
164
+ throw new Error(`A join request's peer id must be an Ed25519 peer id (this one is ${parsed.type})`);
165
+ }
166
+ return parsed.toString();
167
+ }