@calimero-network/mero-js 26.11.0 → 27.0.1

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 (220) hide show
  1. package/README.md +73 -288
  2. package/dist/cloud-v2/attestation.d.ts +91 -0
  3. package/dist/cloud-v2/attestation.js +135 -0
  4. package/dist/cloud-v2/capabilities.d.ts +30 -0
  5. package/dist/cloud-v2/capabilities.js +31 -0
  6. package/dist/cloud-v2/client.d.ts +426 -0
  7. package/dist/cloud-v2/client.js +646 -0
  8. package/dist/cloud-v2/errors.d.ts +98 -0
  9. package/dist/cloud-v2/errors.js +165 -0
  10. package/dist/cloud-v2/index.d.ts +10 -0
  11. package/dist/cloud-v2/index.js +5 -0
  12. package/dist/cloud-v2/passkey.d.ts +27 -0
  13. package/dist/cloud-v2/passkey.js +73 -0
  14. package/dist/crypto/internal.d.ts +3 -37
  15. package/dist/crypto/internal.js +3 -72
  16. package/dist/index.browser.mjs +4 -2
  17. package/dist/index.browser.mjs.map +4 -4
  18. package/dist/index.cjs +1750 -12772
  19. package/dist/index.cjs.map +4 -4
  20. package/dist/index.d.ts +3 -56
  21. package/dist/index.js +14 -75
  22. package/dist/index.mjs +1750 -12772
  23. package/dist/index.mjs.map +4 -4
  24. package/dist/sealed/index.d.ts +4 -4
  25. package/dist/sealed/index.js +2 -2
  26. package/dist/sealed/release.d.ts +21 -76
  27. package/dist/sealed/release.js +24 -108
  28. package/dist/sealed/sealed.d.ts +8 -56
  29. package/dist/sealed/sealed.js +11 -89
  30. package/dist/sealed/verify.d.ts +7 -13
  31. package/dist/sealed/verify.js +3 -7
  32. package/package.json +8 -13
  33. package/dist/account/account-admin.d.ts +0 -80
  34. package/dist/account/account-admin.js +0 -346
  35. package/dist/account/account.d.ts +0 -196
  36. package/dist/account/account.js +0 -269
  37. package/dist/account/application-id.d.ts +0 -50
  38. package/dist/account/application-id.js +0 -83
  39. package/dist/account/bootstrap-from-invitation.d.ts +0 -110
  40. package/dist/account/bootstrap-from-invitation.js +0 -80
  41. package/dist/account/create-context.d.ts +0 -103
  42. package/dist/account/create-context.js +0 -267
  43. package/dist/account/govern.d.ts +0 -24
  44. package/dist/account/govern.js +0 -50
  45. package/dist/account/index.d.ts +0 -26
  46. package/dist/account/index.js +0 -15
  47. package/dist/account/join-as-account.d.ts +0 -23
  48. package/dist/account/join-as-account.js +0 -34
  49. package/dist/account/join-with-node.d.ts +0 -115
  50. package/dist/account/join-with-node.js +0 -161
  51. package/dist/account/node-admin.d.ts +0 -20
  52. package/dist/account/node-admin.js +0 -33
  53. package/dist/account/relay-from-invitation.d.ts +0 -137
  54. package/dist/account/relay-from-invitation.js +0 -180
  55. package/dist/account/relay-login-error.d.ts +0 -32
  56. package/dist/account/relay-login-error.js +0 -34
  57. package/dist/account/session.d.ts +0 -266
  58. package/dist/account/session.js +0 -915
  59. package/dist/account/wordlist.d.ts +0 -16
  60. package/dist/account/wordlist.js +0 -273
  61. package/dist/admin-api/admin-client.d.ts +0 -688
  62. package/dist/admin-api/admin-client.js +0 -1230
  63. package/dist/admin-api/admin-factory.d.ts +0 -14
  64. package/dist/admin-api/admin-factory.js +0 -22
  65. package/dist/admin-api/admin-types.d.ts +0 -2046
  66. package/dist/admin-api/admin-types.js +0 -12
  67. package/dist/admin-api/index.d.ts +0 -4
  68. package/dist/admin-api/index.js +0 -5
  69. package/dist/admin-api/redeem-invitation.d.ts +0 -101
  70. package/dist/admin-api/redeem-invitation.js +0 -200
  71. package/dist/auth/index.d.ts +0 -34
  72. package/dist/auth/index.js +0 -67
  73. package/dist/auth-api/auth-client.d.ts +0 -31
  74. package/dist/auth-api/auth-client.js +0 -117
  75. package/dist/auth-api/auth-factory.d.ts +0 -7
  76. package/dist/auth-api/auth-factory.js +0 -41
  77. package/dist/auth-api/auth-types.d.ts +0 -130
  78. package/dist/auth-api/auth-types.js +0 -2
  79. package/dist/auth-api/index.d.ts +0 -3
  80. package/dist/auth-api/index.js +0 -4
  81. package/dist/capabilities.d.ts +0 -84
  82. package/dist/capabilities.js +0 -117
  83. package/dist/cloud/account-ownership.d.ts +0 -63
  84. package/dist/cloud/account-ownership.js +0 -87
  85. package/dist/cloud/cloud-client.d.ts +0 -949
  86. package/dist/cloud/cloud-client.js +0 -1068
  87. package/dist/cloud/connect.d.ts +0 -213
  88. package/dist/cloud/connect.js +0 -260
  89. package/dist/cloud/enrol-redirect.d.ts +0 -181
  90. package/dist/cloud/enrol-redirect.js +0 -244
  91. package/dist/cloud/index.d.ts +0 -12
  92. package/dist/cloud/index.js +0 -10
  93. package/dist/cloud/link-redirect.d.ts +0 -95
  94. package/dist/cloud/link-redirect.js +0 -121
  95. package/dist/cloud/link-state.d.ts +0 -36
  96. package/dist/cloud/link-state.js +0 -104
  97. package/dist/cloud/routing-proof.d.ts +0 -113
  98. package/dist/cloud/routing-proof.js +0 -96
  99. package/dist/device-cert/device-cert.d.ts +0 -189
  100. package/dist/device-cert/device-cert.js +0 -239
  101. package/dist/device-cert/device-scope.d.ts +0 -58
  102. package/dist/device-cert/device-scope.js +0 -76
  103. package/dist/device-cert/index.d.ts +0 -4
  104. package/dist/device-cert/index.js +0 -3
  105. package/dist/ephemeral/codec.d.ts +0 -4
  106. package/dist/ephemeral/codec.js +0 -12
  107. package/dist/ephemeral/index.d.ts +0 -57
  108. package/dist/ephemeral/index.js +0 -101
  109. package/dist/ephemeral/types.d.ts +0 -27
  110. package/dist/ephemeral/types.js +0 -1
  111. package/dist/errors/classify.d.ts +0 -65
  112. package/dist/errors/classify.js +0 -117
  113. package/dist/errors/index.d.ts +0 -2
  114. package/dist/errors/index.js +0 -1
  115. package/dist/events/group.d.ts +0 -95
  116. package/dist/events/group.js +0 -10
  117. package/dist/events/index.d.ts +0 -5
  118. package/dist/events/index.js +0 -2
  119. package/dist/events/sse.d.ts +0 -127
  120. package/dist/events/sse.js +0 -469
  121. package/dist/events/ws.d.ts +0 -57
  122. package/dist/events/ws.js +0 -212
  123. package/dist/http-client/api-response.d.ts +0 -15
  124. package/dist/http-client/api-response.js +0 -1
  125. package/dist/http-client/http-factory.d.ts +0 -71
  126. package/dist/http-client/http-factory.js +0 -68
  127. package/dist/http-client/http-types.d.ts +0 -87
  128. package/dist/http-client/http-types.js +0 -1
  129. package/dist/http-client/index.d.ts +0 -7
  130. package/dist/http-client/index.js +0 -11
  131. package/dist/http-client/path-segment.d.ts +0 -1
  132. package/dist/http-client/path-segment.js +0 -8
  133. package/dist/http-client/retry.d.ts +0 -5
  134. package/dist/http-client/retry.js +0 -82
  135. package/dist/http-client/signal-utils.d.ts +0 -2
  136. package/dist/http-client/signal-utils.js +0 -41
  137. package/dist/http-client/web-client.d.ts +0 -80
  138. package/dist/http-client/web-client.js +0 -557
  139. package/dist/invitation/index.d.ts +0 -2
  140. package/dist/invitation/index.js +0 -2
  141. package/dist/invitation/invitation.d.ts +0 -106
  142. package/dist/invitation/invitation.js +0 -104
  143. package/dist/login/index.d.ts +0 -4
  144. package/dist/login/index.js +0 -3
  145. package/dist/login/login.d.ts +0 -111
  146. package/dist/login/login.js +0 -110
  147. package/dist/login/session.d.ts +0 -124
  148. package/dist/login/session.js +0 -141
  149. package/dist/mero-js.d.ts +0 -231
  150. package/dist/mero-js.js +0 -469
  151. package/dist/namespace-op/index.d.ts +0 -2
  152. package/dist/namespace-op/index.js +0 -3
  153. package/dist/namespace-op/namespace-op.d.ts +0 -150
  154. package/dist/namespace-op/namespace-op.js +0 -191
  155. package/dist/nodeDiscovery.d.ts +0 -47
  156. package/dist/nodeDiscovery.js +0 -101
  157. package/dist/owner-op/index.d.ts +0 -2
  158. package/dist/owner-op/index.js +0 -3
  159. package/dist/owner-op/owner-op.d.ts +0 -119
  160. package/dist/owner-op/owner-op.js +0 -214
  161. package/dist/presence/relay-presence.d.ts +0 -56
  162. package/dist/presence/relay-presence.js +0 -107
  163. package/dist/presence/statement.d.ts +0 -12
  164. package/dist/presence/statement.js +0 -17
  165. package/dist/relay/index.d.ts +0 -4
  166. package/dist/relay/index.js +0 -2
  167. package/dist/relay/nonce-source.d.ts +0 -72
  168. package/dist/relay/nonce-source.js +0 -111
  169. package/dist/relay/relay-client.d.ts +0 -472
  170. package/dist/relay/relay-client.js +0 -518
  171. package/dist/relay-attestation/attest-node-key.d.ts +0 -66
  172. package/dist/relay-attestation/attest-node-key.js +0 -110
  173. package/dist/relay-attestation/index.d.ts +0 -1
  174. package/dist/relay-attestation/index.js +0 -1
  175. package/dist/request/caller-proof.d.ts +0 -43
  176. package/dist/request/caller-proof.js +0 -36
  177. package/dist/request/index.d.ts +0 -3
  178. package/dist/request/index.js +0 -3
  179. package/dist/request/proof-signer.d.ts +0 -49
  180. package/dist/request/proof-signer.js +0 -59
  181. package/dist/request/request-sig.d.ts +0 -99
  182. package/dist/request/request-sig.js +0 -95
  183. package/dist/request-proof/authorizer.d.ts +0 -41
  184. package/dist/request-proof/authorizer.js +0 -60
  185. package/dist/request-proof/index.d.ts +0 -4
  186. package/dist/request-proof/index.js +0 -2
  187. package/dist/request-proof/request-proof.d.ts +0 -53
  188. package/dist/request-proof/request-proof.js +0 -95
  189. package/dist/rpc/index.d.ts +0 -64
  190. package/dist/rpc/index.js +0 -78
  191. package/dist/signer/index.d.ts +0 -2
  192. package/dist/signer/index.js +0 -3
  193. package/dist/signer/signer.d.ts +0 -92
  194. package/dist/signer/signer.js +0 -147
  195. package/dist/token-store/index.d.ts +0 -27
  196. package/dist/token-store/index.js +0 -62
  197. package/dist/transport/index.d.ts +0 -6
  198. package/dist/transport/index.js +0 -3
  199. package/dist/transport/mero-client.d.ts +0 -294
  200. package/dist/transport/mero-client.js +0 -386
  201. package/dist/transport/relay-observer.d.ts +0 -152
  202. package/dist/transport/relay-observer.js +0 -173
  203. package/dist/transport/relay-transport.d.ts +0 -73
  204. package/dist/transport/relay-transport.js +0 -94
  205. package/dist/transport/types.d.ts +0 -100
  206. package/dist/transport/types.js +0 -32
  207. package/dist/types/api-response.d.ts +0 -15
  208. package/dist/types/api-response.js +0 -1
  209. package/dist/types/index.d.ts +0 -1
  210. package/dist/types/index.js +0 -1
  211. package/dist/warrant/creation-warrant.d.ts +0 -163
  212. package/dist/warrant/creation-warrant.js +0 -194
  213. package/dist/warrant/governance-op.d.ts +0 -202
  214. package/dist/warrant/governance-op.js +0 -355
  215. package/dist/warrant/governance-warrant.d.ts +0 -124
  216. package/dist/warrant/governance-warrant.js +0 -164
  217. package/dist/warrant/index.d.ts +0 -8
  218. package/dist/warrant/index.js +0 -5
  219. package/dist/warrant/warrant.d.ts +0 -188
  220. package/dist/warrant/warrant.js +0 -202
@@ -1,214 +0,0 @@
1
- /**
2
- * Root proofs for owner-level governance ops (core schema 18).
3
- *
4
- * A device key speaks for its account everywhere in governance, so a stolen
5
- * device used to be enough to take a group from its owner: promote an
6
- * accomplice, transfer ownership, remove the owner. Core now refuses the ops
7
- * that can do that unless they carry an authorisation signed by the account's
8
- * **root** key, which a device does not hold:
9
- *
10
- * - `GroupOp::TransferOwnership`, `GroupOp::GroupDelete` and
11
- * `RootOp::AdminChanged`, which are owner-only;
12
- * - the TEE admission, release-admission and authoring policies, which stay
13
- * admin-level but need the signing admin's own root.
14
- *
15
- * A node that holds the account root (as `merod init` provisions) signs the
16
- * proof itself, so a caller on such a node passes nothing. This module is for
17
- * the other case: a nodeless account, or a root kept cold, signing offline and
18
- * passing the hex proof as `rootProof`.
19
- *
20
- * What the root signs binds the account, the namespace, the group, the kind of
21
- * op, a digest of the op's own borsh bytes, the root epoch, and the group's
22
- * guarded-op counter. Read that counter (`ownerOpCounter`, with `namespaceId`)
23
- * from `getGroupInfo` first: the node applies a proof only while the group is
24
- * still at that count, and advances it once the op applies, so each proof is
25
- * single-use. For `AdminChanged` and the TEE policies the group is the
26
- * namespace root.
27
- *
28
- * Byte layouts follow core's `calimero_account::OwnerOpAuthorization` and
29
- * `calimero_governance_types::{GroupOp, RootOp}`. They are pinned against core's
30
- * own vectors (`crates/account/src/tests/owner_op.rs`,
31
- * `root_guarded_vectors_are_stable` in `crates/governance-types/src/tests.rs`)
32
- * in `owner-op.test.ts`.
33
- */
34
- import { concat, derivePublicKey, domainHash, fromHex, fromHexUnsized, hex, importSigningKey, u32le, u64le, } from '../crypto/internal.js';
35
- /** core's `OWNER_OP_SIGN_DOMAIN`: what the root signs under. */
36
- const OWNER_OP_SIGN_DOMAIN = new TextEncoder().encode('calimero.account.owner-op.v1');
37
- /** core's `OWNER_OP_BODY_DOMAIN`: what the op's bytes are hashed under. */
38
- const OWNER_OP_BODY_DOMAIN = new TextEncoder().encode('calimero.account.owner-op.body.v1');
39
- /** core's `ACCOUNT_ID_DOMAIN`. */
40
- const ACCOUNT_ID_DOMAIN = new TextEncoder().encode('calimero.account.genesis.v1');
41
- /** core's `ACCOUNT_GENESIS_VERSION`. */
42
- const ACCOUNT_GENESIS_VERSION = 2;
43
- /**
44
- * Which owner-level op a proof authorises. The value is the byte core signs,
45
- * its `OwnerOpKind` discriminant.
46
- */
47
- export const OWNER_OP_KIND = {
48
- transferOwnership: 0,
49
- adminChanged: 1,
50
- groupDelete: 2,
51
- teeAdmissionPolicy: 3,
52
- teeAuthoringPolicy: 4,
53
- teeReleaseAdmissionPolicy: 5,
54
- };
55
- /** `GroupOp` discriminants, by position in core's enum. */
56
- const GROUP_OP = {
57
- GroupDelete: 14,
58
- TransferOwnership: 21,
59
- TeeAuthoringPolicySet: 33,
60
- TeeAdmissionPolicySetV2: 37,
61
- TeeReleaseAdmissionPolicySetV2: 38,
62
- RootGuarded: 42,
63
- };
64
- /** `RootOp` discriminants, by position in core's enum. */
65
- const ROOT_OP = {
66
- AdminChanged: 3,
67
- RootGuarded: 12,
68
- };
69
- /** core's `TeeAdmissionMode` discriminants. */
70
- const TEE_MODE = { replica: 0, relay: 1 };
71
- const te = new TextEncoder();
72
- function tag(byte) {
73
- return new Uint8Array([byte]);
74
- }
75
- function borshString(value) {
76
- const bytes = te.encode(value);
77
- return concat(u32le(bytes.length), bytes);
78
- }
79
- function borshStrings(values) {
80
- return concat(u32le(values.length), ...values.map(borshString));
81
- }
82
- function borshBool(value) {
83
- return tag(value ? 1 : 0);
84
- }
85
- function teeMode(mode) {
86
- const byte = TEE_MODE[mode ?? 'replica'];
87
- if (byte === undefined) {
88
- throw new Error(`mode must be replica or relay, got ${String(mode)}`);
89
- }
90
- return tag(byte);
91
- }
92
- /** Hand the group to `newOwner` (an account, hex), who must already be an admin. */
93
- export function transferOwnershipOp(newOwner) {
94
- return {
95
- kind: 'transferOwnership',
96
- plane: 'group',
97
- bytes: concat(tag(GROUP_OP.TransferOwnership), fromHex(newOwner, 'newOwner', 32)),
98
- };
99
- }
100
- /** The owner-only deletion of a group that holds no contexts. */
101
- export function groupDeleteOp() {
102
- return { kind: 'groupDelete', plane: 'group', bytes: tag(GROUP_OP.GroupDelete) };
103
- }
104
- /** Repoint the namespace's admin pin at `newAdmin` (an account, hex). A root op. */
105
- export function adminChangedOp(newAdmin) {
106
- return {
107
- kind: 'adminChanged',
108
- plane: 'root',
109
- bytes: concat(tag(ROOT_OP.AdminChanged), fromHex(newAdmin, 'newAdmin', 32)),
110
- };
111
- }
112
- /** The TEE authoring policy `PUT`/`DELETE …/settings/tee-authoring-policy` publishes. */
113
- export function teeAuthoringPolicyOp(allowedMrtd) {
114
- return {
115
- kind: 'teeAuthoringPolicy',
116
- plane: 'group',
117
- bytes: concat(tag(GROUP_OP.TeeAuthoringPolicySet), borshStrings(allowedMrtd)),
118
- };
119
- }
120
- /**
121
- * The measurement-list admission policy `PUT …/settings/tee-admission-policy`
122
- * publishes. Pass the fields exactly as you send them: the node signs over the
123
- * op it builds from them, and any difference is a different op.
124
- */
125
- export function teeAdmissionPolicyOp(input) {
126
- return {
127
- kind: 'teeAdmissionPolicy',
128
- plane: 'group',
129
- bytes: concat(tag(GROUP_OP.TeeAdmissionPolicySetV2), borshStrings(input.allowedMrtd), borshStrings(input.allowedRtmr0), borshStrings(input.allowedRtmr1), borshStrings(input.allowedRtmr2), borshStrings(input.allowedRtmr3), borshStrings(input.allowedTcbStatuses), borshBool(input.acceptMock), teeMode(input.mode)),
130
- };
131
- }
132
- /** The signed-release admission policy `PUT …/settings/tee-admission-policy` publishes. */
133
- export function teeReleaseAdmissionPolicyOp(input) {
134
- const minRelease = input.minReleaseVersion === undefined
135
- ? tag(0)
136
- : concat(tag(1), borshString(input.minReleaseVersion));
137
- return {
138
- kind: 'teeReleaseAdmissionPolicy',
139
- plane: 'group',
140
- bytes: concat(tag(GROUP_OP.TeeReleaseAdmissionPolicySetV2), borshStrings(input.allowedProfiles), minRelease, borshStrings(input.allowedTcbStatuses), borshBool(input.acceptMock), teeMode(input.mode)),
141
- };
142
- }
143
- /** core's `OwnerOpAuthorization::op_digest`: the op's bytes, hashed under the body domain. */
144
- export async function ownerOpDigest(opBytes) {
145
- return domainHash(OWNER_OP_BODY_DOMAIN, [opBytes]);
146
- }
147
- /** core's `OwnerOpTerms::signing_payload`. */
148
- export async function ownerOpSigningPayload(input) {
149
- return domainHash(OWNER_OP_SIGN_DOMAIN, [
150
- fromHex(input.account, 'account', 32),
151
- fromHex(input.namespaceId, 'namespaceId', 32),
152
- fromHex(input.groupId, 'groupId', 32),
153
- tag(kindByte(input.kind)),
154
- input.opDigest,
155
- u64le(input.counter),
156
- u32le(input.keyEpoch),
157
- ]);
158
- }
159
- function kindByte(kind) {
160
- const byte = OWNER_OP_KIND[kind];
161
- if (byte === undefined) {
162
- throw new Error(`unknown owner-op kind ${String(kind)}`);
163
- }
164
- return byte;
165
- }
166
- const HANDOFF_BYTES = 32 + 4 + 32 + 64;
167
- /**
168
- * Sign a root proof for `op` and return it as core's `rootProof`: the hex
169
- * borsh of an `AccountProof<OwnerOpAuthorization>`.
170
- *
171
- * Layout: `AccountGenesis` (version u8 + root pk 32), the handoff chain (u32
172
- * count + 132 bytes each), then `OwnerOpAuthorization` (account 32, namespace
173
- * 32, group 32, kind u8, op digest 32, counter u64, key epoch u32, signature
174
- * 64).
175
- */
176
- export async function signOwnerOpProof(input) {
177
- const rootPublicKey = await derivePublicKey(input.rootSecret);
178
- const genesis = concat(tag(ACCOUNT_GENESIS_VERSION), rootPublicKey);
179
- const account = hex(await domainHash(ACCOUNT_ID_DOMAIN, [genesis]));
180
- const keyEpoch = input.keyEpoch ?? 0;
181
- const chain = (input.chain ?? []).map((handoff, index) => {
182
- const bytes = fromHexUnsized(handoff, `chain[${index}]`);
183
- if (bytes.length !== HANDOFF_BYTES) {
184
- throw new Error(`chain[${index}] must be a ${HANDOFF_BYTES}-byte RootKeyHandoff, got ${bytes.length}`);
185
- }
186
- return bytes;
187
- });
188
- if (keyEpoch > chain.length) {
189
- throw new Error(`keyEpoch ${keyEpoch} needs ${keyEpoch} handoffs in chain, got ${chain.length}`);
190
- }
191
- const opDigest = await ownerOpDigest(input.op.bytes);
192
- const payload = await ownerOpSigningPayload({
193
- account,
194
- namespaceId: input.namespaceId,
195
- groupId: input.groupId,
196
- kind: input.op.kind,
197
- opDigest,
198
- counter: input.counter,
199
- keyEpoch,
200
- });
201
- const key = await importSigningKey(input.rootSecret);
202
- const signature = new Uint8Array(await crypto.subtle.sign('Ed25519', key, payload));
203
- return hex(concat(genesis, u32le(chain.length), ...chain, fromHex(account, 'account', 32), fromHex(input.namespaceId, 'namespaceId', 32), fromHex(input.groupId, 'groupId', 32), tag(kindByte(input.op.kind)), opDigest, u64le(input.counter), u32le(keyEpoch), signature));
204
- }
205
- /**
206
- * The `RootGuarded` wrapper core publishes: the op and its proof, as one
207
- * `GroupOp` or `RootOp`. A client never sends this to a node (the endpoints
208
- * take the proof and build the wrapper); it is here so the whole encoding is
209
- * checkable against core's vector.
210
- */
211
- export function rootGuardedOpBytes(op, rootProof) {
212
- const wrapper = op.plane === 'group' ? GROUP_OP.RootGuarded : ROOT_OP.RootGuarded;
213
- return concat(tag(wrapper), op.bytes, fromHexUnsized(rootProof, 'rootProof'));
214
- }
@@ -1,56 +0,0 @@
1
- /**
2
- * An account's ephemeral presence, through its relay.
3
- *
4
- * A node publishes and heartbeats its own presence; an account has no node, so
5
- * this client signs each update with the device key, posts it to the relay's
6
- * `presence-intents`, and resends the last state every 2.5 s until
7
- * `set(contextId, null)` or `close()`. The relay checks the update and seals
8
- * and gossips it; other members verify the device's signature and certificate
9
- * themselves, so the relay cannot invent or alter an account's presence.
10
- */
11
- import type { SseClient } from '../events/index.js';
12
- import type { Codec, EphemeralEntry } from '../ephemeral/types.js';
13
- import type { RelayClient } from '../relay/relay-client.js';
14
- type PresenceRelay = Pick<RelayClient, 'authorProof' | 'authorSigner' | 'presenceIntent'>;
15
- export declare class RelayPresenceClient {
16
- private readonly relay;
17
- private readonly events;
18
- private readonly heartbeatMs;
19
- private readonly now;
20
- private readonly timers;
21
- private readonly lastSeq;
22
- private readonly refused;
23
- /**
24
- * Bumped by every `set` and `close`, per context. A `set` installs its
25
- * resends only if nothing newer happened while its first send was in flight,
26
- * so a retract (or a newer state) can never be overtaken by an older one.
27
- */
28
- private readonly generation;
29
- /**
30
- * @param opts.events the relay's event stream, asked for only on `subscribe`:
31
- * publishing needs none, so a client with no node key can still publish.
32
- */
33
- constructor(opts: {
34
- relay: PresenceRelay;
35
- events: () => SseClient;
36
- heartbeatMs?: number;
37
- now?: () => number;
38
- });
39
- /**
40
- * Set this account's presence in `contextId`, or retract it with `null`.
41
- * Throws when the relay refuses this first send for good (a 4xx other than
42
- * 429), which also stops the resends. A rate limit, a network failure or a
43
- * 5xx is not thrown: the next resend covers it.
44
- */
45
- set<T>(contextId: string, state: T | null, codec?: Codec<T>): Promise<void>;
46
- subscribe<T>(contextId: string, handler: (entry: EphemeralEntry<T>) => void, codec?: Codec<T>): () => void;
47
- /** Stop every resend. Entries expire on the relay and its peers within 7 s. */
48
- close(): void;
49
- /** Stop the context's resends and invalidate any `set` still in flight. */
50
- private bump;
51
- private stop;
52
- /** Wall-clock seeded and strictly rising, so a relay restart never sees an old seq. */
53
- private nextSeq;
54
- private send;
55
- }
56
- export {};
@@ -1,107 +0,0 @@
1
- import { hex } from '../crypto/internal.js';
2
- import { jsonCodec, subscribePresence } from '../ephemeral/index.js';
3
- import { presenceStatementBytes } from './statement.js';
4
- /** The node's own heartbeat interval, so a relayed entry never outlives the 7 s TTL. */
5
- const HEARTBEAT_MS = 2500;
6
- export class RelayPresenceClient {
7
- /**
8
- * @param opts.events the relay's event stream, asked for only on `subscribe`:
9
- * publishing needs none, so a client with no node key can still publish.
10
- */
11
- constructor(opts) {
12
- this.timers = new Map();
13
- this.lastSeq = new Map();
14
- this.refused = new Set();
15
- /**
16
- * Bumped by every `set` and `close`, per context. A `set` installs its
17
- * resends only if nothing newer happened while its first send was in flight,
18
- * so a retract (or a newer state) can never be overtaken by an older one.
19
- */
20
- this.generation = new Map();
21
- this.relay = opts.relay;
22
- this.events = opts.events;
23
- this.heartbeatMs = opts.heartbeatMs ?? HEARTBEAT_MS;
24
- this.now = opts.now ?? Date.now;
25
- }
26
- /**
27
- * Set this account's presence in `contextId`, or retract it with `null`.
28
- * Throws when the relay refuses this first send for good (a 4xx other than
29
- * 429), which also stops the resends. A rate limit, a network failure or a
30
- * 5xx is not thrown: the next resend covers it.
31
- */
32
- async set(contextId, state, codec = jsonCodec()) {
33
- const generation = this.bump(contextId);
34
- this.refused.delete(contextId);
35
- const bytes = state === null ? null : new Uint8Array(codec.encode(state));
36
- await this.send(contextId, bytes);
37
- if (bytes === null || this.refused.has(contextId))
38
- return;
39
- if (this.generation.get(contextId) !== generation)
40
- return; // a newer set or close won
41
- this.timers.set(contextId, setInterval(() => void this.send(contextId, bytes).catch(() => undefined), this.heartbeatMs));
42
- }
43
- subscribe(contextId, handler, codec = jsonCodec()) {
44
- return subscribePresence(this.events(), contextId, handler, codec);
45
- }
46
- /** Stop every resend. Entries expire on the relay and its peers within 7 s. */
47
- close() {
48
- for (const contextId of new Set([...this.timers.keys(), ...this.generation.keys()])) {
49
- this.bump(contextId);
50
- }
51
- }
52
- /** Stop the context's resends and invalidate any `set` still in flight. */
53
- bump(contextId) {
54
- this.stop(contextId);
55
- const next = (this.generation.get(contextId) ?? 0) + 1;
56
- this.generation.set(contextId, next);
57
- return next;
58
- }
59
- stop(contextId) {
60
- const timer = this.timers.get(contextId);
61
- if (timer !== undefined)
62
- clearInterval(timer);
63
- this.timers.delete(contextId);
64
- }
65
- /** Wall-clock seeded and strictly rising, so a relay restart never sees an old seq. */
66
- nextSeq(contextId) {
67
- const seq = Math.max(this.now(), (this.lastSeq.get(contextId) ?? 0) + 1);
68
- this.lastSeq.set(contextId, seq);
69
- return seq;
70
- }
71
- async send(contextId, state) {
72
- if (this.refused.has(contextId))
73
- return;
74
- const signer = await this.relay.authorSigner();
75
- const seq = this.nextSeq(contextId);
76
- const sentAtMs = this.now();
77
- const statement = await presenceStatementBytes({
78
- contextId,
79
- author: signer.publicKey,
80
- seq: BigInt(seq),
81
- sentAtMs: BigInt(sentAtMs),
82
- state,
83
- });
84
- const signature = hex(await signer.sign(statement));
85
- try {
86
- await this.relay.presenceIntent(contextId, {
87
- state: state === null ? null : hex(state),
88
- seq,
89
- sentAtMs,
90
- signature,
91
- authorProof: this.relay.authorProof,
92
- });
93
- }
94
- catch (err) {
95
- const status = err.status;
96
- // Rate limited, a network failure, or the relay unable right now (5xx,
97
- // e.g. no current key mid-rotation): the next resend covers it.
98
- if (status === undefined || status === 0 || status === 429 || status >= 500)
99
- return;
100
- // Any other refusal (403: not a member or revoked; 400: malformed or a
101
- // clock outside the relay's window) cannot be fixed by resending.
102
- this.refused.add(contextId);
103
- this.stop(contextId);
104
- throw err;
105
- }
106
- }
107
- }
@@ -1,12 +0,0 @@
1
- /** core's `PRESENCE_DOMAIN`: part of the signed bytes. */
2
- export declare const PRESENCE_DOMAIN: Uint8Array;
3
- /** `sha256(borsh(Option<Vec<u8>>))`: `None` is `[0]`, `Some(v)` is `[1] ‖ u32le(len) ‖ v`. */
4
- export declare function stateHash(state: Uint8Array | null): Promise<Uint8Array>;
5
- /** What the device key signs, raw: domain ‖ context ‖ author ‖ seq ‖ sentAtMs ‖ stateHash. */
6
- export declare function presenceStatementBytes(input: {
7
- contextId: string;
8
- author: string;
9
- seq: bigint;
10
- sentAtMs: bigint;
11
- state: Uint8Array | null;
12
- }): Promise<Uint8Array>;
@@ -1,17 +0,0 @@
1
- /**
2
- * The bytes a device signs for one presence update: the borsh of core's
3
- * `PresenceStatement` (`calimero_node_primitives::presence`). Pinned to core's
4
- * vector in `statement.test.ts`, so the two encoders cannot drift.
5
- */
6
- import { concat, fromHex, u32le, u64le } from '../crypto/internal.js';
7
- /** core's `PRESENCE_DOMAIN`: part of the signed bytes. */
8
- export const PRESENCE_DOMAIN = new TextEncoder().encode('calimero/presence/1');
9
- /** `sha256(borsh(Option<Vec<u8>>))`: `None` is `[0]`, `Some(v)` is `[1] ‖ u32le(len) ‖ v`. */
10
- export async function stateHash(state) {
11
- const borsh = state === null ? new Uint8Array([0]) : concat(new Uint8Array([1]), u32le(state.length), state);
12
- return new Uint8Array(await crypto.subtle.digest('SHA-256', borsh));
13
- }
14
- /** What the device key signs, raw: domain ‖ context ‖ author ‖ seq ‖ sentAtMs ‖ stateHash. */
15
- export async function presenceStatementBytes(input) {
16
- return concat(PRESENCE_DOMAIN, fromHex(input.contextId, 'contextId', 32), fromHex(input.author, 'author', 32), u64le(input.seq), u64le(input.sentAtMs), await stateHash(input.state));
17
- }
@@ -1,4 +0,0 @@
1
- export { RelayClient, IntentRefusedError } from './relay-client.js';
2
- export type { RelayClientConfig, RelayExecutor, RelayDescription, IntentResult, CreationDescription, CreateContextInput, CreatedContext, GovernanceDescription, GovernInput, GovernResult, FoundNamespaceInput, FoundedNamespace, } from './relay-client.js';
3
- export { createMemoryNonceSource, createLocalStorageNonceSource, } from './nonce-source.js';
4
- export type { NonceSource } from './nonce-source.js';
@@ -1,2 +0,0 @@
1
- export { RelayClient, IntentRefusedError } from './relay-client.js';
2
- export { createMemoryNonceSource, createLocalStorageNonceSource, } from './nonce-source.js';
@@ -1,72 +0,0 @@
1
- /**
2
- * Where a warrant's nonce comes from.
3
- *
4
- * # Why this is a named concept and not a counter in the relay client
5
- *
6
- * A warrant's nonce is spent, once, per `(context, author device)` on every
7
- * node that applies the delta. So the sequence a client emits is not a local
8
- * detail — it is state the network remembers, and the client is the only party
9
- * that can keep it.
10
- *
11
- * The receiving ledger is a **sliding window** 64 wide, not a high-water mark,
12
- * which is what makes the two failure modes so different:
13
- *
14
- * - **Skipping** numbers is free. Gossip gives no ordering, so the window
15
- * exists precisely to accept whatever unseen nonce arrives.
16
- * - **Restarting** the sequence is fatal. A tab that reloads and resumes at `1`
17
- * re-presents nonces the network has already spent, and every one of those
18
- * writes is refused as a replay — indistinguishable, to the user, from the
19
- * app silently not saving.
20
- *
21
- * That asymmetry is why persistence is the default here and an in-memory source
22
- * is the one you have to ask for. It is also why a source may jump: reserving a
23
- * block and losing the tail is the correct trade.
24
- */
25
- /** Produces the next nonce for one author device. Must never go backwards. */
26
- export interface NonceSource {
27
- next(): Promise<bigint>;
28
- /**
29
- * Raise the floor of the sequence to `floor`, if this source can record it.
30
- *
31
- * Optional, and additive: a source that does not implement it still works
32
- * everywhere a `NonceSource` is taken. Implementing it is what lets a caller
33
- * that learns a higher spent nonce out of band — a refusal naming the nonce,
34
- * say — skip ahead, and lets a *persistent* source remember that jump across
35
- * a cold start.
36
- *
37
- * Never lowers: a smaller `floor` than the source's own position is ignored.
38
- */
39
- advanceTo?(floor: bigint): Promise<void>;
40
- }
41
- /**
42
- * A counter in memory, starting at `start`.
43
- *
44
- * Correct only for a process whose lifetime is the whole sequence — a script, a
45
- * server-side worker that owns its own device key, a test. In a browser tab it
46
- * resets on reload and replays; use {@link createLocalStorageNonceSource}
47
- * there.
48
- */
49
- export declare function createMemoryNonceSource(start?: number | bigint): NonceSource;
50
- /**
51
- * A counter persisted in `localStorage`, so a reload continues the sequence.
52
- *
53
- * Key it per author **device**, not per account: two devices of one account are
54
- * independent replicas with independent sequences, and sharing a counter would
55
- * have them refusing each other's warrants.
56
- *
57
- * **Reserve-then-hand-out.** The stored value is advanced *before* the nonce is
58
- * returned, so a crash between the two loses a number rather than reusing one.
59
- * Losing a number costs nothing (the window accepts gaps); reusing one costs a
60
- * write.
61
- *
62
- * Concurrent tabs are the case this cannot fully solve, because `localStorage`
63
- * has no atomic read-modify-write. Two tabs racing can hand out the same nonce,
64
- * and the loser's write is refused as a replay rather than silently
65
- * misattributed — a visible error, not corruption. An app that genuinely writes
66
- * from several tabs at once should hold a Web Lock around `next()` or give each
67
- * tab its own device.
68
- */
69
- export declare function createLocalStorageNonceSource(key: string, storage?: {
70
- getItem(k: string): string | null;
71
- setItem(k: string, v: string): void;
72
- }): NonceSource;
@@ -1,111 +0,0 @@
1
- /**
2
- * Where a warrant's nonce comes from.
3
- *
4
- * # Why this is a named concept and not a counter in the relay client
5
- *
6
- * A warrant's nonce is spent, once, per `(context, author device)` on every
7
- * node that applies the delta. So the sequence a client emits is not a local
8
- * detail — it is state the network remembers, and the client is the only party
9
- * that can keep it.
10
- *
11
- * The receiving ledger is a **sliding window** 64 wide, not a high-water mark,
12
- * which is what makes the two failure modes so different:
13
- *
14
- * - **Skipping** numbers is free. Gossip gives no ordering, so the window
15
- * exists precisely to accept whatever unseen nonce arrives.
16
- * - **Restarting** the sequence is fatal. A tab that reloads and resumes at `1`
17
- * re-presents nonces the network has already spent, and every one of those
18
- * writes is refused as a replay — indistinguishable, to the user, from the
19
- * app silently not saving.
20
- *
21
- * That asymmetry is why persistence is the default here and an in-memory source
22
- * is the one you have to ask for. It is also why a source may jump: reserving a
23
- * block and losing the tail is the correct trade.
24
- */
25
- /**
26
- * A counter in memory, starting at `start`.
27
- *
28
- * Correct only for a process whose lifetime is the whole sequence — a script, a
29
- * server-side worker that owns its own device key, a test. In a browser tab it
30
- * resets on reload and replays; use {@link createLocalStorageNonceSource}
31
- * there.
32
- */
33
- export function createMemoryNonceSource(start = 1) {
34
- let next = BigInt(start);
35
- if (next < 1n) {
36
- throw new Error('nonce sequence starts at 1');
37
- }
38
- return {
39
- next() {
40
- const value = next;
41
- next += 1n;
42
- return Promise.resolve(value);
43
- },
44
- advanceTo(floor) {
45
- if (floor > next)
46
- next = floor;
47
- return Promise.resolve();
48
- },
49
- };
50
- }
51
- /**
52
- * A counter persisted in `localStorage`, so a reload continues the sequence.
53
- *
54
- * Key it per author **device**, not per account: two devices of one account are
55
- * independent replicas with independent sequences, and sharing a counter would
56
- * have them refusing each other's warrants.
57
- *
58
- * **Reserve-then-hand-out.** The stored value is advanced *before* the nonce is
59
- * returned, so a crash between the two loses a number rather than reusing one.
60
- * Losing a number costs nothing (the window accepts gaps); reusing one costs a
61
- * write.
62
- *
63
- * Concurrent tabs are the case this cannot fully solve, because `localStorage`
64
- * has no atomic read-modify-write. Two tabs racing can hand out the same nonce,
65
- * and the loser's write is refused as a replay rather than silently
66
- * misattributed — a visible error, not corruption. An app that genuinely writes
67
- * from several tabs at once should hold a Web Lock around `next()` or give each
68
- * tab its own device.
69
- */
70
- export function createLocalStorageNonceSource(key, storage) {
71
- const store = storage ?? globalThis.localStorage;
72
- if (!store) {
73
- throw new Error('no localStorage in this runtime: pass a storage implementation, or use createMemoryNonceSource for a process that owns its whole sequence');
74
- }
75
- // A missing, empty, or corrupted value means "no sequence recorded", and the
76
- // safe reading of that is not 1 — a cleared storage on a device that has
77
- // already written would replay. But the client cannot know, and 1 is the only
78
- // defensible start, so it is what a fresh key gets. This is why the doc above
79
- // tells apps to key per device: a device whose storage was cleared should be
80
- // re-paired rather than resumed. No node serves a "where does my sequence
81
- // stand" lookup, so the local counter is the only record there is.
82
- const lastHandedOut = () => {
83
- const raw = store.getItem(key);
84
- if (raw === null || raw === '')
85
- return 0n;
86
- try {
87
- const parsed = BigInt(raw);
88
- return parsed > 0n ? parsed : 0n;
89
- }
90
- catch {
91
- return 0n;
92
- }
93
- };
94
- return {
95
- next() {
96
- const value = lastHandedOut() + 1n;
97
- store.setItem(key, value.toString());
98
- return Promise.resolve(value);
99
- },
100
- advanceTo(floor) {
101
- // Stored is the last number handed out, so the floor is recorded as the
102
- // one below it — persisted rather than held in memory, which is the whole
103
- // point: the next cold start resumes above the jump even if the node
104
- // cannot be reached then.
105
- const wanted = floor > 0n ? floor - 1n : 0n;
106
- if (wanted > lastHandedOut())
107
- store.setItem(key, wanted.toString());
108
- return Promise.resolve();
109
- },
110
- };
111
- }