@calimero-network/mero-js 26.11.0 → 27.0.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 (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
package/dist/mero-js.js DELETED
@@ -1,469 +0,0 @@
1
- import { createBrowserHttpClient } from './http-client/index.js';
2
- import { createAuthApiClientFromHttpClient } from './auth-api/index.js';
3
- import { createAdminApiClientFromHttpClient } from './admin-api/index.js';
4
- import { parseAuthCallback, buildAuthLoginUrl } from './auth/index.js';
5
- import { createProofSigner } from './request/index.js';
6
- import { RpcClient } from './rpc/index.js';
7
- import { SseClient } from './events/sse.js';
8
- import { WsClient } from './events/ws.js';
9
- import { EphemeralClient } from './ephemeral/index.js';
10
- import { CloudClient } from './cloud/cloud-client.js';
11
- /**
12
- * Accept either the keys or a ready signer.
13
- *
14
- * Taking a callback as well as an options object is not a convenience: it is
15
- * how a caller keeps the signing key out of the SDK entirely when it lives
16
- * somewhere the SDK cannot reach.
17
- */
18
- function resolveProofSigner(config) {
19
- if (!config)
20
- return undefined;
21
- return typeof config === 'function' ? config : createProofSigner(config);
22
- }
23
- /** Name of the Web Lock that serializes token refresh across tabs / webviews. */
24
- const REFRESH_LOCK_NAME = 'mero-js:token-refresh';
25
- /** Returns `navigator.locks` when the runtime supports the Web Locks API. */
26
- function getLockManager() {
27
- const nav = globalThis.navigator;
28
- const locks = nav?.locks;
29
- return locks && typeof locks.request === 'function' ? locks : null;
30
- }
31
- /** Key a token bundle to its node: origin plus path prefix, so nodes behind one host stay apart. */
32
- function nodeKeyOf(baseUrl) {
33
- try {
34
- const { origin, pathname } = new URL(baseUrl);
35
- return origin === 'null' ? baseUrl : origin + pathname.replace(/\/+$/, '');
36
- }
37
- catch {
38
- return baseUrl;
39
- }
40
- }
41
- /** Try to extract `exp` (seconds) from a JWT, return ms timestamp or fallback. */
42
- function expiresAtFromJwt(token, fallbackMs) {
43
- try {
44
- const parts = token.split('.');
45
- if (parts.length === 3) {
46
- // JWT uses base64url encoding: replace -/_ with +// and add padding
47
- let b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
48
- while (b64.length % 4)
49
- b64 += '=';
50
- const payload = JSON.parse(atob(b64));
51
- if (typeof payload.exp === 'number') {
52
- return payload.exp * 1000;
53
- }
54
- }
55
- }
56
- catch {
57
- // not a JWT or can't parse
58
- }
59
- return fallbackMs;
60
- }
61
- /**
62
- * Main MeroJs SDK class that manages all API clients and authentication
63
- */
64
- export class MeroJs {
65
- constructor(config) {
66
- this.tokenData = null;
67
- this.refreshPromise = null;
68
- // True while the store is known to hold our tokens; a store that cannot persist never sets it.
69
- this.storeHoldsTokens = false;
70
- // Bumped when local tokens are dropped, so a refresh already in flight cannot restore them.
71
- this.tokenGeneration = 0;
72
- this.onStorage = () => {
73
- if (this.signedOutElsewhere())
74
- this.dropLocalTokens();
75
- };
76
- this.rpcClient = null;
77
- this.sseClient = null;
78
- this.wsClient = null;
79
- this.wsWarned = false;
80
- this.config = {
81
- timeoutMs: 10000,
82
- ...config,
83
- };
84
- this.tokenStore = config.tokenStore ?? null;
85
- this.nodeKey = nodeKeyOf(config.baseUrl);
86
- // Restore tokens from store if available
87
- this.tokenData = this.readStoredTokens();
88
- if (this.tokenStore) {
89
- this.storeHoldsTokens = this.tokenData !== null;
90
- if (typeof window !== 'undefined')
91
- window.addEventListener('storage', this.onStorage);
92
- }
93
- // Create HTTP client with token management
94
- // For Tauri, explicitly set credentials to 'omit' to avoid network errors
95
- const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window;
96
- // One signer for the whole connection: `authClient` and `adminClient` are
97
- // both built FROM this transport, so wiring it here covers every request
98
- // either of them makes.
99
- const getProof = resolveProofSigner(this.config.requestProof);
100
- this.httpClient = createBrowserHttpClient({
101
- baseUrl: this.config.baseUrl,
102
- fetch: this.config.fetch,
103
- getProof,
104
- getAuthToken: async () => {
105
- const token = await this.getValidToken();
106
- return token?.access_token || '';
107
- },
108
- refreshToken: async () => {
109
- const refreshed = await this.performTokenRefresh();
110
- return refreshed.access_token;
111
- },
112
- onTokenRefresh: async (newToken) => {
113
- if (this.tokenData) {
114
- this.tokenData.access_token = newToken;
115
- this.persistTokens();
116
- }
117
- },
118
- onAuthRevoked: async () => {
119
- // The refresh-token family is gone (a single-use refresh token was
120
- // replayed, or the token was revoked). Nothing left to refresh with —
121
- // drop the bundle so the app can force a re-login, then let the app react.
122
- this.clearToken();
123
- try {
124
- await this.config.onAuthRevoked?.();
125
- }
126
- catch {
127
- // A failing app callback must not mask the auth error.
128
- }
129
- },
130
- timeoutMs: this.config.timeoutMs,
131
- credentials: this.config.requestCredentials ?? (isTauri ? 'omit' : undefined),
132
- });
133
- // Create API clients
134
- this.authClient = createAuthApiClientFromHttpClient(this.httpClient, {
135
- baseUrl: this.config.baseUrl,
136
- getAuthToken: async () => {
137
- const token = await this.getValidToken();
138
- return token?.access_token || '';
139
- },
140
- timeoutMs: this.config.timeoutMs,
141
- });
142
- this.adminClient = createAdminApiClientFromHttpClient(this.httpClient, {
143
- baseUrl: this.config.baseUrl,
144
- getAuthToken: async () => {
145
- const token = await this.getValidToken();
146
- return token?.access_token || '';
147
- },
148
- timeoutMs: this.config.timeoutMs,
149
- });
150
- }
151
- /**
152
- * Get the Auth API client
153
- */
154
- get auth() {
155
- return this.authClient;
156
- }
157
- /**
158
- * Get the Admin API client
159
- */
160
- get admin() {
161
- return this.adminClient;
162
- }
163
- /**
164
- * Get the RPC client (lazy initialized)
165
- */
166
- get rpc() {
167
- if (!this.rpcClient) {
168
- this.rpcClient = new RpcClient({ httpClient: this.httpClient });
169
- }
170
- return this.rpcClient;
171
- }
172
- /**
173
- * Get the SSE event client (lazy initialized)
174
- */
175
- get events() {
176
- if (!this.sseClient) {
177
- this.sseClient = new SseClient({
178
- baseUrl: this.config.baseUrl,
179
- fetch: this.config.fetch,
180
- getAuthToken: async () => {
181
- const token = await this.getValidToken();
182
- return token?.access_token || '';
183
- },
184
- });
185
- }
186
- return this.sseClient;
187
- }
188
- /**
189
- * Get the ephemeral-presence client (lazy initialized).
190
- *
191
- * Shares this instance's SSE client, so `subscribe` reuses the one event
192
- * stream rather than opening a second connection.
193
- */
194
- get ephemeral() {
195
- if (!this.ephemeralClient) {
196
- this.ephemeralClient = new EphemeralClient({
197
- httpClient: this.httpClient,
198
- sse: this.events,
199
- });
200
- }
201
- return this.ephemeralClient;
202
- }
203
- /**
204
- * Calimero Cloud (lazy initialized).
205
- *
206
- * Available whether or not `cloud` was configured — an app that signs in
207
- * later gets an unauthenticated client it can call `signInWithGoogle` on,
208
- * which is one fewer construction path than making the getter conditional.
209
- * It is entirely independent of this instance's node credential: the cloud
210
- * knows nothing about the node's tokens and the node knows nothing about the
211
- * cloud session.
212
- */
213
- get cloud() {
214
- if (!this.cloudClient) {
215
- this.cloudClient = new CloudClient(this.config.cloud ?? {});
216
- }
217
- return this.cloudClient;
218
- }
219
- /**
220
- * Get the WebSocket event client (lazy initialized).
221
- * @experimental Use `events` (SSE) for production. WsClient is experimental.
222
- */
223
- get ws() {
224
- if (!this.wsWarned) {
225
- this.wsWarned = true;
226
- console.warn('[mero-js] WsClient is experimental. Use mero.events (SSE) for production.');
227
- }
228
- if (!this.wsClient) {
229
- this.wsClient = new WsClient({
230
- baseUrl: this.config.baseUrl,
231
- getAuthToken: async () => {
232
- const token = await this.getValidToken();
233
- return token?.access_token || '';
234
- },
235
- });
236
- }
237
- return this.wsClient;
238
- }
239
- /**
240
- * Authenticate with the provided credentials
241
- * This will create the root key on first use
242
- */
243
- async authenticate(credentials) {
244
- const creds = credentials || this.config.credentials;
245
- if (!creds) {
246
- throw new Error('No credentials provided for authentication');
247
- }
248
- try {
249
- const requestBody = {
250
- auth_method: 'user_password',
251
- public_key: creds.username,
252
- client_name: 'mero-js-sdk',
253
- permissions: ['admin'],
254
- timestamp: Math.floor(Date.now() / 1000),
255
- provider_data: {
256
- username: creds.username,
257
- password: creds.password,
258
- },
259
- };
260
- const response = await this.authClient.generateTokens(requestBody);
261
- const accessToken = response.data.access_token;
262
- this.tokenData = {
263
- access_token: accessToken,
264
- refresh_token: response.data.refresh_token,
265
- expires_at: expiresAtFromJwt(accessToken, Date.now() + 3600000),
266
- };
267
- this.persistTokens();
268
- return this.tokenData;
269
- }
270
- catch (error) {
271
- throw new Error(`Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
272
- }
273
- }
274
- persistTokens() {
275
- if (!this.tokenStore || !this.tokenData)
276
- return;
277
- this.tokenStore.setTokens({ ...this.tokenData, node_origin: this.nodeKey });
278
- this.storeHoldsTokens = this.tokenStore.getTokens() !== null;
279
- }
280
- /** The stored bundle, only if this node issued it; a legacy or foreign entry reads as empty. */
281
- readStoredTokens() {
282
- const stored = this.tokenStore?.getTokens();
283
- if (!stored || stored.node_origin !== this.nodeKey)
284
- return null;
285
- return {
286
- access_token: stored.access_token,
287
- refresh_token: stored.refresh_token,
288
- expires_at: stored.expires_at,
289
- };
290
- }
291
- // Raw store read: an empty store is a logout, another node's bundle is not.
292
- signedOutElsewhere() {
293
- return this.storeHoldsTokens && !this.tokenStore?.getTokens();
294
- }
295
- dropLocalTokens() {
296
- this.tokenGeneration++;
297
- this.refreshPromise = null;
298
- this.tokenData = null;
299
- this.storeHoldsTokens = false;
300
- }
301
- /**
302
- * Get a valid token. Returns the current token as-is.
303
- * The server rejects refresh attempts while the access token is still valid,
304
- * so we never proactively refresh. Instead, the WebHttpClient handles 401
305
- * responses reactively via the refreshToken transport hook.
306
- */
307
- async getValidToken() {
308
- return this.tokenData;
309
- }
310
- /**
311
- * Refresh the access token. Single-flight: every caller (including the HTTP
312
- * transport's 401 hook) goes through here.
313
- *
314
- * Refresh tokens are single-use (calimero-network/core#3083): each refresh
315
- * consumes the presented token and returns a new one, and replaying a consumed
316
- * token makes the server revoke the whole family. A refresh must therefore
317
- * happen exactly once per rotation, which needs three layers:
318
- * - an in-process promise, so concurrent 401s in this instance share one call;
319
- * - a Web Lock (when available), so other tabs/webviews don't refresh at the
320
- * same time on the same stored token;
321
- * - a re-read of the token store inside the lock, so a bundle that another
322
- * instance already rotated is adopted instead of replaying a stale token.
323
- */
324
- async performTokenRefresh() {
325
- if (this.refreshPromise) {
326
- return this.refreshPromise;
327
- }
328
- // The access token that triggered this refresh. If the store holds a different
329
- // one by the time we get the lock, someone else already rotated for us.
330
- const triggeringAccessToken = this.tokenData?.access_token;
331
- const promise = this.withRefreshLock(() => this.performTokenRefreshLocked(triggeringAccessToken));
332
- this.refreshPromise = promise;
333
- try {
334
- return await promise;
335
- }
336
- finally {
337
- if (this.refreshPromise === promise) {
338
- this.refreshPromise = null;
339
- }
340
- }
341
- }
342
- /**
343
- * Run `fn` under the refresh lock: the token store's own (shared across
344
- * processes), else the cross-tab Web Lock, else directly.
345
- */
346
- withRefreshLock(fn) {
347
- if (this.tokenStore?.withLock)
348
- return this.tokenStore.withLock(fn);
349
- const locks = getLockManager();
350
- return locks ? locks.request(REFRESH_LOCK_NAME, fn) : fn();
351
- }
352
- /**
353
- * Perform the actual token refresh. Runs with the refresh lock held.
354
- */
355
- async performTokenRefreshLocked(triggeringAccessToken) {
356
- // Another instance/tab may have rotated the bundle while we were queued behind
357
- // the lock — the store, not our in-memory copy, is the source of truth.
358
- if (this.signedOutElsewhere()) {
359
- // Another holder signed out and emptied the store; refreshing would undo that.
360
- this.dropLocalTokens();
361
- throw new Error('Signed out in another tab');
362
- }
363
- const stored = this.readStoredTokens();
364
- if (stored?.refresh_token) {
365
- this.storeHoldsTokens = true;
366
- this.tokenData = stored;
367
- if (triggeringAccessToken && stored.access_token !== triggeringAccessToken) {
368
- // Someone else already refreshed. Reuse their bundle rather than replaying
369
- // our (now consumed) refresh token, which would revoke the family.
370
- return stored;
371
- }
372
- }
373
- if (!this.tokenData?.refresh_token) {
374
- throw new Error('No refresh token available');
375
- }
376
- // The store held the token we present; if it no longer does after the refresh, the
377
- // session was replaced or signed out meanwhile and our rotated tokens must not be written.
378
- const generation = this.tokenGeneration;
379
- const presented = this.tokenData.access_token;
380
- const storeHeldPresented = this.tokenStore?.getTokens()?.access_token === presented;
381
- try {
382
- const response = await this.authClient.refreshToken({
383
- access_token: this.tokenData.access_token,
384
- refresh_token: this.tokenData.refresh_token,
385
- });
386
- // A local drop already set the state; a store change by another tab is adopted.
387
- if (generation !== this.tokenGeneration)
388
- throw new Error('Signed out during token refresh');
389
- if (storeHeldPresented && this.tokenStore?.getTokens()?.access_token !== presented) {
390
- this.tokenData = this.tokenStore?.getTokens() ?? null;
391
- this.storeHoldsTokens = this.tokenData !== null;
392
- throw new Error('Signed out during token refresh');
393
- }
394
- const accessToken = response.data.access_token;
395
- this.tokenData = {
396
- access_token: accessToken,
397
- // The refresh token is rotated on every refresh — persist the new one or
398
- // the next refresh replays a consumed token.
399
- refresh_token: response.data.refresh_token,
400
- expires_at: expiresAtFromJwt(accessToken, Date.now() + 3600000),
401
- };
402
- this.persistTokens();
403
- return this.tokenData;
404
- }
405
- catch (error) {
406
- // Don't clear tokens on refresh failure — the access token may still be
407
- // valid (server rejects refresh while access token hasn't expired yet).
408
- // A genuinely dead family arrives as `x-auth-error: token_reuse` and is
409
- // handled by the onAuthRevoked transport hook. Let the caller handle this.
410
- throw new Error(`Token refresh failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
411
- }
412
- }
413
- /**
414
- * Clear the current token
415
- */
416
- clearToken() {
417
- this.dropLocalTokens();
418
- this.tokenStore?.clear();
419
- }
420
- /**
421
- * Check if the SDK is authenticated
422
- */
423
- isAuthenticated() {
424
- return this.tokenData !== null;
425
- }
426
- /**
427
- * Set token data directly (e.g., from auth callback).
428
- * If `expires_at` is missing or 0, attempts to parse the JWT exp claim,
429
- * falling back to 1 hour from now.
430
- */
431
- setTokenData(data) {
432
- const expiresAt = data.expires_at || expiresAtFromJwt(data.access_token, Date.now() + 3600000);
433
- this.tokenData = { ...data, expires_at: expiresAt };
434
- this.persistTokens();
435
- }
436
- /**
437
- * Get the current token data (for debugging)
438
- */
439
- getTokenData() {
440
- return this.tokenData;
441
- }
442
- /**
443
- * Close all event connections and clean up resources
444
- */
445
- close() {
446
- if (typeof window !== 'undefined')
447
- window.removeEventListener('storage', this.onStorage);
448
- this.sseClient?.close();
449
- this.wsClient?.close();
450
- }
451
- /**
452
- * Parse an auth callback URL hash fragment (static utility)
453
- */
454
- static parseAuthCallback(url, options) {
455
- return parseAuthCallback(url, options);
456
- }
457
- /**
458
- * Build an auth login URL (static utility)
459
- */
460
- static buildAuthLoginUrl(nodeUrl, opts) {
461
- return buildAuthLoginUrl(nodeUrl, opts);
462
- }
463
- }
464
- /**
465
- * Create a new MeroJs SDK instance
466
- */
467
- export function createMeroJs(config) {
468
- return new MeroJs(config);
469
- }
@@ -1,2 +0,0 @@
1
- export { signMemberJoinOp, encodeSignedInvitation, SIGNED_NAMESPACE_OP_SCHEMA_VERSION, } from './namespace-op.js';
2
- export type { SignMemberJoinInput } from './namespace-op.js';
@@ -1,3 +0,0 @@
1
- // Signing a namespace governance op from the SDK — the membership op a keyholder
2
- // presents to an admitter, which it could not produce before.
3
- export { signMemberJoinOp, encodeSignedInvitation, SIGNED_NAMESPACE_OP_SCHEMA_VERSION, } from './namespace-op.js';
@@ -1,150 +0,0 @@
1
- /**
2
- * Sign a namespace governance op from the SDK — specifically the membership op a
3
- * keyholder needs to join without running a node.
4
- *
5
- * `admitJoin` carries an already-signed op to a node the inviter named as an
6
- * admitter. Producing that op is the piece a keyholder could not do before: the
7
- * op must be signed by the device key in the credential it carries, because every
8
- * peer checks `signer == credential.statement.sign_pk` when applying a join. An
9
- * admitter can relay it and can never author it.
10
- *
11
- * The encoding is borsh, hand-written to match core's structs. Correctness is not
12
- * asserted against bytes recorded here — a layout transcribed by eye is exactly
13
- * how a golden ends up confidently wrong. It is asserted by a node accepting the
14
- * op, which is what the e2e does.
15
- */
16
- import type { SignedGroupOpenInvitation } from '../admin-api/admin-types.js';
17
- import { type Signer } from '../signer/signer.js';
18
- /**
19
- * Core's `SIGNED_NAMESPACE_OP_SCHEMA_VERSION`. A node refuses any other value.
20
- *
21
- * 15: core went to 13 (TEE rows locked to the TEE roles, a TEE's relay role
22
- * read at its namespace root row, inherited read-only roles read-only in Open
23
- * subgroups), then 14 (`GroupOp::ContextRegisteredOnBehalf`) and 15
24
- * (`GroupOp::OnBehalf` / `RootOp::OnBehalf`) for delegated context creation and
25
- * governance through a relay. None of these changes a layout this module
26
- * signs; the version moves with core's so nodes on different versions never
27
- * share a namespace.
28
- *
29
- * 16: a namespace can be founded through a relay (a delegated
30
- * `NamespaceCreatedV2`), and `GroupOp::FoundingRelayAttested` lets that relay
31
- * admit itself as the namespace's first TEE. Again no layout this module signs.
32
- *
33
- * 17: `RootOp::GroupCreated` carries a `salt`, and its `group_id` must be the
34
- * one `createdSubgroupId(admin, parentId, restricted, salt)` derives
35
- * (calimero-network/core#4244), so two concurrent creates for one id can never
36
- * name different creators. The layout of that variant changed.
37
- *
38
- * 18: owner-level ops need the account ROOT, not only a device of it
39
- * (calimero-network/core, root-guarded owner ops). `GroupOp::RootGuarded` and
40
- * `RootOp::RootGuarded` are appended and carry a root-signed proof, and the bare
41
- * `TransferOwnership`, `AdminChanged`, `GroupDelete` and TEE policy ops are
42
- * refused. No layout this module signs changes; see `../owner-op` for the proof.
43
- *
44
- * 19 is reserved for calimero-network/core#4269.
45
- *
46
- * 20: no layout change. A delegated `GroupCreated` whose executor is a TEE at
47
- * the namespace root now seats that TEE in the new subgroup
48
- * (calimero-network/core#4276), an apply-time rule older peers disagree on.
49
- * Core signs and checks at 20, so every op this module signs must too.
50
- *
51
- * 21: no layout change. An op concurrent with its signer's removal is void
52
- * (calimero-network/core#4275), an apply-time rule older peers disagree on.
53
- * Core signs and checks at 21 from 0.11.0-rc.75. 23.4.0 and 23.4.1 still sign
54
- * at 20, so against rc.75 every namespace op an account signs with them (a
55
- * join, a create, any governance op) is refused "schema version mismatch:
56
- * expected 21, got 20".
57
- *
58
- * 22: `GroupOp::SharedWritersRotated` is appended, a `SharedStorage` cell's
59
- * writer-set step (calimero-network/core#4263), and a capability revoke voids
60
- * the member's concurrent ops that needed a removed bit
61
- * (calimero-network/core#4453). No layout this module signs changes.
62
- *
63
- * 23: a TEE admission op carries the quote it was admitted on and a TEE
64
- * authority evidence op the credential it was made for
65
- * (calimero-network/core#4265). Neither is an op this module signs, so only
66
- * the version byte moves. Core signs and checks at 23 from 0.11.0-rc.81, the
67
- * first release with both 22 and 23.
68
- *
69
- * 24: no layout change. Only a subgroup's creator may open it - flip it to
70
- * `Open` (calimero-network/core#4522) - an apply-time rule older peers
71
- * disagree on, since they let any of its admins open it. A UI that offers
72
- * "make public" should offer it to the creator alone.
73
- *
74
- * 25: no layout change; refused device link/unlink/descope ops are never logged.
75
- *
76
- * 26: no layout change. A device link's scope is judged against every
77
- * application the group may have targeted at the link's cut, not its live
78
- * target (calimero-network/core#4603), so replicas agree on a link that races
79
- * a target change; older peers judge it by fold order.
80
- *
81
- * 27: no layout change. The namespace founder is seated as an admin by the
82
- * genesis op itself, so a founder who transfers ownership stays a member
83
- * (calimero-network/core#4625); older peers seat her from the root's current
84
- * owner and drop her.
85
- */
86
- export declare const SIGNED_NAMESPACE_OP_SCHEMA_VERSION = 27;
87
- /**
88
- * Borsh-encode a `SignedGroupOpenInvitation`.
89
- *
90
- * Re-encoded rather than passed through, because it has to go inside the op that
91
- * gets signed. That makes this function version-locked to core: a field added to
92
- * the invitation and omitted here produces an op whose inner invitation no longer
93
- * matches the inviter's signature over it. That exact drift, in client-py, broke
94
- * 93 scenarios — the failure surfaces as `invalid invitation signature` and points
95
- * nowhere near the encoder.
96
- */
97
- export declare function encodeSignedInvitation(signed: SignedGroupOpenInvitation): Uint8Array;
98
- export interface SignMemberJoinInput {
99
- /** The namespace being joined, 64 hex. */
100
- readonly namespaceId: string;
101
- /** The account joining, 64 hex — the subject of the credential. */
102
- readonly member: string;
103
- /** The invitation, as the admin API returned it. */
104
- readonly invitation: SignedGroupOpenInvitation;
105
- /**
106
- * The joiner's `AccountProof<DeviceCert>`, hex — from `signDeviceCert`.
107
- *
108
- * Its `sign_pk` must be the public key of `deviceSecret`; a peer checks that
109
- * when applying the join and refuses otherwise.
110
- */
111
- readonly credential: string;
112
- /**
113
- * The device signing secret, 64 hex. Must match the credential's `sign_pk`.
114
- *
115
- * Mutually exclusive with {@link SignMemberJoinInput.signer}. A browser should
116
- * prefer the signer: joining is the one governance op a keyholder signs for
117
- * itself, and it should not need the key in a form script can read.
118
- */
119
- readonly deviceSecret?: string;
120
- /**
121
- * The joiner's signer — use instead of
122
- * {@link SignMemberJoinInput.deviceSecret} when the key cannot be exported.
123
- */
124
- readonly signer?: Signer;
125
- /**
126
- * Heads this op is written against. Empty means "signed against an empty head"
127
- * — which is NOT the same as genesis, and a node with a non-empty head will
128
- * reject it as stale rather than treat it as a first op.
129
- */
130
- readonly parentOpHashes?: readonly string[];
131
- /** Anti-replay nonce; a node keeps a window per signer. */
132
- readonly nonce: number | bigint;
133
- /**
134
- * When the join happened, unix seconds. Defaults to now.
135
- *
136
- * Carried only by `MemberJoinedAt`, which is the variant used whenever the
137
- * invitation expires — and since core clamps every invitation to
138
- * `MAX_INVITATION_VALIDITY_SECS`, that is every invitation. The variant is
139
- * inferred from the invitation rather than selected by a flag: an expiring
140
- * invitation joined with plain `MemberJoined` is refused at apply with
141
- * "expiration is set but joined_at is absent", which is not a mistake worth
142
- * leaving available.
143
- */
144
- readonly joinedAt?: number | bigint;
145
- }
146
- /**
147
- * Sign a `MemberJoined` (or `MemberJoinedAt`) namespace op and return it
148
- * borsh-encoded and hex — the `signedOp` that `admitJoin` takes.
149
- */
150
- export declare function signMemberJoinOp(input: SignMemberJoinInput): Promise<string>;