@calimero-network/mero-js 25.2.0 → 25.3.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.
- package/dist/admin-api/admin-client.d.ts +3 -1
- package/dist/admin-api/admin-client.js +4 -0
- package/dist/admin-api/admin-types.d.ts +6 -0
- package/dist/events/sse.js +20 -8
- package/dist/http-client/http-types.d.ts +1 -1
- package/dist/http-client/web-client.d.ts +7 -0
- package/dist/http-client/web-client.js +41 -25
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +295 -267
- package/dist/index.cjs.map +4 -4
- package/dist/index.mjs +295 -267
- package/dist/index.mjs.map +4 -4
- package/dist/request/proof-signer.d.ts +1 -1
- package/dist/request/request-sig.d.ts +9 -9
- package/dist/request/request-sig.js +7 -8
- package/dist/request-proof/authorizer.d.ts +3 -10
- package/dist/request-proof/authorizer.js +5 -10
- package/dist/request-proof/request-proof.d.ts +3 -6
- package/dist/request-proof/request-proof.js +4 -20
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -331,6 +331,244 @@ function createTimeoutSignal(timeoutMs) {
|
|
|
331
331
|
return controller.signal;
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
// src/crypto/internal.ts
|
|
335
|
+
var PKCS8_ED25519_PREFIX = new Uint8Array([
|
|
336
|
+
48,
|
|
337
|
+
46,
|
|
338
|
+
2,
|
|
339
|
+
1,
|
|
340
|
+
0,
|
|
341
|
+
48,
|
|
342
|
+
5,
|
|
343
|
+
6,
|
|
344
|
+
3,
|
|
345
|
+
43,
|
|
346
|
+
101,
|
|
347
|
+
112,
|
|
348
|
+
4,
|
|
349
|
+
34,
|
|
350
|
+
4,
|
|
351
|
+
32
|
|
352
|
+
]);
|
|
353
|
+
function u32le(value) {
|
|
354
|
+
const out = new Uint8Array(4);
|
|
355
|
+
new DataView(out.buffer).setUint32(0, value, true);
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
function hex(bytes) {
|
|
359
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
360
|
+
}
|
|
361
|
+
function fromHex(value, label2, expectedBytes) {
|
|
362
|
+
const clean = value.trim();
|
|
363
|
+
if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length !== expectedBytes * 2) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`${label2} must be ${expectedBytes * 2} hex characters, got ${clean.length}`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
const bytes = new Uint8Array(expectedBytes);
|
|
369
|
+
for (let i = 0; i < expectedBytes; i += 1) {
|
|
370
|
+
bytes[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
371
|
+
}
|
|
372
|
+
return bytes;
|
|
373
|
+
}
|
|
374
|
+
function fromHexUnsized(value, label2) {
|
|
375
|
+
const clean = value.trim();
|
|
376
|
+
if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length % 2 !== 0) {
|
|
377
|
+
throw new Error(`${label2} must be an even number of hex characters`);
|
|
378
|
+
}
|
|
379
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
380
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
381
|
+
bytes[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
382
|
+
}
|
|
383
|
+
return bytes;
|
|
384
|
+
}
|
|
385
|
+
function u64le(value) {
|
|
386
|
+
const out = new Uint8Array(8);
|
|
387
|
+
new DataView(out.buffer).setBigUint64(0, BigInt(value), true);
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
function concat(...parts) {
|
|
391
|
+
const out = new Uint8Array(parts.reduce((total, p) => total + p.length, 0));
|
|
392
|
+
let offset = 0;
|
|
393
|
+
for (const part of parts) {
|
|
394
|
+
out.set(part, offset);
|
|
395
|
+
offset += part.length;
|
|
396
|
+
}
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
async function domainHash(domain, parts) {
|
|
400
|
+
const chunks = [u64le(domain.length), domain];
|
|
401
|
+
for (const part of parts) {
|
|
402
|
+
chunks.push(u64le(part.length), part);
|
|
403
|
+
}
|
|
404
|
+
return new Uint8Array(
|
|
405
|
+
await crypto.subtle.digest("SHA-256", concat(...chunks))
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
async function importSigningKey(secretHex) {
|
|
409
|
+
const seed = fromHex(secretHex, "deviceSecret", 32);
|
|
410
|
+
const pkcs8 = concat(PKCS8_ED25519_PREFIX, seed);
|
|
411
|
+
try {
|
|
412
|
+
return await crypto.subtle.importKey(
|
|
413
|
+
"pkcs8",
|
|
414
|
+
pkcs8,
|
|
415
|
+
{ name: "Ed25519" },
|
|
416
|
+
false,
|
|
417
|
+
["sign"]
|
|
418
|
+
);
|
|
419
|
+
} catch (cause) {
|
|
420
|
+
const detail = cause instanceof Error ? `: ${cause.message}` : "";
|
|
421
|
+
throw new Error(
|
|
422
|
+
"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
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
async function verifySignature(publicKey, signature, payload) {
|
|
427
|
+
const key = await crypto.subtle.importKey(
|
|
428
|
+
"raw",
|
|
429
|
+
fromHex(publicKey, "publicKey", 32),
|
|
430
|
+
{ name: "Ed25519" },
|
|
431
|
+
false,
|
|
432
|
+
["verify"]
|
|
433
|
+
);
|
|
434
|
+
return crypto.subtle.verify({ name: "Ed25519" }, key, signature, payload);
|
|
435
|
+
}
|
|
436
|
+
async function derivePublicKey(secretHex) {
|
|
437
|
+
const seed = fromHex(secretHex, "deviceSecret", 32);
|
|
438
|
+
const pkcs8 = concat(PKCS8_ED25519_PREFIX, seed);
|
|
439
|
+
const key = await crypto.subtle.importKey(
|
|
440
|
+
"pkcs8",
|
|
441
|
+
pkcs8,
|
|
442
|
+
{ name: "Ed25519" },
|
|
443
|
+
true,
|
|
444
|
+
["sign"]
|
|
445
|
+
);
|
|
446
|
+
const jwk = await crypto.subtle.exportKey("jwk", key);
|
|
447
|
+
const raw = jwk.x;
|
|
448
|
+
return Uint8Array.from(
|
|
449
|
+
atob(raw.replace(/-/g, "+").replace(/_/g, "/")),
|
|
450
|
+
(c) => c.charCodeAt(0)
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/request/request-sig.ts
|
|
455
|
+
var SIGN_DOMAIN = new TextEncoder().encode("calimero.auth.request.v2");
|
|
456
|
+
var BODY_DOMAIN = new TextEncoder().encode("calimero.auth.request.body.v1");
|
|
457
|
+
function borshString(value) {
|
|
458
|
+
const bytes = new TextEncoder().encode(value);
|
|
459
|
+
return concat(u32le(bytes.length), bytes);
|
|
460
|
+
}
|
|
461
|
+
async function requestBodyHash(body = new Uint8Array(0)) {
|
|
462
|
+
const bytes = typeof body === "string" ? new TextEncoder().encode(body) : body;
|
|
463
|
+
return domainHash(BODY_DOMAIN, [bytes]);
|
|
464
|
+
}
|
|
465
|
+
async function signRequest(input) {
|
|
466
|
+
const key = await importSigningKey(input.signerSecret);
|
|
467
|
+
const bodyHash = await requestBodyHash(input.body);
|
|
468
|
+
const issuedAt = u64le(input.issuedAt);
|
|
469
|
+
const expiresAt = u64le(input.expiresAt);
|
|
470
|
+
const encoder = new TextEncoder();
|
|
471
|
+
const preimage = await domainHash(SIGN_DOMAIN, [
|
|
472
|
+
encoder.encode(input.method),
|
|
473
|
+
encoder.encode(input.path),
|
|
474
|
+
bodyHash,
|
|
475
|
+
issuedAt,
|
|
476
|
+
expiresAt
|
|
477
|
+
]);
|
|
478
|
+
const signature = new Uint8Array(
|
|
479
|
+
await crypto.subtle.sign({ name: "Ed25519" }, key, preimage)
|
|
480
|
+
);
|
|
481
|
+
return hex(
|
|
482
|
+
concat(
|
|
483
|
+
borshString(input.method),
|
|
484
|
+
borshString(input.path),
|
|
485
|
+
bodyHash,
|
|
486
|
+
issuedAt,
|
|
487
|
+
expiresAt,
|
|
488
|
+
signature
|
|
489
|
+
)
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/request-proof/request-proof.ts
|
|
494
|
+
var utf8 = (text) => new TextEncoder().encode(text);
|
|
495
|
+
var REQUEST_PROOF_HEADER = "X-Calimero-Proof";
|
|
496
|
+
var DEFAULT_VALID_FOR = 300;
|
|
497
|
+
async function signRequestProof(input) {
|
|
498
|
+
const { method, path, credential } = input;
|
|
499
|
+
if (!method) throw new Error("signRequestProof needs the method the request will use");
|
|
500
|
+
if (!path.startsWith("/")) {
|
|
501
|
+
throw new Error(`signRequestProof needs a path, got ${JSON.stringify(path)}`);
|
|
502
|
+
}
|
|
503
|
+
const bodyHash = await requestBodyHash(input.body);
|
|
504
|
+
const issuedAt = Math.floor(input.now ?? Date.now() / 1e3);
|
|
505
|
+
const expiresAt = issuedAt + (input.validForSeconds ?? DEFAULT_VALID_FOR);
|
|
506
|
+
const payload = await domainHash(SIGN_DOMAIN, [
|
|
507
|
+
utf8(method),
|
|
508
|
+
utf8(path),
|
|
509
|
+
bodyHash,
|
|
510
|
+
u64le(issuedAt),
|
|
511
|
+
u64le(expiresAt)
|
|
512
|
+
]);
|
|
513
|
+
const signature = await signPayload(payload, input);
|
|
514
|
+
if (signature.length !== 64) {
|
|
515
|
+
throw new Error(`the signer returned ${signature.length} bytes, not a 64-byte signature`);
|
|
516
|
+
}
|
|
517
|
+
const requestSig = concat(
|
|
518
|
+
u32le(utf8(method).length),
|
|
519
|
+
utf8(method),
|
|
520
|
+
u32le(utf8(path).length),
|
|
521
|
+
utf8(path),
|
|
522
|
+
bodyHash,
|
|
523
|
+
u64le(issuedAt),
|
|
524
|
+
u64le(expiresAt),
|
|
525
|
+
signature
|
|
526
|
+
);
|
|
527
|
+
const accountProof = hexToBytes(credential, "credential");
|
|
528
|
+
return hex(concat(accountProof, new Uint8Array([0]), requestSig));
|
|
529
|
+
}
|
|
530
|
+
async function signPayload(payload, input) {
|
|
531
|
+
if (input.signer) return input.signer.sign(payload);
|
|
532
|
+
if (!input.deviceSecret) {
|
|
533
|
+
throw new Error("signRequestProof needs either `deviceSecret` or `signer`");
|
|
534
|
+
}
|
|
535
|
+
const key = await importSigningKey(input.deviceSecret);
|
|
536
|
+
return new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, key, payload));
|
|
537
|
+
}
|
|
538
|
+
function hexToBytes(value, label2) {
|
|
539
|
+
const clean = value.trim();
|
|
540
|
+
if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length % 2 !== 0) {
|
|
541
|
+
throw new Error(`${label2} is not an even-length hex string`);
|
|
542
|
+
}
|
|
543
|
+
return fromHex(clean, label2, clean.length / 2);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/request-proof/authorizer.ts
|
|
547
|
+
function createProofAuthorizer(credential) {
|
|
548
|
+
if (!credential.credential) {
|
|
549
|
+
throw new Error("a proof authorizer needs the `credential` from signDeviceCert");
|
|
550
|
+
}
|
|
551
|
+
if (!credential.deviceSecret && !credential.signer) {
|
|
552
|
+
throw new Error("a proof authorizer needs either `deviceSecret` or `signer`");
|
|
553
|
+
}
|
|
554
|
+
return async ({ method, path, body }) => {
|
|
555
|
+
const proof = await signRequestProof({
|
|
556
|
+
method,
|
|
557
|
+
path,
|
|
558
|
+
body,
|
|
559
|
+
credential: credential.credential,
|
|
560
|
+
deviceSecret: credential.deviceSecret,
|
|
561
|
+
signer: credential.signer,
|
|
562
|
+
validForSeconds: credential.validForSeconds
|
|
563
|
+
});
|
|
564
|
+
return { [REQUEST_PROOF_HEADER]: proof };
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function signedPath(url2) {
|
|
568
|
+
const parsed = new URL(url2, globalThis.location?.href ?? "http://localhost");
|
|
569
|
+
return parsed.pathname + parsed.search;
|
|
570
|
+
}
|
|
571
|
+
|
|
334
572
|
// src/http-client/web-client.ts
|
|
335
573
|
var TERMINAL_AUTH_ERRORS = /* @__PURE__ */ new Set(["token_reuse", "token_revoked"]);
|
|
336
574
|
function extractErrorMessage(bodyText) {
|
|
@@ -386,13 +624,31 @@ function headersToRecord(headers) {
|
|
|
386
624
|
});
|
|
387
625
|
return record;
|
|
388
626
|
}
|
|
389
|
-
function
|
|
627
|
+
function withoutBareQuery(url2) {
|
|
628
|
+
let parsed;
|
|
390
629
|
try {
|
|
391
|
-
|
|
630
|
+
parsed = new URL(url2, globalThis.location?.href);
|
|
392
631
|
} catch {
|
|
393
|
-
|
|
394
|
-
return query === -1 ? url2 : url2.slice(0, query);
|
|
632
|
+
return url2;
|
|
395
633
|
}
|
|
634
|
+
if (parsed.search === "") parsed.search = "";
|
|
635
|
+
return parsed.href;
|
|
636
|
+
}
|
|
637
|
+
function carriesProof(headers) {
|
|
638
|
+
return Object.keys(headers).some(
|
|
639
|
+
(name) => name.toLowerCase() === REQUEST_PROOF_HEADER.toLowerCase()
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
function refuseProofRedirect(response, url2) {
|
|
643
|
+
const redirected = response.type === "opaqueredirect" || response.status >= 300 && response.status < 400;
|
|
644
|
+
if (!redirected) return;
|
|
645
|
+
throw new HTTPError(
|
|
646
|
+
0,
|
|
647
|
+
"Redirect Not Followed",
|
|
648
|
+
url2,
|
|
649
|
+
response.headers,
|
|
650
|
+
`the node answered with a redirect; a request carrying ${REQUEST_PROOF_HEADER} is not followed to another location`
|
|
651
|
+
);
|
|
396
652
|
}
|
|
397
653
|
var WebHttpClient = class {
|
|
398
654
|
constructor(transport2) {
|
|
@@ -457,7 +713,7 @@ var WebHttpClient = class {
|
|
|
457
713
|
}
|
|
458
714
|
async makeRequest(path, init, retryCount = 0, requestStartTime) {
|
|
459
715
|
const MAX_RETRY_ATTEMPTS = 1;
|
|
460
|
-
const url2 = this.buildUrl(path);
|
|
716
|
+
const url2 = withoutBareQuery(this.buildUrl(path));
|
|
461
717
|
const startTime = requestStartTime ?? Date.now();
|
|
462
718
|
const signal = this.createAbortSignal(init);
|
|
463
719
|
const headers = await this.buildHeaders(init?.headers);
|
|
@@ -482,7 +738,7 @@ var WebHttpClient = class {
|
|
|
482
738
|
headersObj,
|
|
483
739
|
await this.transport.authorizeRequest({
|
|
484
740
|
method,
|
|
485
|
-
path:
|
|
741
|
+
path: signedPath(url2),
|
|
486
742
|
// Narrowed by the guard above; `BodyInit` is wider than what can be
|
|
487
743
|
// signed.
|
|
488
744
|
body
|
|
@@ -542,15 +798,20 @@ var WebHttpClient = class {
|
|
|
542
798
|
if (this.transport.getProof) {
|
|
543
799
|
const proof = await this.transport.getProof({
|
|
544
800
|
method: requestInit.method ?? "GET",
|
|
545
|
-
path:
|
|
801
|
+
path: signedPath(url2),
|
|
546
802
|
body: requestInit.body
|
|
547
803
|
});
|
|
548
804
|
if (proof) {
|
|
549
805
|
headersObj["X-Calimero-Proof"] = proof;
|
|
550
806
|
}
|
|
551
807
|
}
|
|
808
|
+
const proofAttached = carriesProof(headersObj);
|
|
809
|
+
if (proofAttached && requestInit.redirect !== "error") {
|
|
810
|
+
requestInit.redirect = "manual";
|
|
811
|
+
}
|
|
552
812
|
try {
|
|
553
813
|
const response = await this.transport.fetch(url2, requestInit);
|
|
814
|
+
if (proofAttached) refuseProofRedirect(response, url2);
|
|
554
815
|
if (!response.ok) {
|
|
555
816
|
const bodyText = await this.getBodyText(response);
|
|
556
817
|
const httpError = new HTTPError(
|
|
@@ -2569,6 +2830,10 @@ var AdminApiClient = class {
|
|
|
2569
2830
|
async getMigrationStatus(namespaceId) {
|
|
2570
2831
|
return this.httpClient.get(`/admin-api/groups/${pathSegment(namespaceId)}/migration-status`);
|
|
2571
2832
|
}
|
|
2833
|
+
/** Revoked devices and withdrawn slots of a namespace; 403 unless this node is a bound member. */
|
|
2834
|
+
async listNamespaceRevokedDevices(namespaceId) {
|
|
2835
|
+
return this.httpClient.get(`/admin-api/namespaces/${pathSegment(namespaceId)}/revoked-devices`);
|
|
2836
|
+
}
|
|
2572
2837
|
/** Per-group cascade-migration snapshots for a namespace. */
|
|
2573
2838
|
async getCascadeStatus(namespaceId) {
|
|
2574
2839
|
return unwrap(
|
|
@@ -2760,165 +3025,6 @@ function buildAuthLoginUrl(nodeUrl, opts) {
|
|
|
2760
3025
|
return `${base}/auth/login?${params.toString()}`;
|
|
2761
3026
|
}
|
|
2762
3027
|
|
|
2763
|
-
// src/crypto/internal.ts
|
|
2764
|
-
var PKCS8_ED25519_PREFIX = new Uint8Array([
|
|
2765
|
-
48,
|
|
2766
|
-
46,
|
|
2767
|
-
2,
|
|
2768
|
-
1,
|
|
2769
|
-
0,
|
|
2770
|
-
48,
|
|
2771
|
-
5,
|
|
2772
|
-
6,
|
|
2773
|
-
3,
|
|
2774
|
-
43,
|
|
2775
|
-
101,
|
|
2776
|
-
112,
|
|
2777
|
-
4,
|
|
2778
|
-
34,
|
|
2779
|
-
4,
|
|
2780
|
-
32
|
|
2781
|
-
]);
|
|
2782
|
-
function u32le(value) {
|
|
2783
|
-
const out = new Uint8Array(4);
|
|
2784
|
-
new DataView(out.buffer).setUint32(0, value, true);
|
|
2785
|
-
return out;
|
|
2786
|
-
}
|
|
2787
|
-
function hex(bytes) {
|
|
2788
|
-
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
2789
|
-
}
|
|
2790
|
-
function fromHex(value, label2, expectedBytes) {
|
|
2791
|
-
const clean = value.trim();
|
|
2792
|
-
if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length !== expectedBytes * 2) {
|
|
2793
|
-
throw new Error(
|
|
2794
|
-
`${label2} must be ${expectedBytes * 2} hex characters, got ${clean.length}`
|
|
2795
|
-
);
|
|
2796
|
-
}
|
|
2797
|
-
const bytes = new Uint8Array(expectedBytes);
|
|
2798
|
-
for (let i = 0; i < expectedBytes; i += 1) {
|
|
2799
|
-
bytes[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
2800
|
-
}
|
|
2801
|
-
return bytes;
|
|
2802
|
-
}
|
|
2803
|
-
function fromHexUnsized(value, label2) {
|
|
2804
|
-
const clean = value.trim();
|
|
2805
|
-
if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length % 2 !== 0) {
|
|
2806
|
-
throw new Error(`${label2} must be an even number of hex characters`);
|
|
2807
|
-
}
|
|
2808
|
-
const bytes = new Uint8Array(clean.length / 2);
|
|
2809
|
-
for (let i = 0; i < bytes.length; i += 1) {
|
|
2810
|
-
bytes[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
2811
|
-
}
|
|
2812
|
-
return bytes;
|
|
2813
|
-
}
|
|
2814
|
-
function u64le(value) {
|
|
2815
|
-
const out = new Uint8Array(8);
|
|
2816
|
-
new DataView(out.buffer).setBigUint64(0, BigInt(value), true);
|
|
2817
|
-
return out;
|
|
2818
|
-
}
|
|
2819
|
-
function concat(...parts) {
|
|
2820
|
-
const out = new Uint8Array(parts.reduce((total, p) => total + p.length, 0));
|
|
2821
|
-
let offset = 0;
|
|
2822
|
-
for (const part of parts) {
|
|
2823
|
-
out.set(part, offset);
|
|
2824
|
-
offset += part.length;
|
|
2825
|
-
}
|
|
2826
|
-
return out;
|
|
2827
|
-
}
|
|
2828
|
-
async function domainHash(domain, parts) {
|
|
2829
|
-
const chunks = [u64le(domain.length), domain];
|
|
2830
|
-
for (const part of parts) {
|
|
2831
|
-
chunks.push(u64le(part.length), part);
|
|
2832
|
-
}
|
|
2833
|
-
return new Uint8Array(
|
|
2834
|
-
await crypto.subtle.digest("SHA-256", concat(...chunks))
|
|
2835
|
-
);
|
|
2836
|
-
}
|
|
2837
|
-
async function importSigningKey(secretHex) {
|
|
2838
|
-
const seed = fromHex(secretHex, "deviceSecret", 32);
|
|
2839
|
-
const pkcs8 = concat(PKCS8_ED25519_PREFIX, seed);
|
|
2840
|
-
try {
|
|
2841
|
-
return await crypto.subtle.importKey(
|
|
2842
|
-
"pkcs8",
|
|
2843
|
-
pkcs8,
|
|
2844
|
-
{ name: "Ed25519" },
|
|
2845
|
-
false,
|
|
2846
|
-
["sign"]
|
|
2847
|
-
);
|
|
2848
|
-
} catch (cause) {
|
|
2849
|
-
const detail = cause instanceof Error ? `: ${cause.message}` : "";
|
|
2850
|
-
throw new Error(
|
|
2851
|
-
"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
|
|
2852
|
-
);
|
|
2853
|
-
}
|
|
2854
|
-
}
|
|
2855
|
-
async function verifySignature(publicKey, signature, payload) {
|
|
2856
|
-
const key = await crypto.subtle.importKey(
|
|
2857
|
-
"raw",
|
|
2858
|
-
fromHex(publicKey, "publicKey", 32),
|
|
2859
|
-
{ name: "Ed25519" },
|
|
2860
|
-
false,
|
|
2861
|
-
["verify"]
|
|
2862
|
-
);
|
|
2863
|
-
return crypto.subtle.verify({ name: "Ed25519" }, key, signature, payload);
|
|
2864
|
-
}
|
|
2865
|
-
async function derivePublicKey(secretHex) {
|
|
2866
|
-
const seed = fromHex(secretHex, "deviceSecret", 32);
|
|
2867
|
-
const pkcs8 = concat(PKCS8_ED25519_PREFIX, seed);
|
|
2868
|
-
const key = await crypto.subtle.importKey(
|
|
2869
|
-
"pkcs8",
|
|
2870
|
-
pkcs8,
|
|
2871
|
-
{ name: "Ed25519" },
|
|
2872
|
-
true,
|
|
2873
|
-
["sign"]
|
|
2874
|
-
);
|
|
2875
|
-
const jwk = await crypto.subtle.exportKey("jwk", key);
|
|
2876
|
-
const raw = jwk.x;
|
|
2877
|
-
return Uint8Array.from(
|
|
2878
|
-
atob(raw.replace(/-/g, "+").replace(/_/g, "/")),
|
|
2879
|
-
(c) => c.charCodeAt(0)
|
|
2880
|
-
);
|
|
2881
|
-
}
|
|
2882
|
-
|
|
2883
|
-
// src/request/request-sig.ts
|
|
2884
|
-
var SIGN_DOMAIN = new TextEncoder().encode("calimero.auth.request.v1");
|
|
2885
|
-
var BODY_DOMAIN = new TextEncoder().encode("calimero.auth.request.body.v1");
|
|
2886
|
-
function borshString(value) {
|
|
2887
|
-
const bytes = new TextEncoder().encode(value);
|
|
2888
|
-
return concat(u32le(bytes.length), bytes);
|
|
2889
|
-
}
|
|
2890
|
-
async function requestBodyHash(body = new Uint8Array(0)) {
|
|
2891
|
-
const bytes = typeof body === "string" ? new TextEncoder().encode(body) : body;
|
|
2892
|
-
return domainHash(BODY_DOMAIN, [bytes]);
|
|
2893
|
-
}
|
|
2894
|
-
async function signRequest(input) {
|
|
2895
|
-
const key = await importSigningKey(input.signerSecret);
|
|
2896
|
-
const bodyHash = await requestBodyHash(input.body);
|
|
2897
|
-
const issuedAt = u64le(input.issuedAt);
|
|
2898
|
-
const expiresAt = u64le(input.expiresAt);
|
|
2899
|
-
const encoder = new TextEncoder();
|
|
2900
|
-
const preimage = await domainHash(SIGN_DOMAIN, [
|
|
2901
|
-
encoder.encode(input.method),
|
|
2902
|
-
encoder.encode(input.path),
|
|
2903
|
-
bodyHash,
|
|
2904
|
-
issuedAt,
|
|
2905
|
-
expiresAt
|
|
2906
|
-
]);
|
|
2907
|
-
const signature = new Uint8Array(
|
|
2908
|
-
await crypto.subtle.sign({ name: "Ed25519" }, key, preimage)
|
|
2909
|
-
);
|
|
2910
|
-
return hex(
|
|
2911
|
-
concat(
|
|
2912
|
-
borshString(input.method),
|
|
2913
|
-
borshString(input.path),
|
|
2914
|
-
bodyHash,
|
|
2915
|
-
issuedAt,
|
|
2916
|
-
expiresAt,
|
|
2917
|
-
signature
|
|
2918
|
-
)
|
|
2919
|
-
);
|
|
2920
|
-
}
|
|
2921
|
-
|
|
2922
3028
|
// src/request/caller-proof.ts
|
|
2923
3029
|
function callerProof(input) {
|
|
2924
3030
|
const credential = fromHexUnsized(input.credential, "credential");
|
|
@@ -3147,13 +3253,17 @@ var SseClient = class {
|
|
|
3147
3253
|
this.abortController = new AbortController();
|
|
3148
3254
|
try {
|
|
3149
3255
|
const url2 = `${this.baseUrl}/sse`;
|
|
3256
|
+
const headers = {
|
|
3257
|
+
...await this.authorize({ method: "GET", path: signedPath(url2) }),
|
|
3258
|
+
"Accept": "text/event-stream"
|
|
3259
|
+
};
|
|
3260
|
+
const proofAttached = carriesProof(headers);
|
|
3150
3261
|
const response = await this.fetchImpl(url2, {
|
|
3151
|
-
headers
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
},
|
|
3155
|
-
signal: this.abortController.signal
|
|
3262
|
+
headers,
|
|
3263
|
+
signal: this.abortController.signal,
|
|
3264
|
+
...proofAttached && { redirect: "manual" }
|
|
3156
3265
|
});
|
|
3266
|
+
if (proofAttached) refuseProofRedirect(response, url2);
|
|
3157
3267
|
if (!response.ok) {
|
|
3158
3268
|
const terminal = isTerminalAuthFailure(response);
|
|
3159
3269
|
const bodyText = await response.text().catch(() => void 0);
|
|
@@ -3351,15 +3461,19 @@ var SseClient = class {
|
|
|
3351
3461
|
if (ids.groupIds && ids.groupIds.length > 0) params.groupIds = ids.groupIds;
|
|
3352
3462
|
const url2 = `${this.baseUrl}/sse/subscription`;
|
|
3353
3463
|
const body = JSON.stringify({ id: this.sessionId, method, params });
|
|
3464
|
+
const headers = {
|
|
3465
|
+
...await this.authorize({ method: "POST", path: signedPath(url2), body }),
|
|
3466
|
+
"Content-Type": "application/json"
|
|
3467
|
+
};
|
|
3468
|
+
const proofAttached = carriesProof(headers);
|
|
3354
3469
|
const response = await this.fetchImpl(url2, {
|
|
3355
3470
|
method: "POST",
|
|
3356
|
-
headers
|
|
3357
|
-
...await this.authorize({ method: "POST", path: new URL(url2).pathname, body }),
|
|
3358
|
-
"Content-Type": "application/json"
|
|
3359
|
-
},
|
|
3471
|
+
headers,
|
|
3360
3472
|
body,
|
|
3361
|
-
signal: timeout.signal
|
|
3473
|
+
signal: timeout.signal,
|
|
3474
|
+
...proofAttached && { redirect: "manual" }
|
|
3362
3475
|
});
|
|
3476
|
+
if (proofAttached) refuseProofRedirect(response, url2);
|
|
3363
3477
|
if (!response.ok) {
|
|
3364
3478
|
const terminal = isTerminalAuthFailure(response);
|
|
3365
3479
|
const bodyText = await response.text().catch(() => void 0);
|
|
@@ -6479,7 +6593,7 @@ var MAX_HANDSHAKE_REPLY = 1024;
|
|
|
6479
6593
|
var RENEW_BEFORE_MS = 6e4;
|
|
6480
6594
|
var TRANSPORT_BINDING_DOMAIN = "calimero.tee-attest.transport-key.v1";
|
|
6481
6595
|
var NULL_BODY_STATUSES = [101, 204, 205, 304];
|
|
6482
|
-
var
|
|
6596
|
+
var utf82 = new TextEncoder();
|
|
6483
6597
|
var StaleTransportKeyError = class extends Error {
|
|
6484
6598
|
constructor() {
|
|
6485
6599
|
super("The node no longer holds the attested transport key; attest again for the current one");
|
|
@@ -6498,7 +6612,7 @@ var SealedTransportError = class extends Error {
|
|
|
6498
6612
|
async function transportKeyBinding(inner, transportKey) {
|
|
6499
6613
|
const digest = await crypto.subtle.digest(
|
|
6500
6614
|
"SHA-256",
|
|
6501
|
-
concat(
|
|
6615
|
+
concat(utf82.encode(TRANSPORT_BINDING_DOMAIN), inner, transportKey)
|
|
6502
6616
|
);
|
|
6503
6617
|
return new Uint8Array(digest);
|
|
6504
6618
|
}
|
|
@@ -6702,7 +6816,7 @@ function nonce(requestId, index) {
|
|
|
6702
6816
|
return bytes;
|
|
6703
6817
|
}
|
|
6704
6818
|
function joinInner(head, body) {
|
|
6705
|
-
const encoded =
|
|
6819
|
+
const encoded = utf82.encode(JSON.stringify(head));
|
|
6706
6820
|
const len = new Uint8Array(4);
|
|
6707
6821
|
new DataView(len.buffer).setUint32(0, encoded.length, false);
|
|
6708
6822
|
return concat(len, encoded, body);
|
|
@@ -7192,7 +7306,7 @@ var FULCIO_ISSUER_V1 = "1.3.6.1.4.1.57264.1.1";
|
|
|
7192
7306
|
var SAN_URI = 134;
|
|
7193
7307
|
var UTF8_STRING = 12;
|
|
7194
7308
|
var SEQUENCE2 = 48;
|
|
7195
|
-
var
|
|
7309
|
+
var utf83 = new TextEncoder();
|
|
7196
7310
|
var fromUtf8 = (bytes) => new TextDecoder().decode(bytes);
|
|
7197
7311
|
async function verifyCosignBundle(body, bundle, signer, signature) {
|
|
7198
7312
|
const { base64Signature, cert, rekorBundle } = cosignBundle(bundle);
|
|
@@ -7205,7 +7319,7 @@ async function verifyCosignBundle(body, bundle, signer, signature) {
|
|
|
7205
7319
|
const leaf = parseCertificate(leafDer);
|
|
7206
7320
|
const log = REKOR_LOGS.find((candidate) => candidate.logId === payload.logID.toLowerCase());
|
|
7207
7321
|
if (!log) throw new Error(`The bundle names Rekor log ${payload.logID}, which the Sigstore trust root does not know`);
|
|
7208
|
-
const canonical =
|
|
7322
|
+
const canonical = utf83.encode(
|
|
7209
7323
|
`{"body":${JSON.stringify(payload.body)},"integratedTime":${payload.integratedTime},"logID":${JSON.stringify(payload.logID)},"logIndex":${payload.logIndex}}`
|
|
7210
7324
|
);
|
|
7211
7325
|
const setVerifies = await verifyEcdsa(
|
|
@@ -8943,92 +9057,6 @@ var RelayObserver = class {
|
|
|
8943
9057
|
}
|
|
8944
9058
|
};
|
|
8945
9059
|
|
|
8946
|
-
// src/request-proof/request-proof.ts
|
|
8947
|
-
var utf83 = (text) => new TextEncoder().encode(text);
|
|
8948
|
-
var REQUEST_SIGN_DOMAIN = utf83("calimero.auth.request.v1");
|
|
8949
|
-
var REQUEST_BODY_DOMAIN = utf83("calimero.auth.request.body.v1");
|
|
8950
|
-
var REQUEST_PROOF_HEADER = "X-Calimero-Proof";
|
|
8951
|
-
var DEFAULT_VALID_FOR = 300;
|
|
8952
|
-
async function signRequestProof(input) {
|
|
8953
|
-
const { method, path, credential } = input;
|
|
8954
|
-
if (!method) throw new Error("signRequestProof needs the method the request will use");
|
|
8955
|
-
if (!path.startsWith("/")) {
|
|
8956
|
-
throw new Error(`signRequestProof needs a path, got ${JSON.stringify(path)}`);
|
|
8957
|
-
}
|
|
8958
|
-
if (path.includes("?")) {
|
|
8959
|
-
throw new Error(
|
|
8960
|
-
`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.`
|
|
8961
|
-
);
|
|
8962
|
-
}
|
|
8963
|
-
const bodyBytes2 = input.body === void 0 ? new Uint8Array() : typeof input.body === "string" ? utf83(input.body) : input.body;
|
|
8964
|
-
const bodyHash = await domainHash(REQUEST_BODY_DOMAIN, [bodyBytes2]);
|
|
8965
|
-
const issuedAt = Math.floor(input.now ?? Date.now() / 1e3);
|
|
8966
|
-
const expiresAt = issuedAt + (input.validForSeconds ?? DEFAULT_VALID_FOR);
|
|
8967
|
-
const payload = await domainHash(REQUEST_SIGN_DOMAIN, [
|
|
8968
|
-
utf83(method),
|
|
8969
|
-
utf83(path),
|
|
8970
|
-
bodyHash,
|
|
8971
|
-
u64le(issuedAt),
|
|
8972
|
-
u64le(expiresAt)
|
|
8973
|
-
]);
|
|
8974
|
-
const signature = await signPayload(payload, input);
|
|
8975
|
-
if (signature.length !== 64) {
|
|
8976
|
-
throw new Error(`the signer returned ${signature.length} bytes, not a 64-byte signature`);
|
|
8977
|
-
}
|
|
8978
|
-
const requestSig = concat(
|
|
8979
|
-
u32le(utf83(method).length),
|
|
8980
|
-
utf83(method),
|
|
8981
|
-
u32le(utf83(path).length),
|
|
8982
|
-
utf83(path),
|
|
8983
|
-
bodyHash,
|
|
8984
|
-
u64le(issuedAt),
|
|
8985
|
-
u64le(expiresAt),
|
|
8986
|
-
signature
|
|
8987
|
-
);
|
|
8988
|
-
const accountProof = hexToBytes(credential, "credential");
|
|
8989
|
-
return hex(concat(accountProof, new Uint8Array([0]), requestSig));
|
|
8990
|
-
}
|
|
8991
|
-
async function signPayload(payload, input) {
|
|
8992
|
-
if (input.signer) return input.signer.sign(payload);
|
|
8993
|
-
if (!input.deviceSecret) {
|
|
8994
|
-
throw new Error("signRequestProof needs either `deviceSecret` or `signer`");
|
|
8995
|
-
}
|
|
8996
|
-
const key = await importSigningKey(input.deviceSecret);
|
|
8997
|
-
return new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, key, payload));
|
|
8998
|
-
}
|
|
8999
|
-
function hexToBytes(value, label2) {
|
|
9000
|
-
const clean = value.trim();
|
|
9001
|
-
if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length % 2 !== 0) {
|
|
9002
|
-
throw new Error(`${label2} is not an even-length hex string`);
|
|
9003
|
-
}
|
|
9004
|
-
return fromHex(clean, label2, clean.length / 2);
|
|
9005
|
-
}
|
|
9006
|
-
|
|
9007
|
-
// src/request-proof/authorizer.ts
|
|
9008
|
-
function createProofAuthorizer(credential) {
|
|
9009
|
-
if (!credential.credential) {
|
|
9010
|
-
throw new Error("a proof authorizer needs the `credential` from signDeviceCert");
|
|
9011
|
-
}
|
|
9012
|
-
if (!credential.deviceSecret && !credential.signer) {
|
|
9013
|
-
throw new Error("a proof authorizer needs either `deviceSecret` or `signer`");
|
|
9014
|
-
}
|
|
9015
|
-
return async ({ method, path, body }) => {
|
|
9016
|
-
const proof = await signRequestProof({
|
|
9017
|
-
method,
|
|
9018
|
-
path,
|
|
9019
|
-
body,
|
|
9020
|
-
credential: credential.credential,
|
|
9021
|
-
deviceSecret: credential.deviceSecret,
|
|
9022
|
-
signer: credential.signer,
|
|
9023
|
-
validForSeconds: credential.validForSeconds
|
|
9024
|
-
});
|
|
9025
|
-
return { [REQUEST_PROOF_HEADER]: proof };
|
|
9026
|
-
};
|
|
9027
|
-
}
|
|
9028
|
-
function signedPath(url2) {
|
|
9029
|
-
return new URL(url2).pathname;
|
|
9030
|
-
}
|
|
9031
|
-
|
|
9032
9060
|
// src/presence/statement.ts
|
|
9033
9061
|
var PRESENCE_DOMAIN = new TextEncoder().encode("calimero/presence/1");
|
|
9034
9062
|
async function stateHash(state) {
|