@calimero-network/mero-js 23.1.0 → 23.3.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/cloud/account-ownership.d.ts +30 -0
- package/dist/cloud/account-ownership.js +55 -0
- package/dist/cloud/cloud-client.d.ts +8 -0
- package/dist/cloud/cloud-client.js +9 -0
- package/dist/cloud/index.d.ts +2 -0
- package/dist/cloud/index.js +1 -0
- package/dist/ephemeral/index.d.ts +6 -0
- package/dist/ephemeral/index.js +39 -29
- package/dist/ephemeral/types.d.ts +5 -0
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +223 -40
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/index.mjs +223 -40
- package/dist/index.mjs.map +4 -4
- package/dist/presence/relay-presence.d.ts +56 -0
- package/dist/presence/relay-presence.js +107 -0
- package/dist/presence/statement.d.ts +12 -0
- package/dist/presence/statement.js +17 -0
- package/dist/relay/relay-client.d.ts +13 -0
- package/dist/relay/relay-client.js +12 -0
- package/dist/transport/mero-client.d.ts +11 -12
- package/dist/transport/mero-client.js +14 -15
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -39,9 +39,11 @@ __export(index_exports, {
|
|
|
39
39
|
MeroJs: () => MeroJs,
|
|
40
40
|
NODE_RELEASE_SIGNER: () => NODE_RELEASE_SIGNER,
|
|
41
41
|
OWNER_OP_KIND: () => OWNER_OP_KIND,
|
|
42
|
+
PRESENCE_DOMAIN: () => PRESENCE_DOMAIN,
|
|
42
43
|
REQUEST_PROOF_HEADER: () => REQUEST_PROOF_HEADER,
|
|
43
44
|
RelayClient: () => RelayClient,
|
|
44
45
|
RelayObserver: () => RelayObserver,
|
|
46
|
+
RelayPresenceClient: () => RelayPresenceClient,
|
|
45
47
|
RelayTransport: () => RelayTransport,
|
|
46
48
|
RpcClient: () => RpcClient,
|
|
47
49
|
RpcError: () => RpcError,
|
|
@@ -161,6 +163,7 @@ __export(index_exports, {
|
|
|
161
163
|
parseDeviceCredential: () => parseDeviceCredential,
|
|
162
164
|
parseGovernanceWarrant: () => parseGovernanceWarrant,
|
|
163
165
|
parseWarrantNonce: () => parseWarrantNonce,
|
|
166
|
+
presenceStatementBytes: () => presenceStatementBytes,
|
|
164
167
|
probeNodeHealth: () => probeNodeHealth,
|
|
165
168
|
readCloudLinkCallback: () => readCloudLinkCallback,
|
|
166
169
|
readEnrolmentCallback: () => readEnrolmentCallback,
|
|
@@ -174,6 +177,7 @@ __export(index_exports, {
|
|
|
174
177
|
shouldRetain: () => shouldRetain,
|
|
175
178
|
signAccountLink: () => signAccountLink,
|
|
176
179
|
signAccountLogin: () => signAccountLogin,
|
|
180
|
+
signAccountOwnershipClaim: () => signAccountOwnershipClaim,
|
|
177
181
|
signCreationWarrant: () => signCreationWarrant,
|
|
178
182
|
signDeviceCert: () => signDeviceCert,
|
|
179
183
|
signDeviceScope: () => signDeviceScope,
|
|
@@ -189,8 +193,10 @@ __export(index_exports, {
|
|
|
189
193
|
signedPath: () => signedPath,
|
|
190
194
|
signerFromCryptoKey: () => signerFromCryptoKey,
|
|
191
195
|
signerFromSecret: () => signerFromSecret,
|
|
196
|
+
stateHash: () => stateHash,
|
|
192
197
|
subgroupCreation: () => subgroupCreation,
|
|
193
198
|
subgroupVisibilitySetOp: () => subgroupVisibilitySetOp,
|
|
199
|
+
subscribePresence: () => subscribePresence,
|
|
194
200
|
teeAdmissionPolicyOp: () => teeAdmissionPolicyOp,
|
|
195
201
|
teeAuthoringPolicyOp: () => teeAuthoringPolicyOp,
|
|
196
202
|
teeReleaseAdmissionPolicyOp: () => teeReleaseAdmissionPolicyOp,
|
|
@@ -3500,28 +3506,32 @@ var EphemeralClient = class {
|
|
|
3500
3506
|
* and zero mean different things). Returns an unsubscribe function.
|
|
3501
3507
|
*/
|
|
3502
3508
|
subscribe(contextId, handler, codec = jsonCodec()) {
|
|
3503
|
-
|
|
3504
|
-
const e = event;
|
|
3505
|
-
if (e.type !== "Ephemeral" || e.contextId !== contextId) return;
|
|
3506
|
-
const data = e.data;
|
|
3507
|
-
if (!data?.author) return;
|
|
3508
|
-
const removed = Boolean(data.removed);
|
|
3509
|
-
const entry = {
|
|
3510
|
-
author: data.author,
|
|
3511
|
-
state: removed || data.state === void 0 ? void 0 : codec.decode(data.state),
|
|
3512
|
-
removed
|
|
3513
|
-
};
|
|
3514
|
-
if (data.ageMs !== void 0) entry.ageMs = data.ageMs;
|
|
3515
|
-
handler(entry);
|
|
3516
|
-
};
|
|
3517
|
-
this.sse.on("event", listener);
|
|
3518
|
-
void this.sse.connect().catch(() => void 0);
|
|
3519
|
-
void this.sse.subscribe([contextId]).catch(() => void 0);
|
|
3520
|
-
return () => {
|
|
3521
|
-
this.sse.off("event", listener);
|
|
3522
|
-
};
|
|
3509
|
+
return subscribePresence(this.sse, contextId, handler, codec);
|
|
3523
3510
|
}
|
|
3524
3511
|
};
|
|
3512
|
+
function subscribePresence(sse, contextId, handler, codec = jsonCodec()) {
|
|
3513
|
+
const listener = (event) => {
|
|
3514
|
+
const e = event;
|
|
3515
|
+
if (e.type !== "Ephemeral" || e.contextId !== contextId) return;
|
|
3516
|
+
const data = e.data;
|
|
3517
|
+
if (!data?.author) return;
|
|
3518
|
+
const removed = Boolean(data.removed);
|
|
3519
|
+
const entry = {
|
|
3520
|
+
author: data.author,
|
|
3521
|
+
state: removed || data.state === void 0 ? void 0 : codec.decode(data.state),
|
|
3522
|
+
removed
|
|
3523
|
+
};
|
|
3524
|
+
if (data.ageMs !== void 0) entry.ageMs = data.ageMs;
|
|
3525
|
+
if (data.account !== void 0) entry.account = data.account;
|
|
3526
|
+
handler(entry);
|
|
3527
|
+
};
|
|
3528
|
+
sse.on("event", listener);
|
|
3529
|
+
void sse.connect().catch(() => void 0);
|
|
3530
|
+
void sse.subscribe([contextId]).catch(() => void 0);
|
|
3531
|
+
return () => {
|
|
3532
|
+
sse.off("event", listener);
|
|
3533
|
+
};
|
|
3534
|
+
}
|
|
3525
3535
|
|
|
3526
3536
|
// src/signer/signer.ts
|
|
3527
3537
|
var PKCS8_ED25519_PREFIX2 = new Uint8Array([
|
|
@@ -6482,6 +6492,19 @@ var CloudClient = class {
|
|
|
6482
6492
|
{ body }
|
|
6483
6493
|
);
|
|
6484
6494
|
}
|
|
6495
|
+
/**
|
|
6496
|
+
* Enable HA for a namespace this ACCOUNT founded through a relay, with the
|
|
6497
|
+
* founder's claim (`signAccountOwnershipClaim`) instead of a node's
|
|
6498
|
+
* ownership proof: a nodeless account holds no node that could issue one.
|
|
6499
|
+
* The login must be linked to that account.
|
|
6500
|
+
*/
|
|
6501
|
+
async enableFoundedNamespaceHa(namespaceId, ownershipProof) {
|
|
6502
|
+
return this.request(
|
|
6503
|
+
"POST",
|
|
6504
|
+
`/api/cloud/me/namespaces/${encodeURIComponent(namespaceId)}/enable-ha`,
|
|
6505
|
+
{ body: { groups: [], ownership_proof: ownershipProof } }
|
|
6506
|
+
);
|
|
6507
|
+
}
|
|
6485
6508
|
/**
|
|
6486
6509
|
* Stop hosting a claimed namespace.
|
|
6487
6510
|
*
|
|
@@ -7934,6 +7957,19 @@ var RelayClient = class {
|
|
|
7934
7957
|
);
|
|
7935
7958
|
return { rootHash: body.data.rootHash, returns: body.data.returns ?? null };
|
|
7936
7959
|
}
|
|
7960
|
+
/**
|
|
7961
|
+
* Publish one presence statement through the relay (`presence-intents`).
|
|
7962
|
+
*
|
|
7963
|
+
* Not retried on 429: presence resends every 2.5 s anyway, and a retry would
|
|
7964
|
+
* only queue behind the next resend. A refusal throws with its `status`.
|
|
7965
|
+
*/
|
|
7966
|
+
async presenceIntent(contextId, body) {
|
|
7967
|
+
await this.jsonOnce(
|
|
7968
|
+
"POST",
|
|
7969
|
+
`/admin-api/contexts/${encodeURIComponent(contextId)}/presence-intents`,
|
|
7970
|
+
body
|
|
7971
|
+
);
|
|
7972
|
+
}
|
|
7937
7973
|
/**
|
|
7938
7974
|
* Ask the relay what it can do about creating contexts in `groupId`.
|
|
7939
7975
|
*
|
|
@@ -8235,6 +8271,7 @@ var RelayClient = class {
|
|
|
8235
8271
|
text
|
|
8236
8272
|
);
|
|
8237
8273
|
}
|
|
8274
|
+
if (response.status === 204) return void 0;
|
|
8238
8275
|
return await response.json();
|
|
8239
8276
|
}
|
|
8240
8277
|
};
|
|
@@ -8662,6 +8699,121 @@ function signedPath(url) {
|
|
|
8662
8699
|
return new URL(url).pathname;
|
|
8663
8700
|
}
|
|
8664
8701
|
|
|
8702
|
+
// src/presence/statement.ts
|
|
8703
|
+
var PRESENCE_DOMAIN = new TextEncoder().encode("calimero/presence/1");
|
|
8704
|
+
async function stateHash(state) {
|
|
8705
|
+
const borsh = state === null ? new Uint8Array([0]) : concat(new Uint8Array([1]), u32le(state.length), state);
|
|
8706
|
+
return new Uint8Array(await crypto.subtle.digest("SHA-256", borsh));
|
|
8707
|
+
}
|
|
8708
|
+
async function presenceStatementBytes(input) {
|
|
8709
|
+
return concat(
|
|
8710
|
+
PRESENCE_DOMAIN,
|
|
8711
|
+
fromHex(input.contextId, "contextId", 32),
|
|
8712
|
+
fromHex(input.author, "author", 32),
|
|
8713
|
+
u64le(input.seq),
|
|
8714
|
+
u64le(input.sentAtMs),
|
|
8715
|
+
await stateHash(input.state)
|
|
8716
|
+
);
|
|
8717
|
+
}
|
|
8718
|
+
|
|
8719
|
+
// src/presence/relay-presence.ts
|
|
8720
|
+
var HEARTBEAT_MS = 2500;
|
|
8721
|
+
var RelayPresenceClient = class {
|
|
8722
|
+
/**
|
|
8723
|
+
* @param opts.events the relay's event stream, asked for only on `subscribe`:
|
|
8724
|
+
* publishing needs none, so a client with no node key can still publish.
|
|
8725
|
+
*/
|
|
8726
|
+
constructor(opts) {
|
|
8727
|
+
this.timers = /* @__PURE__ */ new Map();
|
|
8728
|
+
this.lastSeq = /* @__PURE__ */ new Map();
|
|
8729
|
+
this.refused = /* @__PURE__ */ new Set();
|
|
8730
|
+
/**
|
|
8731
|
+
* Bumped by every `set` and `close`, per context. A `set` installs its
|
|
8732
|
+
* resends only if nothing newer happened while its first send was in flight,
|
|
8733
|
+
* so a retract (or a newer state) can never be overtaken by an older one.
|
|
8734
|
+
*/
|
|
8735
|
+
this.generation = /* @__PURE__ */ new Map();
|
|
8736
|
+
this.relay = opts.relay;
|
|
8737
|
+
this.events = opts.events;
|
|
8738
|
+
this.heartbeatMs = opts.heartbeatMs ?? HEARTBEAT_MS;
|
|
8739
|
+
this.now = opts.now ?? Date.now;
|
|
8740
|
+
}
|
|
8741
|
+
/**
|
|
8742
|
+
* Set this account's presence in `contextId`, or retract it with `null`.
|
|
8743
|
+
* Throws when the relay refuses this first send for good (a 4xx other than
|
|
8744
|
+
* 429), which also stops the resends. A rate limit, a network failure or a
|
|
8745
|
+
* 5xx is not thrown: the next resend covers it.
|
|
8746
|
+
*/
|
|
8747
|
+
async set(contextId, state, codec = jsonCodec()) {
|
|
8748
|
+
const generation = this.bump(contextId);
|
|
8749
|
+
this.refused.delete(contextId);
|
|
8750
|
+
const bytes = state === null ? null : new Uint8Array(codec.encode(state));
|
|
8751
|
+
await this.send(contextId, bytes);
|
|
8752
|
+
if (bytes === null || this.refused.has(contextId)) return;
|
|
8753
|
+
if (this.generation.get(contextId) !== generation) return;
|
|
8754
|
+
this.timers.set(
|
|
8755
|
+
contextId,
|
|
8756
|
+
setInterval(() => void this.send(contextId, bytes).catch(() => void 0), this.heartbeatMs)
|
|
8757
|
+
);
|
|
8758
|
+
}
|
|
8759
|
+
subscribe(contextId, handler, codec = jsonCodec()) {
|
|
8760
|
+
return subscribePresence(this.events(), contextId, handler, codec);
|
|
8761
|
+
}
|
|
8762
|
+
/** Stop every resend. Entries expire on the relay and its peers within 7 s. */
|
|
8763
|
+
close() {
|
|
8764
|
+
for (const contextId of /* @__PURE__ */ new Set([...this.timers.keys(), ...this.generation.keys()])) {
|
|
8765
|
+
this.bump(contextId);
|
|
8766
|
+
}
|
|
8767
|
+
}
|
|
8768
|
+
/** Stop the context's resends and invalidate any `set` still in flight. */
|
|
8769
|
+
bump(contextId) {
|
|
8770
|
+
this.stop(contextId);
|
|
8771
|
+
const next = (this.generation.get(contextId) ?? 0) + 1;
|
|
8772
|
+
this.generation.set(contextId, next);
|
|
8773
|
+
return next;
|
|
8774
|
+
}
|
|
8775
|
+
stop(contextId) {
|
|
8776
|
+
const timer = this.timers.get(contextId);
|
|
8777
|
+
if (timer !== void 0) clearInterval(timer);
|
|
8778
|
+
this.timers.delete(contextId);
|
|
8779
|
+
}
|
|
8780
|
+
/** Wall-clock seeded and strictly rising, so a relay restart never sees an old seq. */
|
|
8781
|
+
nextSeq(contextId) {
|
|
8782
|
+
const seq = Math.max(this.now(), (this.lastSeq.get(contextId) ?? 0) + 1);
|
|
8783
|
+
this.lastSeq.set(contextId, seq);
|
|
8784
|
+
return seq;
|
|
8785
|
+
}
|
|
8786
|
+
async send(contextId, state) {
|
|
8787
|
+
if (this.refused.has(contextId)) return;
|
|
8788
|
+
const signer = await this.relay.authorSigner();
|
|
8789
|
+
const seq = this.nextSeq(contextId);
|
|
8790
|
+
const sentAtMs = this.now();
|
|
8791
|
+
const statement = await presenceStatementBytes({
|
|
8792
|
+
contextId,
|
|
8793
|
+
author: signer.publicKey,
|
|
8794
|
+
seq: BigInt(seq),
|
|
8795
|
+
sentAtMs: BigInt(sentAtMs),
|
|
8796
|
+
state
|
|
8797
|
+
});
|
|
8798
|
+
const signature = hex(await signer.sign(statement));
|
|
8799
|
+
try {
|
|
8800
|
+
await this.relay.presenceIntent(contextId, {
|
|
8801
|
+
state: state === null ? null : hex(state),
|
|
8802
|
+
seq,
|
|
8803
|
+
sentAtMs,
|
|
8804
|
+
signature,
|
|
8805
|
+
authorProof: this.relay.authorProof
|
|
8806
|
+
});
|
|
8807
|
+
} catch (err) {
|
|
8808
|
+
const status = err.status;
|
|
8809
|
+
if (status === void 0 || status === 0 || status === 429 || status >= 500) return;
|
|
8810
|
+
this.refused.add(contextId);
|
|
8811
|
+
this.stop(contextId);
|
|
8812
|
+
throw err;
|
|
8813
|
+
}
|
|
8814
|
+
}
|
|
8815
|
+
};
|
|
8816
|
+
|
|
8665
8817
|
// src/transport/mero-client.ts
|
|
8666
8818
|
function relayHasNo(surface, why) {
|
|
8667
8819
|
return new Error(
|
|
@@ -8705,6 +8857,7 @@ var MeroClient = class {
|
|
|
8705
8857
|
this.proofAuthorizer = null;
|
|
8706
8858
|
this.relayUrl = null;
|
|
8707
8859
|
this.proofEvents = null;
|
|
8860
|
+
this.relayPresence = null;
|
|
8708
8861
|
if (config.transport === "relay") {
|
|
8709
8862
|
this.transport = "relay";
|
|
8710
8863
|
this.nodeClient = null;
|
|
@@ -8859,26 +9012,19 @@ var MeroClient = class {
|
|
|
8859
9012
|
/**
|
|
8860
9013
|
* Ephemeral presence. Node transport only.
|
|
8861
9014
|
*
|
|
8862
|
-
*
|
|
8863
|
-
*
|
|
8864
|
-
*
|
|
8865
|
-
*
|
|
8866
|
-
*
|
|
8867
|
-
*
|
|
8868
|
-
*
|
|
8869
|
-
*
|
|
8870
|
-
* The rejected alternative was to expose it over the relay session and let
|
|
8871
|
-
* `set` fail at the node. That trades a clear construction-time answer for a
|
|
8872
|
-
* runtime failure inside a UI that has already rendered.
|
|
9015
|
+
* On a relay client this is an account's presence client
|
|
9016
|
+
* ({@link RelayPresenceClient}): `set` signs each update with the device key
|
|
9017
|
+
* and posts it to the relay's `presence-intents`, resending every 2.5 s, and
|
|
9018
|
+
* `subscribe` reads the relay's event stream. The relay checks and forwards
|
|
9019
|
+
* the update; other members verify the device's signature and certificate
|
|
9020
|
+
* themselves, so it can drop or delay an account's presence but not invent or
|
|
9021
|
+
* alter it. Only `subscribe` needs the relay's node key (for the event
|
|
9022
|
+
* stream), so publishing works on any relay client.
|
|
8873
9023
|
*/
|
|
8874
9024
|
get ephemeral() {
|
|
8875
|
-
if (
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
"publishing resolves the author from an owned context identity on the node, which a delegated session is not known to have \u2014 read presence off `client.events` instead"
|
|
8879
|
-
);
|
|
8880
|
-
}
|
|
8881
|
-
return this.nodeClient.ephemeral;
|
|
9025
|
+
if (this.nodeClient) return this.nodeClient.ephemeral;
|
|
9026
|
+
this.relayPresence ?? (this.relayPresence = new RelayPresenceClient({ relay: this.relay, events: () => this.events }));
|
|
9027
|
+
return this.relayPresence;
|
|
8882
9028
|
}
|
|
8883
9029
|
/**
|
|
8884
9030
|
* Calimero Cloud.
|
|
@@ -9183,8 +9329,8 @@ async function hkdf2(chainingKey, inputKeyMaterial) {
|
|
|
9183
9329
|
return [first, second];
|
|
9184
9330
|
}
|
|
9185
9331
|
function base64UrlDecode(value) {
|
|
9186
|
-
const
|
|
9187
|
-
const binary = atob(
|
|
9332
|
+
const base644 = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
9333
|
+
const binary = atob(base644 + "=".repeat((4 - base644.length % 4) % 4));
|
|
9188
9334
|
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
9189
9335
|
}
|
|
9190
9336
|
|
|
@@ -9840,6 +9986,43 @@ function assertSafeReturnTo(returnTo) {
|
|
|
9840
9986
|
);
|
|
9841
9987
|
}
|
|
9842
9988
|
|
|
9989
|
+
// src/cloud/account-ownership.ts
|
|
9990
|
+
var DOMAIN = new TextEncoder().encode("calimero.mdma.account-ownership-claim.v1\0");
|
|
9991
|
+
var DEFAULT_TTL_MS = 6e4;
|
|
9992
|
+
var MAX_TTL_MS = 5 * 6e4;
|
|
9993
|
+
function base643(bytes) {
|
|
9994
|
+
let binary = "";
|
|
9995
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
9996
|
+
return btoa(binary);
|
|
9997
|
+
}
|
|
9998
|
+
var hex32 = (value, what) => {
|
|
9999
|
+
if (!/^[0-9a-fA-F]{64}$/.test(value)) throw new Error(`${what} must be 64 hex characters`);
|
|
10000
|
+
return value.toLowerCase();
|
|
10001
|
+
};
|
|
10002
|
+
async function signAccountOwnershipClaim(input) {
|
|
10003
|
+
const ttl = input.ttlMs ?? DEFAULT_TTL_MS;
|
|
10004
|
+
if (!(ttl > 0 && ttl <= MAX_TTL_MS)) throw new Error(`ttlMs must be in (0, ${MAX_TTL_MS}]`);
|
|
10005
|
+
if (!input.subject) throw new Error("subject must be the cloud login email");
|
|
10006
|
+
const signer = await resolveSigner(input.deviceSecret, input.signer, "deviceSecret");
|
|
10007
|
+
const issued = (input.now ?? Date.now)();
|
|
10008
|
+
const nonce2 = Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
10009
|
+
const payload = new TextEncoder().encode(
|
|
10010
|
+
JSON.stringify({
|
|
10011
|
+
v: 1,
|
|
10012
|
+
audience: "mdma:enable-ha-namespace",
|
|
10013
|
+
group_id: hex32(input.namespaceId, "namespaceId"),
|
|
10014
|
+
account_id: hex32(input.accountId, "accountId"),
|
|
10015
|
+
salt: hex32(input.salt, "salt"),
|
|
10016
|
+
subject: input.subject,
|
|
10017
|
+
nonce: nonce2,
|
|
10018
|
+
issued_at_ms: issued,
|
|
10019
|
+
expires_at_ms: issued + ttl
|
|
10020
|
+
})
|
|
10021
|
+
);
|
|
10022
|
+
const signature = await signer.sign(concat(DOMAIN, payload));
|
|
10023
|
+
return { kind: "account", credential: input.credential, signed_payload: base643(payload), signature: base643(signature) };
|
|
10024
|
+
}
|
|
10025
|
+
|
|
9843
10026
|
// src/sealed/verify.ts
|
|
9844
10027
|
var MEASUREMENT_HEX = /^[0-9a-f]{96}$/;
|
|
9845
10028
|
var REGISTERS = ["mrtd", "rtmr0", "rtmr1", "rtmr2", "rtmr3"];
|