@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.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
|
|
327
|
+
function withoutBareQuery(url2) {
|
|
328
|
+
let parsed;
|
|
90
329
|
try {
|
|
91
|
-
|
|
330
|
+
parsed = new URL(url2, globalThis.location?.href);
|
|
92
331
|
} catch {
|
|
93
|
-
|
|
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:
|
|
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:
|
|
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(
|
|
@@ -2269,6 +2530,10 @@ var AdminApiClient = class {
|
|
|
2269
2530
|
async getMigrationStatus(namespaceId) {
|
|
2270
2531
|
return this.httpClient.get(`/admin-api/groups/${pathSegment(namespaceId)}/migration-status`);
|
|
2271
2532
|
}
|
|
2533
|
+
/** Revoked devices and withdrawn slots of a namespace; 403 unless this node is a bound member. */
|
|
2534
|
+
async listNamespaceRevokedDevices(namespaceId) {
|
|
2535
|
+
return this.httpClient.get(`/admin-api/namespaces/${pathSegment(namespaceId)}/revoked-devices`);
|
|
2536
|
+
}
|
|
2272
2537
|
/** Per-group cascade-migration snapshots for a namespace. */
|
|
2273
2538
|
async getCascadeStatus(namespaceId) {
|
|
2274
2539
|
return unwrap(
|
|
@@ -2460,165 +2725,6 @@ function buildAuthLoginUrl(nodeUrl, opts) {
|
|
|
2460
2725
|
return `${base}/auth/login?${params.toString()}`;
|
|
2461
2726
|
}
|
|
2462
2727
|
|
|
2463
|
-
// src/crypto/internal.ts
|
|
2464
|
-
var PKCS8_ED25519_PREFIX = new Uint8Array([
|
|
2465
|
-
48,
|
|
2466
|
-
46,
|
|
2467
|
-
2,
|
|
2468
|
-
1,
|
|
2469
|
-
0,
|
|
2470
|
-
48,
|
|
2471
|
-
5,
|
|
2472
|
-
6,
|
|
2473
|
-
3,
|
|
2474
|
-
43,
|
|
2475
|
-
101,
|
|
2476
|
-
112,
|
|
2477
|
-
4,
|
|
2478
|
-
34,
|
|
2479
|
-
4,
|
|
2480
|
-
32
|
|
2481
|
-
]);
|
|
2482
|
-
function u32le(value) {
|
|
2483
|
-
const out = new Uint8Array(4);
|
|
2484
|
-
new DataView(out.buffer).setUint32(0, value, true);
|
|
2485
|
-
return out;
|
|
2486
|
-
}
|
|
2487
|
-
function hex(bytes) {
|
|
2488
|
-
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
2489
|
-
}
|
|
2490
|
-
function fromHex(value, label2, expectedBytes) {
|
|
2491
|
-
const clean = value.trim();
|
|
2492
|
-
if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length !== expectedBytes * 2) {
|
|
2493
|
-
throw new Error(
|
|
2494
|
-
`${label2} must be ${expectedBytes * 2} hex characters, got ${clean.length}`
|
|
2495
|
-
);
|
|
2496
|
-
}
|
|
2497
|
-
const bytes = new Uint8Array(expectedBytes);
|
|
2498
|
-
for (let i = 0; i < expectedBytes; i += 1) {
|
|
2499
|
-
bytes[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
2500
|
-
}
|
|
2501
|
-
return bytes;
|
|
2502
|
-
}
|
|
2503
|
-
function fromHexUnsized(value, label2) {
|
|
2504
|
-
const clean = value.trim();
|
|
2505
|
-
if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length % 2 !== 0) {
|
|
2506
|
-
throw new Error(`${label2} must be an even number of hex characters`);
|
|
2507
|
-
}
|
|
2508
|
-
const bytes = new Uint8Array(clean.length / 2);
|
|
2509
|
-
for (let i = 0; i < bytes.length; i += 1) {
|
|
2510
|
-
bytes[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
2511
|
-
}
|
|
2512
|
-
return bytes;
|
|
2513
|
-
}
|
|
2514
|
-
function u64le(value) {
|
|
2515
|
-
const out = new Uint8Array(8);
|
|
2516
|
-
new DataView(out.buffer).setBigUint64(0, BigInt(value), true);
|
|
2517
|
-
return out;
|
|
2518
|
-
}
|
|
2519
|
-
function concat(...parts) {
|
|
2520
|
-
const out = new Uint8Array(parts.reduce((total, p) => total + p.length, 0));
|
|
2521
|
-
let offset = 0;
|
|
2522
|
-
for (const part of parts) {
|
|
2523
|
-
out.set(part, offset);
|
|
2524
|
-
offset += part.length;
|
|
2525
|
-
}
|
|
2526
|
-
return out;
|
|
2527
|
-
}
|
|
2528
|
-
async function domainHash(domain, parts) {
|
|
2529
|
-
const chunks = [u64le(domain.length), domain];
|
|
2530
|
-
for (const part of parts) {
|
|
2531
|
-
chunks.push(u64le(part.length), part);
|
|
2532
|
-
}
|
|
2533
|
-
return new Uint8Array(
|
|
2534
|
-
await crypto.subtle.digest("SHA-256", concat(...chunks))
|
|
2535
|
-
);
|
|
2536
|
-
}
|
|
2537
|
-
async function importSigningKey(secretHex) {
|
|
2538
|
-
const seed = fromHex(secretHex, "deviceSecret", 32);
|
|
2539
|
-
const pkcs8 = concat(PKCS8_ED25519_PREFIX, seed);
|
|
2540
|
-
try {
|
|
2541
|
-
return await crypto.subtle.importKey(
|
|
2542
|
-
"pkcs8",
|
|
2543
|
-
pkcs8,
|
|
2544
|
-
{ name: "Ed25519" },
|
|
2545
|
-
false,
|
|
2546
|
-
["sign"]
|
|
2547
|
-
);
|
|
2548
|
-
} catch (cause) {
|
|
2549
|
-
const detail = cause instanceof Error ? `: ${cause.message}` : "";
|
|
2550
|
-
throw new Error(
|
|
2551
|
-
"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
|
|
2552
|
-
);
|
|
2553
|
-
}
|
|
2554
|
-
}
|
|
2555
|
-
async function verifySignature(publicKey, signature, payload) {
|
|
2556
|
-
const key = await crypto.subtle.importKey(
|
|
2557
|
-
"raw",
|
|
2558
|
-
fromHex(publicKey, "publicKey", 32),
|
|
2559
|
-
{ name: "Ed25519" },
|
|
2560
|
-
false,
|
|
2561
|
-
["verify"]
|
|
2562
|
-
);
|
|
2563
|
-
return crypto.subtle.verify({ name: "Ed25519" }, key, signature, payload);
|
|
2564
|
-
}
|
|
2565
|
-
async function derivePublicKey(secretHex) {
|
|
2566
|
-
const seed = fromHex(secretHex, "deviceSecret", 32);
|
|
2567
|
-
const pkcs8 = concat(PKCS8_ED25519_PREFIX, seed);
|
|
2568
|
-
const key = await crypto.subtle.importKey(
|
|
2569
|
-
"pkcs8",
|
|
2570
|
-
pkcs8,
|
|
2571
|
-
{ name: "Ed25519" },
|
|
2572
|
-
true,
|
|
2573
|
-
["sign"]
|
|
2574
|
-
);
|
|
2575
|
-
const jwk = await crypto.subtle.exportKey("jwk", key);
|
|
2576
|
-
const raw = jwk.x;
|
|
2577
|
-
return Uint8Array.from(
|
|
2578
|
-
atob(raw.replace(/-/g, "+").replace(/_/g, "/")),
|
|
2579
|
-
(c) => c.charCodeAt(0)
|
|
2580
|
-
);
|
|
2581
|
-
}
|
|
2582
|
-
|
|
2583
|
-
// src/request/request-sig.ts
|
|
2584
|
-
var SIGN_DOMAIN = new TextEncoder().encode("calimero.auth.request.v1");
|
|
2585
|
-
var BODY_DOMAIN = new TextEncoder().encode("calimero.auth.request.body.v1");
|
|
2586
|
-
function borshString(value) {
|
|
2587
|
-
const bytes = new TextEncoder().encode(value);
|
|
2588
|
-
return concat(u32le(bytes.length), bytes);
|
|
2589
|
-
}
|
|
2590
|
-
async function requestBodyHash(body = new Uint8Array(0)) {
|
|
2591
|
-
const bytes = typeof body === "string" ? new TextEncoder().encode(body) : body;
|
|
2592
|
-
return domainHash(BODY_DOMAIN, [bytes]);
|
|
2593
|
-
}
|
|
2594
|
-
async function signRequest(input) {
|
|
2595
|
-
const key = await importSigningKey(input.signerSecret);
|
|
2596
|
-
const bodyHash = await requestBodyHash(input.body);
|
|
2597
|
-
const issuedAt = u64le(input.issuedAt);
|
|
2598
|
-
const expiresAt = u64le(input.expiresAt);
|
|
2599
|
-
const encoder = new TextEncoder();
|
|
2600
|
-
const preimage = await domainHash(SIGN_DOMAIN, [
|
|
2601
|
-
encoder.encode(input.method),
|
|
2602
|
-
encoder.encode(input.path),
|
|
2603
|
-
bodyHash,
|
|
2604
|
-
issuedAt,
|
|
2605
|
-
expiresAt
|
|
2606
|
-
]);
|
|
2607
|
-
const signature = new Uint8Array(
|
|
2608
|
-
await crypto.subtle.sign({ name: "Ed25519" }, key, preimage)
|
|
2609
|
-
);
|
|
2610
|
-
return hex(
|
|
2611
|
-
concat(
|
|
2612
|
-
borshString(input.method),
|
|
2613
|
-
borshString(input.path),
|
|
2614
|
-
bodyHash,
|
|
2615
|
-
issuedAt,
|
|
2616
|
-
expiresAt,
|
|
2617
|
-
signature
|
|
2618
|
-
)
|
|
2619
|
-
);
|
|
2620
|
-
}
|
|
2621
|
-
|
|
2622
2728
|
// src/request/caller-proof.ts
|
|
2623
2729
|
function callerProof(input) {
|
|
2624
2730
|
const credential = fromHexUnsized(input.credential, "credential");
|
|
@@ -2847,13 +2953,17 @@ var SseClient = class {
|
|
|
2847
2953
|
this.abortController = new AbortController();
|
|
2848
2954
|
try {
|
|
2849
2955
|
const url2 = `${this.baseUrl}/sse`;
|
|
2956
|
+
const headers = {
|
|
2957
|
+
...await this.authorize({ method: "GET", path: signedPath(url2) }),
|
|
2958
|
+
"Accept": "text/event-stream"
|
|
2959
|
+
};
|
|
2960
|
+
const proofAttached = carriesProof(headers);
|
|
2850
2961
|
const response = await this.fetchImpl(url2, {
|
|
2851
|
-
headers
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
},
|
|
2855
|
-
signal: this.abortController.signal
|
|
2962
|
+
headers,
|
|
2963
|
+
signal: this.abortController.signal,
|
|
2964
|
+
...proofAttached && { redirect: "manual" }
|
|
2856
2965
|
});
|
|
2966
|
+
if (proofAttached) refuseProofRedirect(response, url2);
|
|
2857
2967
|
if (!response.ok) {
|
|
2858
2968
|
const terminal = isTerminalAuthFailure(response);
|
|
2859
2969
|
const bodyText = await response.text().catch(() => void 0);
|
|
@@ -3051,15 +3161,19 @@ var SseClient = class {
|
|
|
3051
3161
|
if (ids.groupIds && ids.groupIds.length > 0) params.groupIds = ids.groupIds;
|
|
3052
3162
|
const url2 = `${this.baseUrl}/sse/subscription`;
|
|
3053
3163
|
const body = JSON.stringify({ id: this.sessionId, method, params });
|
|
3164
|
+
const headers = {
|
|
3165
|
+
...await this.authorize({ method: "POST", path: signedPath(url2), body }),
|
|
3166
|
+
"Content-Type": "application/json"
|
|
3167
|
+
};
|
|
3168
|
+
const proofAttached = carriesProof(headers);
|
|
3054
3169
|
const response = await this.fetchImpl(url2, {
|
|
3055
3170
|
method: "POST",
|
|
3056
|
-
headers
|
|
3057
|
-
...await this.authorize({ method: "POST", path: new URL(url2).pathname, body }),
|
|
3058
|
-
"Content-Type": "application/json"
|
|
3059
|
-
},
|
|
3171
|
+
headers,
|
|
3060
3172
|
body,
|
|
3061
|
-
signal: timeout.signal
|
|
3173
|
+
signal: timeout.signal,
|
|
3174
|
+
...proofAttached && { redirect: "manual" }
|
|
3062
3175
|
});
|
|
3176
|
+
if (proofAttached) refuseProofRedirect(response, url2);
|
|
3063
3177
|
if (!response.ok) {
|
|
3064
3178
|
const terminal = isTerminalAuthFailure(response);
|
|
3065
3179
|
const bodyText = await response.text().catch(() => void 0);
|
|
@@ -6179,7 +6293,7 @@ var MAX_HANDSHAKE_REPLY = 1024;
|
|
|
6179
6293
|
var RENEW_BEFORE_MS = 6e4;
|
|
6180
6294
|
var TRANSPORT_BINDING_DOMAIN = "calimero.tee-attest.transport-key.v1";
|
|
6181
6295
|
var NULL_BODY_STATUSES = [101, 204, 205, 304];
|
|
6182
|
-
var
|
|
6296
|
+
var utf82 = new TextEncoder();
|
|
6183
6297
|
var StaleTransportKeyError = class extends Error {
|
|
6184
6298
|
constructor() {
|
|
6185
6299
|
super("The node no longer holds the attested transport key; attest again for the current one");
|
|
@@ -6198,7 +6312,7 @@ var SealedTransportError = class extends Error {
|
|
|
6198
6312
|
async function transportKeyBinding(inner, transportKey) {
|
|
6199
6313
|
const digest = await crypto.subtle.digest(
|
|
6200
6314
|
"SHA-256",
|
|
6201
|
-
concat(
|
|
6315
|
+
concat(utf82.encode(TRANSPORT_BINDING_DOMAIN), inner, transportKey)
|
|
6202
6316
|
);
|
|
6203
6317
|
return new Uint8Array(digest);
|
|
6204
6318
|
}
|
|
@@ -6402,7 +6516,7 @@ function nonce(requestId, index) {
|
|
|
6402
6516
|
return bytes;
|
|
6403
6517
|
}
|
|
6404
6518
|
function joinInner(head, body) {
|
|
6405
|
-
const encoded =
|
|
6519
|
+
const encoded = utf82.encode(JSON.stringify(head));
|
|
6406
6520
|
const len = new Uint8Array(4);
|
|
6407
6521
|
new DataView(len.buffer).setUint32(0, encoded.length, false);
|
|
6408
6522
|
return concat(len, encoded, body);
|
|
@@ -6892,7 +7006,7 @@ var FULCIO_ISSUER_V1 = "1.3.6.1.4.1.57264.1.1";
|
|
|
6892
7006
|
var SAN_URI = 134;
|
|
6893
7007
|
var UTF8_STRING = 12;
|
|
6894
7008
|
var SEQUENCE2 = 48;
|
|
6895
|
-
var
|
|
7009
|
+
var utf83 = new TextEncoder();
|
|
6896
7010
|
var fromUtf8 = (bytes) => new TextDecoder().decode(bytes);
|
|
6897
7011
|
async function verifyCosignBundle(body, bundle, signer, signature) {
|
|
6898
7012
|
const { base64Signature, cert, rekorBundle } = cosignBundle(bundle);
|
|
@@ -6905,7 +7019,7 @@ async function verifyCosignBundle(body, bundle, signer, signature) {
|
|
|
6905
7019
|
const leaf = parseCertificate(leafDer);
|
|
6906
7020
|
const log = REKOR_LOGS.find((candidate) => candidate.logId === payload.logID.toLowerCase());
|
|
6907
7021
|
if (!log) throw new Error(`The bundle names Rekor log ${payload.logID}, which the Sigstore trust root does not know`);
|
|
6908
|
-
const canonical =
|
|
7022
|
+
const canonical = utf83.encode(
|
|
6909
7023
|
`{"body":${JSON.stringify(payload.body)},"integratedTime":${payload.integratedTime},"logID":${JSON.stringify(payload.logID)},"logIndex":${payload.logIndex}}`
|
|
6910
7024
|
);
|
|
6911
7025
|
const setVerifies = await verifyEcdsa(
|
|
@@ -8643,92 +8757,6 @@ var RelayObserver = class {
|
|
|
8643
8757
|
}
|
|
8644
8758
|
};
|
|
8645
8759
|
|
|
8646
|
-
// src/request-proof/request-proof.ts
|
|
8647
|
-
var utf83 = (text) => new TextEncoder().encode(text);
|
|
8648
|
-
var REQUEST_SIGN_DOMAIN = utf83("calimero.auth.request.v1");
|
|
8649
|
-
var REQUEST_BODY_DOMAIN = utf83("calimero.auth.request.body.v1");
|
|
8650
|
-
var REQUEST_PROOF_HEADER = "X-Calimero-Proof";
|
|
8651
|
-
var DEFAULT_VALID_FOR = 300;
|
|
8652
|
-
async function signRequestProof(input) {
|
|
8653
|
-
const { method, path, credential } = input;
|
|
8654
|
-
if (!method) throw new Error("signRequestProof needs the method the request will use");
|
|
8655
|
-
if (!path.startsWith("/")) {
|
|
8656
|
-
throw new Error(`signRequestProof needs a path, got ${JSON.stringify(path)}`);
|
|
8657
|
-
}
|
|
8658
|
-
if (path.includes("?")) {
|
|
8659
|
-
throw new Error(
|
|
8660
|
-
`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.`
|
|
8661
|
-
);
|
|
8662
|
-
}
|
|
8663
|
-
const bodyBytes2 = input.body === void 0 ? new Uint8Array() : typeof input.body === "string" ? utf83(input.body) : input.body;
|
|
8664
|
-
const bodyHash = await domainHash(REQUEST_BODY_DOMAIN, [bodyBytes2]);
|
|
8665
|
-
const issuedAt = Math.floor(input.now ?? Date.now() / 1e3);
|
|
8666
|
-
const expiresAt = issuedAt + (input.validForSeconds ?? DEFAULT_VALID_FOR);
|
|
8667
|
-
const payload = await domainHash(REQUEST_SIGN_DOMAIN, [
|
|
8668
|
-
utf83(method),
|
|
8669
|
-
utf83(path),
|
|
8670
|
-
bodyHash,
|
|
8671
|
-
u64le(issuedAt),
|
|
8672
|
-
u64le(expiresAt)
|
|
8673
|
-
]);
|
|
8674
|
-
const signature = await signPayload(payload, input);
|
|
8675
|
-
if (signature.length !== 64) {
|
|
8676
|
-
throw new Error(`the signer returned ${signature.length} bytes, not a 64-byte signature`);
|
|
8677
|
-
}
|
|
8678
|
-
const requestSig = concat(
|
|
8679
|
-
u32le(utf83(method).length),
|
|
8680
|
-
utf83(method),
|
|
8681
|
-
u32le(utf83(path).length),
|
|
8682
|
-
utf83(path),
|
|
8683
|
-
bodyHash,
|
|
8684
|
-
u64le(issuedAt),
|
|
8685
|
-
u64le(expiresAt),
|
|
8686
|
-
signature
|
|
8687
|
-
);
|
|
8688
|
-
const accountProof = hexToBytes(credential, "credential");
|
|
8689
|
-
return hex(concat(accountProof, new Uint8Array([0]), requestSig));
|
|
8690
|
-
}
|
|
8691
|
-
async function signPayload(payload, input) {
|
|
8692
|
-
if (input.signer) return input.signer.sign(payload);
|
|
8693
|
-
if (!input.deviceSecret) {
|
|
8694
|
-
throw new Error("signRequestProof needs either `deviceSecret` or `signer`");
|
|
8695
|
-
}
|
|
8696
|
-
const key = await importSigningKey(input.deviceSecret);
|
|
8697
|
-
return new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, key, payload));
|
|
8698
|
-
}
|
|
8699
|
-
function hexToBytes(value, label2) {
|
|
8700
|
-
const clean = value.trim();
|
|
8701
|
-
if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length % 2 !== 0) {
|
|
8702
|
-
throw new Error(`${label2} is not an even-length hex string`);
|
|
8703
|
-
}
|
|
8704
|
-
return fromHex(clean, label2, clean.length / 2);
|
|
8705
|
-
}
|
|
8706
|
-
|
|
8707
|
-
// src/request-proof/authorizer.ts
|
|
8708
|
-
function createProofAuthorizer(credential) {
|
|
8709
|
-
if (!credential.credential) {
|
|
8710
|
-
throw new Error("a proof authorizer needs the `credential` from signDeviceCert");
|
|
8711
|
-
}
|
|
8712
|
-
if (!credential.deviceSecret && !credential.signer) {
|
|
8713
|
-
throw new Error("a proof authorizer needs either `deviceSecret` or `signer`");
|
|
8714
|
-
}
|
|
8715
|
-
return async ({ method, path, body }) => {
|
|
8716
|
-
const proof = await signRequestProof({
|
|
8717
|
-
method,
|
|
8718
|
-
path,
|
|
8719
|
-
body,
|
|
8720
|
-
credential: credential.credential,
|
|
8721
|
-
deviceSecret: credential.deviceSecret,
|
|
8722
|
-
signer: credential.signer,
|
|
8723
|
-
validForSeconds: credential.validForSeconds
|
|
8724
|
-
});
|
|
8725
|
-
return { [REQUEST_PROOF_HEADER]: proof };
|
|
8726
|
-
};
|
|
8727
|
-
}
|
|
8728
|
-
function signedPath(url2) {
|
|
8729
|
-
return new URL(url2).pathname;
|
|
8730
|
-
}
|
|
8731
|
-
|
|
8732
8760
|
// src/presence/statement.ts
|
|
8733
8761
|
var PRESENCE_DOMAIN = new TextEncoder().encode("calimero/presence/1");
|
|
8734
8762
|
async function stateHash(state) {
|