@calimero-network/mero-js 19.19.0 → 19.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin-api/admin-client.d.ts +8 -1
- package/dist/admin-api/admin-client.js +9 -0
- package/dist/admin-api/admin-types.d.ts +18 -0
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +226 -0
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.mjs +226 -0
- package/dist/index.mjs.map +4 -4
- package/dist/sealed/index.d.ts +2 -0
- package/dist/sealed/index.js +1 -0
- package/dist/sealed/sealed.d.ts +126 -0
- package/dist/sealed/sealed.js +237 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type { SseEventData, WsEventData, AppVersionChangedEvent, GroupMembership
|
|
|
15
15
|
export { EphemeralClient, jsonCodec } from "./ephemeral/index.js";
|
|
16
16
|
export type { Codec, EphemeralEntry } from "./ephemeral/index.js";
|
|
17
17
|
export * from "./cloud/index.js";
|
|
18
|
+
export * from "./sealed/index.js";
|
|
18
19
|
export * from "./relay/index.js";
|
|
19
20
|
export { signWarrant, intentHash } from "./warrant/index.js";
|
|
20
21
|
export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, signAccountLink, signAccountLogin, } from "./account/index.js";
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,8 @@ export { SseClient, WsClient } from "./events/index.js";
|
|
|
20
20
|
export { EphemeralClient, jsonCodec } from "./ephemeral/index.js";
|
|
21
21
|
// Cloud client — namespaces, relays, HA, and the cloud sign-in path
|
|
22
22
|
export * from "./cloud/index.js";
|
|
23
|
+
// Sealed transport — requests encrypted to a TEE node's attested key
|
|
24
|
+
export * from "./sealed/index.js";
|
|
23
25
|
// Relay client — write through delegated execution, holding only a signing key
|
|
24
26
|
export * from "./relay/index.js";
|
|
25
27
|
// Warrant signing — mint the author's consent for a relay to run one intent
|
package/dist/index.mjs
CHANGED
|
@@ -1668,6 +1668,15 @@ var AdminApiClient = class {
|
|
|
1668
1668
|
const response = await this.httpClient.get(`/admin-api/groups/${groupId}/settings/tee-admission-policy`);
|
|
1669
1669
|
return response.data ?? response;
|
|
1670
1670
|
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Sets which admitted TEEs may author as the TEE authority. Pass
|
|
1673
|
+
* `{ allowedMrtd: [] }` to turn TEE authorship off. `groupId` must be a
|
|
1674
|
+
* namespace root; the node refuses a subgroup. The node exposes no read-back
|
|
1675
|
+
* route for this policy.
|
|
1676
|
+
*/
|
|
1677
|
+
async setTeeAuthoringPolicy(groupId, request) {
|
|
1678
|
+
await this.httpClient.put(`/admin-api/groups/${groupId}/settings/tee-authoring-policy`, request);
|
|
1679
|
+
}
|
|
1671
1680
|
// ---- Group / member / context metadata ----
|
|
1672
1681
|
async setGroupMetadata(groupId, request) {
|
|
1673
1682
|
await this.httpClient.put(`/admin-api/groups/${groupId}/metadata`, request);
|
|
@@ -6480,6 +6489,216 @@ async function defaultNonceSource(deviceSecret) {
|
|
|
6480
6489
|
}
|
|
6481
6490
|
}
|
|
6482
6491
|
|
|
6492
|
+
// src/sealed/sealed.ts
|
|
6493
|
+
var SEALED_PATH = "/sealed/v1";
|
|
6494
|
+
var SEALED_CONTENT_TYPE = "application/vnd.calimero.sealed";
|
|
6495
|
+
var VERSION = 1;
|
|
6496
|
+
var NONCE_LEN = 12;
|
|
6497
|
+
var REQUEST_DOMAIN = "calimero/sealed-http/v1/request";
|
|
6498
|
+
var RESPONSE_DOMAIN = "calimero/sealed-http/v1/response";
|
|
6499
|
+
var TRANSPORT_BINDING_DOMAIN = "calimero.tee-attest.transport-key.v1";
|
|
6500
|
+
var PKCS8_X25519_PREFIX = new Uint8Array([
|
|
6501
|
+
48,
|
|
6502
|
+
46,
|
|
6503
|
+
2,
|
|
6504
|
+
1,
|
|
6505
|
+
0,
|
|
6506
|
+
48,
|
|
6507
|
+
5,
|
|
6508
|
+
6,
|
|
6509
|
+
3,
|
|
6510
|
+
43,
|
|
6511
|
+
101,
|
|
6512
|
+
110,
|
|
6513
|
+
4,
|
|
6514
|
+
34,
|
|
6515
|
+
4,
|
|
6516
|
+
32
|
|
6517
|
+
]);
|
|
6518
|
+
var utf8 = new TextEncoder();
|
|
6519
|
+
var StaleTransportKeyError = class extends Error {
|
|
6520
|
+
constructor() {
|
|
6521
|
+
super(
|
|
6522
|
+
"The node no longer holds the transport key this request was sealed to; attest again for the current one"
|
|
6523
|
+
);
|
|
6524
|
+
this.name = "StaleTransportKeyError";
|
|
6525
|
+
}
|
|
6526
|
+
};
|
|
6527
|
+
var SealedTransportError = class extends Error {
|
|
6528
|
+
constructor(status, code, message) {
|
|
6529
|
+
super(message);
|
|
6530
|
+
this.status = status;
|
|
6531
|
+
this.code = code;
|
|
6532
|
+
this.name = "SealedTransportError";
|
|
6533
|
+
}
|
|
6534
|
+
};
|
|
6535
|
+
async function transportKeyBinding(inner, transportKey) {
|
|
6536
|
+
const digest = await crypto.subtle.digest(
|
|
6537
|
+
"SHA-256",
|
|
6538
|
+
concat(utf8.encode(TRANSPORT_BINDING_DOMAIN), inner, transportKey)
|
|
6539
|
+
);
|
|
6540
|
+
return new Uint8Array(digest);
|
|
6541
|
+
}
|
|
6542
|
+
async function fetchAttestedTransportKey(admin, verify, options = {}) {
|
|
6543
|
+
if (options.applicationId === void 0 !== (options.applicationHash === void 0)) {
|
|
6544
|
+
throw new Error("applicationId and applicationHash are given together or not at all");
|
|
6545
|
+
}
|
|
6546
|
+
const nonce = hex(crypto.getRandomValues(new Uint8Array(32)));
|
|
6547
|
+
const attested = await admin.teeAttest({
|
|
6548
|
+
nonce,
|
|
6549
|
+
applicationId: options.applicationId,
|
|
6550
|
+
bindTransportKey: true
|
|
6551
|
+
});
|
|
6552
|
+
if (!attested.transportPublicKey) {
|
|
6553
|
+
throw new Error("The node did not report a transport key; it predates sealed transport");
|
|
6554
|
+
}
|
|
6555
|
+
const transportKey = fromHex(attested.transportPublicKey, "transportPublicKey", 32);
|
|
6556
|
+
const inner = options.applicationHash ? fromHex(options.applicationHash, "applicationHash", 32) : new Uint8Array(32);
|
|
6557
|
+
const reportDataSuffix = hex(await transportKeyBinding(inner, transportKey));
|
|
6558
|
+
if (!await verify({ quoteB64: attested.quoteB64, nonce, reportDataSuffix })) {
|
|
6559
|
+
throw new Error("The attestation did not verify, so its transport key is not trusted");
|
|
6560
|
+
}
|
|
6561
|
+
return transportKey;
|
|
6562
|
+
}
|
|
6563
|
+
function createSealedFetch(options) {
|
|
6564
|
+
const base = new URL(options.baseUrl);
|
|
6565
|
+
const basePath = base.pathname.replace(/\/+$/, "");
|
|
6566
|
+
const sealedUrl = `${base.origin}${basePath}${SEALED_PATH}`;
|
|
6567
|
+
const transportKey = options.transportPublicKey;
|
|
6568
|
+
if (transportKey.length !== 32) {
|
|
6569
|
+
throw new Error("transportPublicKey must be 32 bytes");
|
|
6570
|
+
}
|
|
6571
|
+
const baseFetch = options.fetch ?? ((input, init) => fetch(input, init));
|
|
6572
|
+
return async (input, init) => {
|
|
6573
|
+
const request = new Request(input, init);
|
|
6574
|
+
const url = new URL(request.url);
|
|
6575
|
+
if (url.origin !== base.origin || !url.pathname.startsWith(`${basePath}/`)) {
|
|
6576
|
+
throw new Error(`Refusing to send ${url.href} outside the sealed node at ${options.baseUrl}`);
|
|
6577
|
+
}
|
|
6578
|
+
const headers = [];
|
|
6579
|
+
request.headers.forEach((value, name) => headers.push([name, value]));
|
|
6580
|
+
const head = {
|
|
6581
|
+
method: request.method,
|
|
6582
|
+
path: url.pathname.slice(basePath.length) + url.search,
|
|
6583
|
+
headers,
|
|
6584
|
+
ts: Math.floor(Date.now() / 1e3)
|
|
6585
|
+
};
|
|
6586
|
+
const body = new Uint8Array(await request.arrayBuffer());
|
|
6587
|
+
const exchange = await sealRequest(transportKey, head, body);
|
|
6588
|
+
const response = await baseFetch(sealedUrl, {
|
|
6589
|
+
method: "POST",
|
|
6590
|
+
headers: { "content-type": SEALED_CONTENT_TYPE },
|
|
6591
|
+
body: exchange.envelope,
|
|
6592
|
+
signal: request.signal
|
|
6593
|
+
});
|
|
6594
|
+
if (response.headers.get("content-type") !== SEALED_CONTENT_TYPE) {
|
|
6595
|
+
throw await envelopeRefusal(response);
|
|
6596
|
+
}
|
|
6597
|
+
const opened = await exchange.open(new Uint8Array(await response.arrayBuffer()));
|
|
6598
|
+
const nullBody = [101, 204, 205, 304].includes(opened.head.status);
|
|
6599
|
+
return new Response(nullBody ? null : opened.body, {
|
|
6600
|
+
status: opened.head.status,
|
|
6601
|
+
headers: opened.head.headers
|
|
6602
|
+
});
|
|
6603
|
+
};
|
|
6604
|
+
}
|
|
6605
|
+
async function envelopeRefusal(response) {
|
|
6606
|
+
let code;
|
|
6607
|
+
let message = `Sealed request refused: HTTP ${response.status}`;
|
|
6608
|
+
try {
|
|
6609
|
+
const body = await response.json();
|
|
6610
|
+
code = body?.error?.code;
|
|
6611
|
+
if (body?.error?.message) message = `Sealed request refused: ${body.error.message}`;
|
|
6612
|
+
} catch {
|
|
6613
|
+
}
|
|
6614
|
+
if (code === "stale_transport_key") return new StaleTransportKeyError();
|
|
6615
|
+
return new SealedTransportError(response.status, code, message);
|
|
6616
|
+
}
|
|
6617
|
+
async function sealRequest(transportKey, head, body, fixed) {
|
|
6618
|
+
const { privateKey, publicKey } = await clientKeyPair(fixed?.clientSecret);
|
|
6619
|
+
const transport = await crypto.subtle.importKey("raw", transportKey, { name: "X25519" }, false, []);
|
|
6620
|
+
const shared = new Uint8Array(
|
|
6621
|
+
await crypto.subtle.deriveBits({ name: "X25519", public: transport }, privateKey, 256)
|
|
6622
|
+
);
|
|
6623
|
+
if (shared.every((byte) => byte === 0)) {
|
|
6624
|
+
throw new Error("The transport key is a low-order point");
|
|
6625
|
+
}
|
|
6626
|
+
const salt = concat(transportKey, publicKey);
|
|
6627
|
+
const requestKey = await exchangeKey(shared, salt, REQUEST_DOMAIN, "encrypt");
|
|
6628
|
+
const responseKey = await exchangeKey(shared, salt, RESPONSE_DOMAIN, "decrypt");
|
|
6629
|
+
const nonce = fixed?.nonce ?? crypto.getRandomValues(new Uint8Array(NONCE_LEN));
|
|
6630
|
+
const header = concat(new Uint8Array([VERSION]), transportKey, publicKey, nonce);
|
|
6631
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
6632
|
+
{ name: "AES-GCM", iv: nonce, additionalData: header },
|
|
6633
|
+
requestKey,
|
|
6634
|
+
joinInner(head, body)
|
|
6635
|
+
);
|
|
6636
|
+
const responseAad = concat(new Uint8Array([VERSION]), salt);
|
|
6637
|
+
return {
|
|
6638
|
+
envelope: concat(header, new Uint8Array(ciphertext)),
|
|
6639
|
+
async open(sealed) {
|
|
6640
|
+
if (sealed.length < 1 + NONCE_LEN + 16 || sealed[0] !== VERSION) {
|
|
6641
|
+
throw new Error("The sealed response is malformed");
|
|
6642
|
+
}
|
|
6643
|
+
let plaintext;
|
|
6644
|
+
try {
|
|
6645
|
+
plaintext = new Uint8Array(
|
|
6646
|
+
await crypto.subtle.decrypt(
|
|
6647
|
+
{ name: "AES-GCM", iv: sealed.slice(1, 1 + NONCE_LEN), additionalData: responseAad },
|
|
6648
|
+
responseKey,
|
|
6649
|
+
sealed.slice(1 + NONCE_LEN)
|
|
6650
|
+
)
|
|
6651
|
+
);
|
|
6652
|
+
} catch {
|
|
6653
|
+
throw new Error("The sealed response did not open: it was not sealed by the attested node");
|
|
6654
|
+
}
|
|
6655
|
+
const view = new DataView(plaintext.buffer, plaintext.byteOffset, plaintext.byteLength);
|
|
6656
|
+
const len = view.getUint32(0, false);
|
|
6657
|
+
const head2 = JSON.parse(new TextDecoder().decode(plaintext.slice(4, 4 + len)));
|
|
6658
|
+
return { head: head2, body: plaintext.slice(4 + len) };
|
|
6659
|
+
}
|
|
6660
|
+
};
|
|
6661
|
+
}
|
|
6662
|
+
function joinInner(head, body) {
|
|
6663
|
+
const encoded = utf8.encode(JSON.stringify(head));
|
|
6664
|
+
const len = new Uint8Array(4);
|
|
6665
|
+
new DataView(len.buffer).setUint32(0, encoded.length, false);
|
|
6666
|
+
return concat(len, encoded, body);
|
|
6667
|
+
}
|
|
6668
|
+
async function exchangeKey(shared, salt, domain, usage) {
|
|
6669
|
+
const ikm = await crypto.subtle.importKey("raw", shared, "HKDF", false, ["deriveKey"]);
|
|
6670
|
+
return crypto.subtle.deriveKey(
|
|
6671
|
+
{ name: "HKDF", hash: "SHA-256", salt, info: utf8.encode(domain) },
|
|
6672
|
+
ikm,
|
|
6673
|
+
{ name: "AES-GCM", length: 256 },
|
|
6674
|
+
false,
|
|
6675
|
+
[usage]
|
|
6676
|
+
);
|
|
6677
|
+
}
|
|
6678
|
+
async function clientKeyPair(secret) {
|
|
6679
|
+
if (!secret) {
|
|
6680
|
+
const pair = await crypto.subtle.generateKey({ name: "X25519" }, true, [
|
|
6681
|
+
"deriveBits"
|
|
6682
|
+
]);
|
|
6683
|
+
const publicKey = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey));
|
|
6684
|
+
return { privateKey: pair.privateKey, publicKey };
|
|
6685
|
+
}
|
|
6686
|
+
const privateKey = await crypto.subtle.importKey(
|
|
6687
|
+
"pkcs8",
|
|
6688
|
+
concat(PKCS8_X25519_PREFIX, secret),
|
|
6689
|
+
{ name: "X25519" },
|
|
6690
|
+
true,
|
|
6691
|
+
["deriveBits"]
|
|
6692
|
+
);
|
|
6693
|
+
const jwk = await crypto.subtle.exportKey("jwk", privateKey);
|
|
6694
|
+
return { privateKey, publicKey: base64UrlDecode(jwk.x) };
|
|
6695
|
+
}
|
|
6696
|
+
function base64UrlDecode(value) {
|
|
6697
|
+
const base643 = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
6698
|
+
const binary = atob(base643 + "=".repeat((4 - base643.length % 4) % 4));
|
|
6699
|
+
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
6700
|
+
}
|
|
6701
|
+
|
|
6483
6702
|
// src/namespace-op/namespace-op.ts
|
|
6484
6703
|
var SIGNED_NAMESPACE_OP_SCHEMA_VERSION = 8;
|
|
6485
6704
|
var NAMESPACE_SIGN_DOMAIN = new TextEncoder().encode("calimero.namespace.v1");
|
|
@@ -6822,8 +7041,12 @@ export {
|
|
|
6822
7041
|
RelayClient,
|
|
6823
7042
|
RpcClient,
|
|
6824
7043
|
RpcError,
|
|
7044
|
+
SEALED_CONTENT_TYPE,
|
|
7045
|
+
SEALED_PATH,
|
|
6825
7046
|
SIGNED_NAMESPACE_OP_SCHEMA_VERSION,
|
|
7047
|
+
SealedTransportError,
|
|
6826
7048
|
SseClient,
|
|
7049
|
+
StaleTransportKeyError,
|
|
6827
7050
|
WebHttpClient,
|
|
6828
7051
|
WsClient,
|
|
6829
7052
|
accountForRoot,
|
|
@@ -6851,11 +7074,13 @@ export {
|
|
|
6851
7074
|
createNodeHttpClient,
|
|
6852
7075
|
createProofSigner,
|
|
6853
7076
|
createRetryableMethod,
|
|
7077
|
+
createSealedFetch,
|
|
6854
7078
|
createTimeoutSignal,
|
|
6855
7079
|
createUniversalHttpClient,
|
|
6856
7080
|
deviceCertPayload,
|
|
6857
7081
|
discoverLocalNodes,
|
|
6858
7082
|
encodeSignedInvitation,
|
|
7083
|
+
fetchAttestedTransportKey,
|
|
6859
7084
|
generateAccountRoot,
|
|
6860
7085
|
generateSessionKey,
|
|
6861
7086
|
hasCap,
|
|
@@ -6878,6 +7103,7 @@ export {
|
|
|
6878
7103
|
signRequest,
|
|
6879
7104
|
signRoutingChallenge,
|
|
6880
7105
|
signWarrant,
|
|
7106
|
+
transportKeyBinding,
|
|
6881
7107
|
withCap,
|
|
6882
7108
|
withRetry,
|
|
6883
7109
|
withoutCap
|