@calimero-network/mero-js 25.3.0 → 25.3.2

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.
package/dist/index.mjs CHANGED
@@ -31,6 +31,244 @@ function createTimeoutSignal(timeoutMs) {
31
31
  return controller.signal;
32
32
  }
33
33
 
34
+ // src/crypto/internal.ts
35
+ var PKCS8_ED25519_PREFIX = new Uint8Array([
36
+ 48,
37
+ 46,
38
+ 2,
39
+ 1,
40
+ 0,
41
+ 48,
42
+ 5,
43
+ 6,
44
+ 3,
45
+ 43,
46
+ 101,
47
+ 112,
48
+ 4,
49
+ 34,
50
+ 4,
51
+ 32
52
+ ]);
53
+ function u32le(value) {
54
+ const out = new Uint8Array(4);
55
+ new DataView(out.buffer).setUint32(0, value, true);
56
+ return out;
57
+ }
58
+ function hex(bytes) {
59
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
60
+ }
61
+ function fromHex(value, label2, expectedBytes) {
62
+ const clean = value.trim();
63
+ if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length !== expectedBytes * 2) {
64
+ throw new Error(
65
+ `${label2} must be ${expectedBytes * 2} hex characters, got ${clean.length}`
66
+ );
67
+ }
68
+ const bytes = new Uint8Array(expectedBytes);
69
+ for (let i = 0; i < expectedBytes; i += 1) {
70
+ bytes[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
71
+ }
72
+ return bytes;
73
+ }
74
+ function fromHexUnsized(value, label2) {
75
+ const clean = value.trim();
76
+ if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length % 2 !== 0) {
77
+ throw new Error(`${label2} must be an even number of hex characters`);
78
+ }
79
+ const bytes = new Uint8Array(clean.length / 2);
80
+ for (let i = 0; i < bytes.length; i += 1) {
81
+ bytes[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
82
+ }
83
+ return bytes;
84
+ }
85
+ function u64le(value) {
86
+ const out = new Uint8Array(8);
87
+ new DataView(out.buffer).setBigUint64(0, BigInt(value), true);
88
+ return out;
89
+ }
90
+ function concat(...parts) {
91
+ const out = new Uint8Array(parts.reduce((total, p) => total + p.length, 0));
92
+ let offset = 0;
93
+ for (const part of parts) {
94
+ out.set(part, offset);
95
+ offset += part.length;
96
+ }
97
+ return out;
98
+ }
99
+ async function domainHash(domain, parts) {
100
+ const chunks = [u64le(domain.length), domain];
101
+ for (const part of parts) {
102
+ chunks.push(u64le(part.length), part);
103
+ }
104
+ return new Uint8Array(
105
+ await crypto.subtle.digest("SHA-256", concat(...chunks))
106
+ );
107
+ }
108
+ async function importSigningKey(secretHex) {
109
+ const seed = fromHex(secretHex, "deviceSecret", 32);
110
+ const pkcs8 = concat(PKCS8_ED25519_PREFIX, seed);
111
+ try {
112
+ return await crypto.subtle.importKey(
113
+ "pkcs8",
114
+ pkcs8,
115
+ { name: "Ed25519" },
116
+ false,
117
+ ["sign"]
118
+ );
119
+ } catch (cause) {
120
+ const detail = cause instanceof Error ? `: ${cause.message}` : "";
121
+ throw new Error(
122
+ "this runtime has no WebCrypto Ed25519, so a warrant cannot be signed here. Node 18.4+, Safari 17+, Firefox 129+ and Chrome 137+ have it; otherwise mint the warrant with `merod account warrant`" + detail
123
+ );
124
+ }
125
+ }
126
+ async function verifySignature(publicKey, signature, payload) {
127
+ const key = await crypto.subtle.importKey(
128
+ "raw",
129
+ fromHex(publicKey, "publicKey", 32),
130
+ { name: "Ed25519" },
131
+ false,
132
+ ["verify"]
133
+ );
134
+ return crypto.subtle.verify({ name: "Ed25519" }, key, signature, payload);
135
+ }
136
+ async function derivePublicKey(secretHex) {
137
+ const seed = fromHex(secretHex, "deviceSecret", 32);
138
+ const pkcs8 = concat(PKCS8_ED25519_PREFIX, seed);
139
+ const key = await crypto.subtle.importKey(
140
+ "pkcs8",
141
+ pkcs8,
142
+ { name: "Ed25519" },
143
+ true,
144
+ ["sign"]
145
+ );
146
+ const jwk = await crypto.subtle.exportKey("jwk", key);
147
+ const raw = jwk.x;
148
+ return Uint8Array.from(
149
+ atob(raw.replace(/-/g, "+").replace(/_/g, "/")),
150
+ (c) => c.charCodeAt(0)
151
+ );
152
+ }
153
+
154
+ // src/request/request-sig.ts
155
+ var SIGN_DOMAIN = new TextEncoder().encode("calimero.auth.request.v2");
156
+ var BODY_DOMAIN = new TextEncoder().encode("calimero.auth.request.body.v1");
157
+ function borshString(value) {
158
+ const bytes = new TextEncoder().encode(value);
159
+ return concat(u32le(bytes.length), bytes);
160
+ }
161
+ async function requestBodyHash(body = new Uint8Array(0)) {
162
+ const bytes = typeof body === "string" ? new TextEncoder().encode(body) : body;
163
+ return domainHash(BODY_DOMAIN, [bytes]);
164
+ }
165
+ async function signRequest(input) {
166
+ const key = await importSigningKey(input.signerSecret);
167
+ const bodyHash = await requestBodyHash(input.body);
168
+ const issuedAt = u64le(input.issuedAt);
169
+ const expiresAt = u64le(input.expiresAt);
170
+ const encoder = new TextEncoder();
171
+ const preimage = await domainHash(SIGN_DOMAIN, [
172
+ encoder.encode(input.method),
173
+ encoder.encode(input.path),
174
+ bodyHash,
175
+ issuedAt,
176
+ expiresAt
177
+ ]);
178
+ const signature = new Uint8Array(
179
+ await crypto.subtle.sign({ name: "Ed25519" }, key, preimage)
180
+ );
181
+ return hex(
182
+ concat(
183
+ borshString(input.method),
184
+ borshString(input.path),
185
+ bodyHash,
186
+ issuedAt,
187
+ expiresAt,
188
+ signature
189
+ )
190
+ );
191
+ }
192
+
193
+ // src/request-proof/request-proof.ts
194
+ var utf8 = (text) => new TextEncoder().encode(text);
195
+ var REQUEST_PROOF_HEADER = "X-Calimero-Proof";
196
+ var DEFAULT_VALID_FOR = 300;
197
+ async function signRequestProof(input) {
198
+ const { method, path, credential } = input;
199
+ if (!method) throw new Error("signRequestProof needs the method the request will use");
200
+ if (!path.startsWith("/")) {
201
+ throw new Error(`signRequestProof needs a path, got ${JSON.stringify(path)}`);
202
+ }
203
+ const bodyHash = await requestBodyHash(input.body);
204
+ const issuedAt = Math.floor(input.now ?? Date.now() / 1e3);
205
+ const expiresAt = issuedAt + (input.validForSeconds ?? DEFAULT_VALID_FOR);
206
+ const payload = await domainHash(SIGN_DOMAIN, [
207
+ utf8(method),
208
+ utf8(path),
209
+ bodyHash,
210
+ u64le(issuedAt),
211
+ u64le(expiresAt)
212
+ ]);
213
+ const signature = await signPayload(payload, input);
214
+ if (signature.length !== 64) {
215
+ throw new Error(`the signer returned ${signature.length} bytes, not a 64-byte signature`);
216
+ }
217
+ const requestSig = concat(
218
+ u32le(utf8(method).length),
219
+ utf8(method),
220
+ u32le(utf8(path).length),
221
+ utf8(path),
222
+ bodyHash,
223
+ u64le(issuedAt),
224
+ u64le(expiresAt),
225
+ signature
226
+ );
227
+ const accountProof = hexToBytes(credential, "credential");
228
+ return hex(concat(accountProof, new Uint8Array([0]), requestSig));
229
+ }
230
+ async function signPayload(payload, input) {
231
+ if (input.signer) return input.signer.sign(payload);
232
+ if (!input.deviceSecret) {
233
+ throw new Error("signRequestProof needs either `deviceSecret` or `signer`");
234
+ }
235
+ const key = await importSigningKey(input.deviceSecret);
236
+ return new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, key, payload));
237
+ }
238
+ function hexToBytes(value, label2) {
239
+ const clean = value.trim();
240
+ if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length % 2 !== 0) {
241
+ throw new Error(`${label2} is not an even-length hex string`);
242
+ }
243
+ return fromHex(clean, label2, clean.length / 2);
244
+ }
245
+
246
+ // src/request-proof/authorizer.ts
247
+ function createProofAuthorizer(credential) {
248
+ if (!credential.credential) {
249
+ throw new Error("a proof authorizer needs the `credential` from signDeviceCert");
250
+ }
251
+ if (!credential.deviceSecret && !credential.signer) {
252
+ throw new Error("a proof authorizer needs either `deviceSecret` or `signer`");
253
+ }
254
+ return async ({ method, path, body }) => {
255
+ const proof = await signRequestProof({
256
+ method,
257
+ path,
258
+ body,
259
+ credential: credential.credential,
260
+ deviceSecret: credential.deviceSecret,
261
+ signer: credential.signer,
262
+ validForSeconds: credential.validForSeconds
263
+ });
264
+ return { [REQUEST_PROOF_HEADER]: proof };
265
+ };
266
+ }
267
+ function signedPath(url2) {
268
+ const parsed = new URL(url2, globalThis.location?.href ?? "http://localhost");
269
+ return parsed.pathname + parsed.search;
270
+ }
271
+
34
272
  // src/http-client/web-client.ts
35
273
  var TERMINAL_AUTH_ERRORS = /* @__PURE__ */ new Set(["token_reuse", "token_revoked"]);
36
274
  function extractErrorMessage(bodyText) {
@@ -86,13 +324,31 @@ function headersToRecord(headers) {
86
324
  });
87
325
  return record;
88
326
  }
89
- function pathForProof(url2) {
327
+ function withoutBareQuery(url2) {
328
+ let parsed;
90
329
  try {
91
- return new URL(url2).pathname;
330
+ parsed = new URL(url2, globalThis.location?.href);
92
331
  } catch {
93
- const query = url2.indexOf("?");
94
- return query === -1 ? url2 : url2.slice(0, query);
332
+ return url2;
95
333
  }
334
+ if (parsed.search === "") parsed.search = "";
335
+ return parsed.href;
336
+ }
337
+ function carriesProof(headers) {
338
+ return Object.keys(headers).some(
339
+ (name) => name.toLowerCase() === REQUEST_PROOF_HEADER.toLowerCase()
340
+ );
341
+ }
342
+ function refuseProofRedirect(response, url2) {
343
+ const redirected = response.type === "opaqueredirect" || response.status >= 300 && response.status < 400;
344
+ if (!redirected) return;
345
+ throw new HTTPError(
346
+ 0,
347
+ "Redirect Not Followed",
348
+ url2,
349
+ response.headers,
350
+ `the node answered with a redirect; a request carrying ${REQUEST_PROOF_HEADER} is not followed to another location`
351
+ );
96
352
  }
97
353
  var WebHttpClient = class {
98
354
  constructor(transport2) {
@@ -157,7 +413,7 @@ var WebHttpClient = class {
157
413
  }
158
414
  async makeRequest(path, init, retryCount = 0, requestStartTime) {
159
415
  const MAX_RETRY_ATTEMPTS = 1;
160
- const url2 = this.buildUrl(path);
416
+ const url2 = withoutBareQuery(this.buildUrl(path));
161
417
  const startTime = requestStartTime ?? Date.now();
162
418
  const signal = this.createAbortSignal(init);
163
419
  const headers = await this.buildHeaders(init?.headers);
@@ -182,7 +438,7 @@ var WebHttpClient = class {
182
438
  headersObj,
183
439
  await this.transport.authorizeRequest({
184
440
  method,
185
- path: new URL(url2).pathname,
441
+ path: signedPath(url2),
186
442
  // Narrowed by the guard above; `BodyInit` is wider than what can be
187
443
  // signed.
188
444
  body
@@ -242,15 +498,20 @@ var WebHttpClient = class {
242
498
  if (this.transport.getProof) {
243
499
  const proof = await this.transport.getProof({
244
500
  method: requestInit.method ?? "GET",
245
- path: pathForProof(url2),
501
+ path: signedPath(url2),
246
502
  body: requestInit.body
247
503
  });
248
504
  if (proof) {
249
505
  headersObj["X-Calimero-Proof"] = proof;
250
506
  }
251
507
  }
508
+ const proofAttached = carriesProof(headersObj);
509
+ if (proofAttached && requestInit.redirect !== "error") {
510
+ requestInit.redirect = "manual";
511
+ }
252
512
  try {
253
513
  const response = await this.transport.fetch(url2, requestInit);
514
+ if (proofAttached) refuseProofRedirect(response, url2);
254
515
  if (!response.ok) {
255
516
  const bodyText = await this.getBodyText(response);
256
517
  const httpError = new HTTPError(
@@ -2423,204 +2684,123 @@ function createAdminApiClientFromHttpClient(httpClient, _config) {
2423
2684
  return new AdminApiClient(httpClient);
2424
2685
  }
2425
2686
 
2426
- // src/auth/index.ts
2427
- function parseAuthCallback(url2) {
2428
- try {
2429
- const hashIndex = url2.indexOf("#");
2430
- if (hashIndex === -1) return null;
2431
- const hash = url2.substring(hashIndex + 1);
2432
- const params = new URLSearchParams(hash);
2433
- const accessToken = params.get("access_token");
2434
- if (!accessToken) return null;
2435
- return {
2436
- accessToken,
2437
- refreshToken: params.get("refresh_token") ?? "",
2438
- applicationId: params.get("application_id") ?? "",
2439
- contextId: params.get("context_id") ?? "",
2440
- contextIdentity: params.get("context_identity") ?? "",
2441
- nodeUrl: params.get("node_url") ?? ""
2442
- };
2443
- } catch {
2444
- return null;
2445
- }
2446
- }
2447
- function buildAuthLoginUrl(nodeUrl, opts) {
2448
- const params = new URLSearchParams();
2449
- params.set("callback-url", opts.callbackUrl);
2450
- if (opts.permissions && opts.permissions.length > 0) {
2451
- params.set("permissions", opts.permissions.join(","));
2452
- }
2453
- params.set("mode", opts.mode);
2454
- if (opts.packageName) {
2455
- params.set("package-name", opts.packageName);
2456
- if (opts.packageVersion) {
2457
- params.set("package-version", opts.packageVersion);
2458
- }
2459
- if (opts.registryUrl) {
2460
- params.set("registry-url", opts.registryUrl);
2461
- }
2462
- }
2463
- const base = nodeUrl.replace(/\/+$/, "");
2464
- return `${base}/auth/login?${params.toString()}`;
2465
- }
2466
-
2467
- // src/crypto/internal.ts
2468
- var PKCS8_ED25519_PREFIX = new Uint8Array([
2469
- 48,
2470
- 46,
2471
- 2,
2472
- 1,
2473
- 0,
2474
- 48,
2475
- 5,
2476
- 6,
2477
- 3,
2478
- 43,
2479
- 101,
2480
- 112,
2481
- 4,
2482
- 34,
2483
- 4,
2484
- 32
2485
- ]);
2486
- function u32le(value) {
2487
- const out = new Uint8Array(4);
2488
- new DataView(out.buffer).setUint32(0, value, true);
2489
- return out;
2490
- }
2491
- function hex(bytes) {
2492
- return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
2493
- }
2494
- function fromHex(value, label2, expectedBytes) {
2495
- const clean = value.trim();
2496
- if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length !== expectedBytes * 2) {
2497
- throw new Error(
2498
- `${label2} must be ${expectedBytes * 2} hex characters, got ${clean.length}`
2499
- );
2500
- }
2501
- const bytes = new Uint8Array(expectedBytes);
2502
- for (let i = 0; i < expectedBytes; i += 1) {
2503
- bytes[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
2504
- }
2505
- return bytes;
2506
- }
2507
- function fromHexUnsized(value, label2) {
2508
- const clean = value.trim();
2509
- if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length % 2 !== 0) {
2510
- throw new Error(`${label2} must be an even number of hex characters`);
2511
- }
2512
- const bytes = new Uint8Array(clean.length / 2);
2513
- for (let i = 0; i < bytes.length; i += 1) {
2514
- bytes[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
2515
- }
2516
- return bytes;
2517
- }
2518
- function u64le(value) {
2519
- const out = new Uint8Array(8);
2520
- new DataView(out.buffer).setBigUint64(0, BigInt(value), true);
2521
- return out;
2522
- }
2523
- function concat(...parts) {
2524
- const out = new Uint8Array(parts.reduce((total, p) => total + p.length, 0));
2525
- let offset = 0;
2526
- for (const part of parts) {
2527
- out.set(part, offset);
2528
- offset += part.length;
2529
- }
2530
- return out;
2531
- }
2532
- async function domainHash(domain, parts) {
2533
- const chunks = [u64le(domain.length), domain];
2534
- for (const part of parts) {
2535
- chunks.push(u64le(part.length), part);
2536
- }
2537
- return new Uint8Array(
2538
- await crypto.subtle.digest("SHA-256", concat(...chunks))
2539
- );
2540
- }
2541
- async function importSigningKey(secretHex) {
2542
- const seed = fromHex(secretHex, "deviceSecret", 32);
2543
- const pkcs8 = concat(PKCS8_ED25519_PREFIX, seed);
2687
+ // src/cloud/link-state.ts
2688
+ var KEY = "calimero.cloud.link.pending";
2689
+ var STATE_PARAM = "link_state";
2690
+ var LIFETIME_MS = 10 * 60 * 1e3;
2691
+ var ACCOUNT_HEX = /^[0-9a-f]{64}$/;
2692
+ var MAX_PENDING = 20;
2693
+ function readPending(key, now) {
2544
2694
  try {
2545
- return await crypto.subtle.importKey(
2546
- "pkcs8",
2547
- pkcs8,
2548
- { name: "Ed25519" },
2549
- false,
2550
- ["sign"]
2551
- );
2552
- } catch (cause) {
2553
- const detail = cause instanceof Error ? `: ${cause.message}` : "";
2554
- throw new Error(
2555
- "this runtime has no WebCrypto Ed25519, so a warrant cannot be signed here. Node 18.4+, Safari 17+, Firefox 129+ and Chrome 137+ have it; otherwise mint the warrant with `merod account warrant`" + detail
2695
+ const raw = localStorage.getItem(key);
2696
+ const pending = raw ? JSON.parse(raw) : {};
2697
+ return Object.fromEntries(
2698
+ Object.entries(pending).filter(
2699
+ ([, p]) => p && typeof p.expiresAt === "number" && p.expiresAt > now
2700
+ )
2556
2701
  );
2702
+ } catch {
2703
+ return {};
2557
2704
  }
2558
2705
  }
2559
- async function verifySignature(publicKey, signature, payload) {
2560
- const key = await crypto.subtle.importKey(
2561
- "raw",
2562
- fromHex(publicKey, "publicKey", 32),
2563
- { name: "Ed25519" },
2564
- false,
2565
- ["verify"]
2566
- );
2567
- return crypto.subtle.verify({ name: "Ed25519" }, key, signature, payload);
2706
+ function writePending(key, pending) {
2707
+ try {
2708
+ localStorage.setItem(key, JSON.stringify(pending));
2709
+ } catch {
2710
+ }
2568
2711
  }
2569
- async function derivePublicKey(secretHex) {
2570
- const seed = fromHex(secretHex, "deviceSecret", 32);
2571
- const pkcs8 = concat(PKCS8_ED25519_PREFIX, seed);
2572
- const key = await crypto.subtle.importKey(
2573
- "pkcs8",
2574
- pkcs8,
2575
- { name: "Ed25519" },
2576
- true,
2577
- ["sign"]
2578
- );
2579
- const jwk = await crypto.subtle.exportKey("jwk", key);
2580
- const raw = jwk.x;
2581
- return Uint8Array.from(
2582
- atob(raw.replace(/-/g, "+").replace(/_/g, "/")),
2583
- (c) => c.charCodeAt(0)
2712
+ function beginState(key, payload) {
2713
+ const now = Date.now();
2714
+ const pending = readPending(key, now);
2715
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
2716
+ const state = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(
2717
+ ""
2584
2718
  );
2719
+ pending[state] = { ...payload, expiresAt: now + LIFETIME_MS };
2720
+ writePending(key, Object.fromEntries(Object.entries(pending).slice(-MAX_PENDING)));
2721
+ return state;
2722
+ }
2723
+ function takeState(key, state) {
2724
+ const pending = readPending(key, Date.now());
2725
+ if (!state || !Object.prototype.hasOwnProperty.call(pending, state))
2726
+ return null;
2727
+ const entry = pending[state];
2728
+ delete pending[state];
2729
+ writePending(key, pending);
2730
+ return entry;
2731
+ }
2732
+ function beginLinkState(account, returnTo) {
2733
+ if (!ACCOUNT_HEX.test(account)) {
2734
+ throw new Error("accountId must be 64 lowercase hex characters");
2735
+ }
2736
+ const url2 = new URL(returnTo);
2737
+ url2.searchParams.set(STATE_PARAM, beginState(KEY, { account }));
2738
+ return url2.toString();
2739
+ }
2740
+ function takeLinkState(search) {
2741
+ return takeState(KEY, new URLSearchParams(search).get(STATE_PARAM));
2742
+ }
2743
+ function withoutLinkState(search) {
2744
+ const params = new URLSearchParams(search);
2745
+ params.delete(STATE_PARAM);
2746
+ const rest = params.toString();
2747
+ return rest ? `?${rest}` : "";
2585
2748
  }
2586
2749
 
2587
- // src/request/request-sig.ts
2588
- var SIGN_DOMAIN = new TextEncoder().encode("calimero.auth.request.v1");
2589
- var BODY_DOMAIN = new TextEncoder().encode("calimero.auth.request.body.v1");
2590
- function borshString(value) {
2591
- const bytes = new TextEncoder().encode(value);
2592
- return concat(u32le(bytes.length), bytes);
2750
+ // src/auth/index.ts
2751
+ var PENDING_KEY = "calimero.auth.login.pending";
2752
+ var STATE_PARAM2 = "auth_state";
2753
+ function parseAuthCallback(url2, options = {}) {
2754
+ try {
2755
+ const hashIndex = url2.indexOf("#");
2756
+ if (hashIndex === -1) return null;
2757
+ const hash = url2.substring(hashIndex + 1);
2758
+ const params = new URLSearchParams(hash);
2759
+ const accessToken = params.get("access_token");
2760
+ if (!accessToken) return null;
2761
+ if (!options.allowUnsolicited && !takeState(PENDING_KEY, stateOf(url2, hashIndex))) {
2762
+ console.warn(
2763
+ "[mero-js] ignoring auth callback tokens: they answer no login begun in this browser"
2764
+ );
2765
+ return null;
2766
+ }
2767
+ return {
2768
+ accessToken,
2769
+ refreshToken: params.get("refresh_token") ?? "",
2770
+ applicationId: params.get("application_id") ?? "",
2771
+ contextId: params.get("context_id") ?? "",
2772
+ contextIdentity: params.get("context_identity") ?? "",
2773
+ nodeUrl: params.get("node_url") ?? ""
2774
+ };
2775
+ } catch {
2776
+ return null;
2777
+ }
2593
2778
  }
2594
- async function requestBodyHash(body = new Uint8Array(0)) {
2595
- const bytes = typeof body === "string" ? new TextEncoder().encode(body) : body;
2596
- return domainHash(BODY_DOMAIN, [bytes]);
2779
+ function stateOf(url2, hashIndex) {
2780
+ const queryIndex = url2.indexOf("?");
2781
+ if (queryIndex === -1 || queryIndex > hashIndex) return null;
2782
+ return new URLSearchParams(url2.substring(queryIndex + 1, hashIndex)).get(STATE_PARAM2);
2597
2783
  }
2598
- async function signRequest(input) {
2599
- const key = await importSigningKey(input.signerSecret);
2600
- const bodyHash = await requestBodyHash(input.body);
2601
- const issuedAt = u64le(input.issuedAt);
2602
- const expiresAt = u64le(input.expiresAt);
2603
- const encoder = new TextEncoder();
2604
- const preimage = await domainHash(SIGN_DOMAIN, [
2605
- encoder.encode(input.method),
2606
- encoder.encode(input.path),
2607
- bodyHash,
2608
- issuedAt,
2609
- expiresAt
2610
- ]);
2611
- const signature = new Uint8Array(
2612
- await crypto.subtle.sign({ name: "Ed25519" }, key, preimage)
2613
- );
2614
- return hex(
2615
- concat(
2616
- borshString(input.method),
2617
- borshString(input.path),
2618
- bodyHash,
2619
- issuedAt,
2620
- expiresAt,
2621
- signature
2622
- )
2623
- );
2784
+ function buildAuthLoginUrl(nodeUrl, opts) {
2785
+ const callback = new URL(opts.callbackUrl);
2786
+ callback.searchParams.set(STATE_PARAM2, beginState(PENDING_KEY, {}));
2787
+ const params = new URLSearchParams();
2788
+ params.set("callback-url", callback.toString());
2789
+ if (opts.permissions && opts.permissions.length > 0) {
2790
+ params.set("permissions", opts.permissions.join(","));
2791
+ }
2792
+ params.set("mode", opts.mode);
2793
+ if (opts.packageName) {
2794
+ params.set("package-name", opts.packageName);
2795
+ if (opts.packageVersion) {
2796
+ params.set("package-version", opts.packageVersion);
2797
+ }
2798
+ if (opts.registryUrl) {
2799
+ params.set("registry-url", opts.registryUrl);
2800
+ }
2801
+ }
2802
+ const base = nodeUrl.replace(/\/+$/, "");
2803
+ return `${base}/auth/login?${params.toString()}`;
2624
2804
  }
2625
2805
 
2626
2806
  // src/request/caller-proof.ts
@@ -2851,13 +3031,17 @@ var SseClient = class {
2851
3031
  this.abortController = new AbortController();
2852
3032
  try {
2853
3033
  const url2 = `${this.baseUrl}/sse`;
3034
+ const headers = {
3035
+ ...await this.authorize({ method: "GET", path: signedPath(url2) }),
3036
+ "Accept": "text/event-stream"
3037
+ };
3038
+ const proofAttached = carriesProof(headers);
2854
3039
  const response = await this.fetchImpl(url2, {
2855
- headers: {
2856
- ...await this.authorize({ method: "GET", path: new URL(url2).pathname }),
2857
- "Accept": "text/event-stream"
2858
- },
2859
- signal: this.abortController.signal
3040
+ headers,
3041
+ signal: this.abortController.signal,
3042
+ ...proofAttached && { redirect: "manual" }
2860
3043
  });
3044
+ if (proofAttached) refuseProofRedirect(response, url2);
2861
3045
  if (!response.ok) {
2862
3046
  const terminal = isTerminalAuthFailure(response);
2863
3047
  const bodyText = await response.text().catch(() => void 0);
@@ -3055,15 +3239,19 @@ var SseClient = class {
3055
3239
  if (ids.groupIds && ids.groupIds.length > 0) params.groupIds = ids.groupIds;
3056
3240
  const url2 = `${this.baseUrl}/sse/subscription`;
3057
3241
  const body = JSON.stringify({ id: this.sessionId, method, params });
3242
+ const headers = {
3243
+ ...await this.authorize({ method: "POST", path: signedPath(url2), body }),
3244
+ "Content-Type": "application/json"
3245
+ };
3246
+ const proofAttached = carriesProof(headers);
3058
3247
  const response = await this.fetchImpl(url2, {
3059
3248
  method: "POST",
3060
- headers: {
3061
- ...await this.authorize({ method: "POST", path: new URL(url2).pathname, body }),
3062
- "Content-Type": "application/json"
3063
- },
3249
+ headers,
3064
3250
  body,
3065
- signal: timeout.signal
3251
+ signal: timeout.signal,
3252
+ ...proofAttached && { redirect: "manual" }
3066
3253
  });
3254
+ if (proofAttached) refuseProofRedirect(response, url2);
3067
3255
  if (!response.ok) {
3068
3256
  const terminal = isTerminalAuthFailure(response);
3069
3257
  const bodyText = await response.text().catch(() => void 0);
@@ -6183,7 +6371,7 @@ var MAX_HANDSHAKE_REPLY = 1024;
6183
6371
  var RENEW_BEFORE_MS = 6e4;
6184
6372
  var TRANSPORT_BINDING_DOMAIN = "calimero.tee-attest.transport-key.v1";
6185
6373
  var NULL_BODY_STATUSES = [101, 204, 205, 304];
6186
- var utf8 = new TextEncoder();
6374
+ var utf82 = new TextEncoder();
6187
6375
  var StaleTransportKeyError = class extends Error {
6188
6376
  constructor() {
6189
6377
  super("The node no longer holds the attested transport key; attest again for the current one");
@@ -6202,7 +6390,7 @@ var SealedTransportError = class extends Error {
6202
6390
  async function transportKeyBinding(inner, transportKey) {
6203
6391
  const digest = await crypto.subtle.digest(
6204
6392
  "SHA-256",
6205
- concat(utf8.encode(TRANSPORT_BINDING_DOMAIN), inner, transportKey)
6393
+ concat(utf82.encode(TRANSPORT_BINDING_DOMAIN), inner, transportKey)
6206
6394
  );
6207
6395
  return new Uint8Array(digest);
6208
6396
  }
@@ -6406,7 +6594,7 @@ function nonce(requestId, index) {
6406
6594
  return bytes;
6407
6595
  }
6408
6596
  function joinInner(head, body) {
6409
- const encoded = utf8.encode(JSON.stringify(head));
6597
+ const encoded = utf82.encode(JSON.stringify(head));
6410
6598
  const len = new Uint8Array(4);
6411
6599
  new DataView(len.buffer).setUint32(0, encoded.length, false);
6412
6600
  return concat(len, encoded, body);
@@ -6896,7 +7084,7 @@ var FULCIO_ISSUER_V1 = "1.3.6.1.4.1.57264.1.1";
6896
7084
  var SAN_URI = 134;
6897
7085
  var UTF8_STRING = 12;
6898
7086
  var SEQUENCE2 = 48;
6899
- var utf82 = new TextEncoder();
7087
+ var utf83 = new TextEncoder();
6900
7088
  var fromUtf8 = (bytes) => new TextDecoder().decode(bytes);
6901
7089
  async function verifyCosignBundle(body, bundle, signer, signature) {
6902
7090
  const { base64Signature, cert, rekorBundle } = cosignBundle(bundle);
@@ -6909,7 +7097,7 @@ async function verifyCosignBundle(body, bundle, signer, signature) {
6909
7097
  const leaf = parseCertificate(leafDer);
6910
7098
  const log = REKOR_LOGS.find((candidate) => candidate.logId === payload.logID.toLowerCase());
6911
7099
  if (!log) throw new Error(`The bundle names Rekor log ${payload.logID}, which the Sigstore trust root does not know`);
6912
- const canonical = utf82.encode(
7100
+ const canonical = utf83.encode(
6913
7101
  `{"body":${JSON.stringify(payload.body)},"integratedTime":${payload.integratedTime},"logID":${JSON.stringify(payload.logID)},"logIndex":${payload.logIndex}}`
6914
7102
  );
6915
7103
  const setVerifies = await verifyEcdsa(
@@ -8647,92 +8835,6 @@ var RelayObserver = class {
8647
8835
  }
8648
8836
  };
8649
8837
 
8650
- // src/request-proof/request-proof.ts
8651
- var utf83 = (text) => new TextEncoder().encode(text);
8652
- var REQUEST_SIGN_DOMAIN = utf83("calimero.auth.request.v1");
8653
- var REQUEST_BODY_DOMAIN = utf83("calimero.auth.request.body.v1");
8654
- var REQUEST_PROOF_HEADER = "X-Calimero-Proof";
8655
- var DEFAULT_VALID_FOR = 300;
8656
- async function signRequestProof(input) {
8657
- const { method, path, credential } = input;
8658
- if (!method) throw new Error("signRequestProof needs the method the request will use");
8659
- if (!path.startsWith("/")) {
8660
- throw new Error(`signRequestProof needs a path, got ${JSON.stringify(path)}`);
8661
- }
8662
- if (path.includes("?")) {
8663
- throw new Error(
8664
- `the path must carry no query string, got ${JSON.stringify(path)} \u2014 a query is excluded from the signature on purpose, because a proxy may rewrite one. Anything that must be bound belongs in the body.`
8665
- );
8666
- }
8667
- const bodyBytes2 = input.body === void 0 ? new Uint8Array() : typeof input.body === "string" ? utf83(input.body) : input.body;
8668
- const bodyHash = await domainHash(REQUEST_BODY_DOMAIN, [bodyBytes2]);
8669
- const issuedAt = Math.floor(input.now ?? Date.now() / 1e3);
8670
- const expiresAt = issuedAt + (input.validForSeconds ?? DEFAULT_VALID_FOR);
8671
- const payload = await domainHash(REQUEST_SIGN_DOMAIN, [
8672
- utf83(method),
8673
- utf83(path),
8674
- bodyHash,
8675
- u64le(issuedAt),
8676
- u64le(expiresAt)
8677
- ]);
8678
- const signature = await signPayload(payload, input);
8679
- if (signature.length !== 64) {
8680
- throw new Error(`the signer returned ${signature.length} bytes, not a 64-byte signature`);
8681
- }
8682
- const requestSig = concat(
8683
- u32le(utf83(method).length),
8684
- utf83(method),
8685
- u32le(utf83(path).length),
8686
- utf83(path),
8687
- bodyHash,
8688
- u64le(issuedAt),
8689
- u64le(expiresAt),
8690
- signature
8691
- );
8692
- const accountProof = hexToBytes(credential, "credential");
8693
- return hex(concat(accountProof, new Uint8Array([0]), requestSig));
8694
- }
8695
- async function signPayload(payload, input) {
8696
- if (input.signer) return input.signer.sign(payload);
8697
- if (!input.deviceSecret) {
8698
- throw new Error("signRequestProof needs either `deviceSecret` or `signer`");
8699
- }
8700
- const key = await importSigningKey(input.deviceSecret);
8701
- return new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, key, payload));
8702
- }
8703
- function hexToBytes(value, label2) {
8704
- const clean = value.trim();
8705
- if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length % 2 !== 0) {
8706
- throw new Error(`${label2} is not an even-length hex string`);
8707
- }
8708
- return fromHex(clean, label2, clean.length / 2);
8709
- }
8710
-
8711
- // src/request-proof/authorizer.ts
8712
- function createProofAuthorizer(credential) {
8713
- if (!credential.credential) {
8714
- throw new Error("a proof authorizer needs the `credential` from signDeviceCert");
8715
- }
8716
- if (!credential.deviceSecret && !credential.signer) {
8717
- throw new Error("a proof authorizer needs either `deviceSecret` or `signer`");
8718
- }
8719
- return async ({ method, path, body }) => {
8720
- const proof = await signRequestProof({
8721
- method,
8722
- path,
8723
- body,
8724
- credential: credential.credential,
8725
- deviceSecret: credential.deviceSecret,
8726
- signer: credential.signer,
8727
- validForSeconds: credential.validForSeconds
8728
- });
8729
- return { [REQUEST_PROOF_HEADER]: proof };
8730
- };
8731
- }
8732
- function signedPath(url2) {
8733
- return new URL(url2).pathname;
8734
- }
8735
-
8736
8838
  // src/presence/statement.ts
8737
8839
  var PRESENCE_DOMAIN = new TextEncoder().encode("calimero/presence/1");
8738
8840
  async function stateHash(state) {
@@ -9099,12 +9201,12 @@ function createMeroClient(config) {
9099
9201
  }
9100
9202
 
9101
9203
  // src/account/session.ts
9102
- var KEY = "calimero.delegated.connection";
9204
+ var KEY2 = "calimero.delegated.connection";
9103
9205
  var CREDENTIAL_KEY = "calimero.delegated.credential";
9104
9206
  function readDelegatedSession() {
9105
9207
  if (typeof sessionStorage === "undefined") return null;
9106
9208
  try {
9107
- const raw = sessionStorage.getItem(KEY);
9209
+ const raw = sessionStorage.getItem(KEY2);
9108
9210
  return raw ? JSON.parse(raw) : null;
9109
9211
  } catch {
9110
9212
  return null;
@@ -9112,13 +9214,13 @@ function readDelegatedSession() {
9112
9214
  }
9113
9215
  function saveDelegatedSession(s) {
9114
9216
  try {
9115
- sessionStorage.setItem(KEY, JSON.stringify(s));
9217
+ sessionStorage.setItem(KEY2, JSON.stringify(s));
9116
9218
  } catch {
9117
9219
  }
9118
9220
  }
9119
9221
  function clearDelegatedSession() {
9120
9222
  try {
9121
- sessionStorage.removeItem(KEY);
9223
+ sessionStorage.removeItem(KEY2);
9122
9224
  } catch {
9123
9225
  }
9124
9226
  }
@@ -9916,64 +10018,6 @@ function base643(bytes) {
9916
10018
  return btoa(binary);
9917
10019
  }
9918
10020
 
9919
- // src/cloud/link-state.ts
9920
- var KEY2 = "calimero.cloud.link.pending";
9921
- var STATE_PARAM = "link_state";
9922
- var LIFETIME_MS = 10 * 60 * 1e3;
9923
- var ACCOUNT_HEX = /^[0-9a-f]{64}$/;
9924
- var MAX_PENDING = 20;
9925
- function readPending(now) {
9926
- try {
9927
- const raw = localStorage.getItem(KEY2);
9928
- const pending = raw ? JSON.parse(raw) : {};
9929
- return Object.fromEntries(
9930
- Object.entries(pending).filter(
9931
- ([, p]) => p && typeof p.expiresAt === "number" && p.expiresAt > now
9932
- )
9933
- );
9934
- } catch {
9935
- return {};
9936
- }
9937
- }
9938
- function writePending(pending) {
9939
- try {
9940
- localStorage.setItem(KEY2, JSON.stringify(pending));
9941
- } catch {
9942
- }
9943
- }
9944
- function beginLinkState(account, returnTo) {
9945
- if (!ACCOUNT_HEX.test(account)) {
9946
- throw new Error("accountId must be 64 lowercase hex characters");
9947
- }
9948
- const url2 = new URL(returnTo);
9949
- const now = Date.now();
9950
- const pending = readPending(now);
9951
- const bytes = crypto.getRandomValues(new Uint8Array(16));
9952
- const state = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(
9953
- ""
9954
- );
9955
- pending[state] = { account, expiresAt: now + LIFETIME_MS };
9956
- writePending(Object.fromEntries(Object.entries(pending).slice(-MAX_PENDING)));
9957
- url2.searchParams.set(STATE_PARAM, state);
9958
- return url2.toString();
9959
- }
9960
- function takeLinkState(search) {
9961
- const state = new URLSearchParams(search).get(STATE_PARAM);
9962
- const pending = readPending(Date.now());
9963
- if (!state || !Object.prototype.hasOwnProperty.call(pending, state))
9964
- return null;
9965
- const entry = pending[state];
9966
- delete pending[state];
9967
- writePending(pending);
9968
- return entry;
9969
- }
9970
- function withoutLinkState(search) {
9971
- const params = new URLSearchParams(search);
9972
- params.delete(STATE_PARAM);
9973
- const rest = params.toString();
9974
- return rest ? `?${rest}` : "";
9975
- }
9976
-
9977
10021
  // src/cloud/link-redirect.ts
9978
10022
  var DEFAULT_CLOUD_WEB_URL = "https://cloud.calimero.network";
9979
10023
  function cloudLinkUrl(options) {
@@ -10030,7 +10074,8 @@ async function completeCloudLink(options) {
10030
10074
  var DEVICE_PARAM = "enrol-device";
10031
10075
  var KEM_PARAM = "enrol-kem";
10032
10076
  var CALLBACK_PARAM = "callback-url";
10033
- var STATE_PARAM2 = "state";
10077
+ var STATE_PARAM3 = "state";
10078
+ var PENDING_KEY2 = "calimero.device.enrol.pending";
10034
10079
  function deviceEnrolmentUrl(options) {
10035
10080
  assertHex(options.devicePublicKey, "devicePublicKey");
10036
10081
  assertHex(options.kemPublicKey, "kemPublicKey");
@@ -10040,7 +10085,7 @@ function deviceEnrolmentUrl(options) {
10040
10085
  url2.searchParams.set(DEVICE_PARAM, options.devicePublicKey);
10041
10086
  url2.searchParams.set(KEM_PARAM, options.kemPublicKey);
10042
10087
  url2.searchParams.set(CALLBACK_PARAM, options.returnTo);
10043
- if (options.state) url2.searchParams.set(STATE_PARAM2, options.state);
10088
+ url2.searchParams.set(STATE_PARAM3, beginState(PENDING_KEY2, {}));
10044
10089
  return url2.toString();
10045
10090
  }
10046
10091
  function beginDeviceEnrolment(options) {
@@ -10051,6 +10096,7 @@ function readEnrolmentCallback(location2 = window.location) {
10051
10096
  const params = new URLSearchParams(location2.hash.replace(/^#/, ""));
10052
10097
  const error = params.get("error");
10053
10098
  if (error) {
10099
+ takeState(PENDING_KEY2, params.get(STATE_PARAM3));
10054
10100
  stripFragment2();
10055
10101
  throw new Error(
10056
10102
  error === "denied" || error === "cancelled" ? "the device was not approved at the wallet" : `the wallet refused this enrolment: ${error}`
@@ -10069,9 +10115,9 @@ function stripFragment2() {
10069
10115
  history.replaceState(null, "", location.pathname + location.search);
10070
10116
  }
10071
10117
  async function completeDeviceEnrolment(options) {
10072
- if (options.expectState !== void 0 && options.expectState !== options.state) {
10118
+ if (!takeState(PENDING_KEY2, options.state)) {
10073
10119
  throw new Error(
10074
- "this enrolment answers a request this app did not make \u2014 the state it carries is not the one sent"
10120
+ "this enrolment answers a request this app did not make - start the enrolment again from here"
10075
10121
  );
10076
10122
  }
10077
10123
  const certificate = await verifyDeviceCredential(options.credential);
@@ -12464,8 +12510,8 @@ var MeroJs = class {
12464
12510
  /**
12465
12511
  * Parse an auth callback URL hash fragment (static utility)
12466
12512
  */
12467
- static parseAuthCallback(url2) {
12468
- return parseAuthCallback(url2);
12513
+ static parseAuthCallback(url2, options) {
12514
+ return parseAuthCallback(url2, options);
12469
12515
  }
12470
12516
  /**
12471
12517
  * Build an auth login URL (static utility)