@calimero-network/mero-js 24.6.0 → 24.7.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/account/create-context.js +17 -6
- package/dist/account/index.d.ts +1 -1
- package/dist/account/index.js +1 -1
- package/dist/account/session.d.ts +23 -1
- package/dist/account/session.js +96 -12
- package/dist/admin-api/admin-types.d.ts +2 -0
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +3 -3
- package/dist/index.cjs +73 -12
- package/dist/index.cjs.map +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +73 -12
- package/dist/index.mjs.map +3 -3
- package/dist/transport/mero-client.d.ts +9 -0
- package/dist/transport/mero-client.js +13 -5
- package/package.json +1 -1
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { AccountHaRefusedError, AccountNotLinkedError, CloudClient, } from '../cloud/index.js';
|
|
14
14
|
import { memberAddedOp, subgroupCreation, } from '../warrant/index.js';
|
|
15
15
|
import { RelayClient, } from '../relay/index.js';
|
|
16
|
-
import { attemptRelayNodeKey, knownRelays, markContextNonceSpent, readRelayMap, rememberRelay, } from './session.js';
|
|
16
|
+
import { attemptRelayNodeKey, knownRelays, markContextNonceSpent, readRelayMap, relayTransportFetch, rememberRelay, } from './session.js';
|
|
17
17
|
import { fetchRegistryBundles, selectLatestBundle } from './application-id.js';
|
|
18
18
|
/**
|
|
19
19
|
* A governance warrant's nonce, for one (relay, group): spent in a sliding
|
|
@@ -55,7 +55,11 @@ function initArgsOf(params) {
|
|
|
55
55
|
return {};
|
|
56
56
|
return JSON.parse(new TextDecoder().decode(Uint8Array.from(params)));
|
|
57
57
|
}
|
|
58
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* A relay client for these calls: the author's credential, whatever nonces the
|
|
60
|
+
* call needs, and the relay's sealed transport unless the caller brings its own
|
|
61
|
+
* `fetch` (see `relayTransportFetch`).
|
|
62
|
+
*/
|
|
59
63
|
function client(s, relayUrl, nonces, fetch) {
|
|
60
64
|
return new RelayClient({
|
|
61
65
|
relayUrl,
|
|
@@ -63,7 +67,7 @@ function client(s, relayUrl, nonces, fetch) {
|
|
|
63
67
|
authorProof: s.credential,
|
|
64
68
|
deviceSecret: s.deviceSecret,
|
|
65
69
|
nonces,
|
|
66
|
-
fetch,
|
|
70
|
+
fetch: fetch ?? relayTransportFetch(relayUrl),
|
|
67
71
|
});
|
|
68
72
|
}
|
|
69
73
|
export async function createDelegatedContext(s, req, deps = {}) {
|
|
@@ -176,14 +180,21 @@ async function foundingExecutor(s, relay, fetch) {
|
|
|
176
180
|
}
|
|
177
181
|
return { executorAccount: described.executorAccount, executorKey };
|
|
178
182
|
}
|
|
179
|
-
/**
|
|
183
|
+
/**
|
|
184
|
+
* The relay's node key, attested now; any failed attestation throws.
|
|
185
|
+
*
|
|
186
|
+
* Never the pin alone: the relay founded through becomes the namespace's first
|
|
187
|
+
* TEE and holds its keys, so it is checked against the signed release on the
|
|
188
|
+
* day it is trusted with them, not on whichever day its key was pinned. A key
|
|
189
|
+
* pinned by hand therefore no longer stands in for an attestation here.
|
|
190
|
+
*/
|
|
180
191
|
async function attestedNodeKey(relay) {
|
|
181
|
-
const key = await attemptRelayNodeKey(relay);
|
|
192
|
+
const key = await attemptRelayNodeKey(relay, { fresh: true });
|
|
182
193
|
if (key.kind === 'learned')
|
|
183
194
|
return key.nodeKey;
|
|
184
195
|
throw new Error(key.kind === 'unavailable'
|
|
185
196
|
? "the relay's signing key could not be learned: its attestation did not answer, try again"
|
|
186
|
-
: "the relay's attestation was refused, so founding through it is not possible
|
|
197
|
+
: "the relay's attestation was refused, so founding through it is not possible: it is not a signed locked-read-only release");
|
|
187
198
|
}
|
|
188
199
|
/** What an app can tell a person whose account the cloud cannot place. */
|
|
189
200
|
export const HA_ACCOUNT_NOT_LINKED_MESSAGE = 'link this account to your cloud user in the wallet so invitees can find this namespace';
|
package/dist/account/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type { RootSource } from './account.js';
|
|
|
5
5
|
export { resolveRootPair } from './account.js';
|
|
6
6
|
export type { ResolvedRoot } from './account.js';
|
|
7
7
|
export type { DelegatedCredential, DelegatedSession as DelegatedAccountSession, RelayMap, RelayNodeKeyAttempt } from './session.js';
|
|
8
|
-
export { readDelegatedSession, saveDelegatedSession, clearDelegatedSession, readDelegatedCredential, saveDelegatedCredential, clearDelegatedCredential, persistedNonces, markContextNonceSpent, pinRelayNodeKey, readPinnedRelayNodeKey, attemptRelayNodeKey, resolveRelayNodeKey, learnRelayNodeKey, RELAY_NODE_KEY_RETRY_MS, forgetMethodKinds, buildDelegatedClient, readRelayMap, rememberRelay, relayForContext, carryExecutorAccount, knownRelays, listDelegatedContexts, listDelegatedNamespaces, } from './session.js';
|
|
8
|
+
export { readDelegatedSession, saveDelegatedSession, clearDelegatedSession, readDelegatedCredential, saveDelegatedCredential, clearDelegatedCredential, persistedNonces, markContextNonceSpent, pinRelayNodeKey, readPinnedRelayNodeKey, attemptRelayNodeKey, resolveRelayNodeKey, learnRelayNodeKey, RELAY_NODE_KEY_RETRY_MS, forgetMethodKinds, forgetRelaySealing, relayTransportFetch, buildDelegatedClient, readRelayMap, rememberRelay, relayForContext, carryExecutorAccount, knownRelays, listDelegatedContexts, listDelegatedNamespaces, } from './session.js';
|
|
9
9
|
export { resolveRelayFromInvitation, normaliseAccount } from './relay-from-invitation.js';
|
|
10
10
|
export type { RelayResolutionStep, RelayResolutionFailure, ResolvedInvitationRelay, ResolveRelayResult, ResolveRelayFromInvitationInput, } from './relay-from-invitation.js';
|
|
11
11
|
export { joinWithNode } from './join-with-node.js';
|
package/dist/account/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export { generateAccountRoot, accountRootFromPhrase, accountRootFromSecret, createAccountRootSigner, accountRootSignerFromPhrase, signAccountLink, signAccountLogin, } from './account.js';
|
|
3
3
|
export { resolveRoot } from './account.js';
|
|
4
4
|
export { resolveRootPair } from './account.js';
|
|
5
|
-
export { readDelegatedSession, saveDelegatedSession, clearDelegatedSession, readDelegatedCredential, saveDelegatedCredential, clearDelegatedCredential, persistedNonces, markContextNonceSpent, pinRelayNodeKey, readPinnedRelayNodeKey, attemptRelayNodeKey, resolveRelayNodeKey, learnRelayNodeKey, RELAY_NODE_KEY_RETRY_MS, forgetMethodKinds, buildDelegatedClient, readRelayMap, rememberRelay, relayForContext, carryExecutorAccount, knownRelays, listDelegatedContexts, listDelegatedNamespaces, } from './session.js';
|
|
5
|
+
export { readDelegatedSession, saveDelegatedSession, clearDelegatedSession, readDelegatedCredential, saveDelegatedCredential, clearDelegatedCredential, persistedNonces, markContextNonceSpent, pinRelayNodeKey, readPinnedRelayNodeKey, attemptRelayNodeKey, resolveRelayNodeKey, learnRelayNodeKey, RELAY_NODE_KEY_RETRY_MS, forgetMethodKinds, forgetRelaySealing, relayTransportFetch, buildDelegatedClient, readRelayMap, rememberRelay, relayForContext, carryExecutorAccount, knownRelays, listDelegatedContexts, listDelegatedNamespaces, } from './session.js';
|
|
6
6
|
export { resolveRelayFromInvitation, normaliseAccount } from './relay-from-invitation.js';
|
|
7
7
|
export { joinWithNode } from './join-with-node.js';
|
|
8
8
|
export { bootstrapFromInvitation } from './bootstrap-from-invitation.js';
|
|
@@ -124,7 +124,21 @@ export type RelayNodeKeyAttempt = {
|
|
|
124
124
|
kind: 'refused';
|
|
125
125
|
error: unknown;
|
|
126
126
|
};
|
|
127
|
-
|
|
127
|
+
/**
|
|
128
|
+
* The relay's node key: the pinned one, or else learned from the relay's TEE
|
|
129
|
+
* attestation (see mero-js `attestRelayNodeKey`) and pinned for next time.
|
|
130
|
+
*
|
|
131
|
+
* A mock quote proves nothing about hardware, so one is accepted only from a
|
|
132
|
+
* loopback relay — a local rig. A real one must be a signed release of the
|
|
133
|
+
* trusted profile ({@link relayQuoteVerifier}).
|
|
134
|
+
*
|
|
135
|
+
* `fresh` skips the pin and attests now, for a step that hands the relay
|
|
136
|
+
* something it keeps — founding a namespace through it. A pinned key was
|
|
137
|
+
* checked once, on some earlier day, against whatever answered then.
|
|
138
|
+
*/
|
|
139
|
+
export declare function attemptRelayNodeKey(relayUrl: string, options?: {
|
|
140
|
+
fresh?: boolean;
|
|
141
|
+
}): Promise<RelayNodeKeyAttempt>;
|
|
128
142
|
/**
|
|
129
143
|
* The relay's node key: the pinned one, or else learned from the relay's TEE
|
|
130
144
|
* attestation in one attempt; `null` when that attempt did not learn it, for
|
|
@@ -151,6 +165,14 @@ export declare const RELAY_NODE_KEY_RETRY_MS: readonly number[];
|
|
|
151
165
|
export declare function learnRelayNodeKey(relayUrl: string, options?: {
|
|
152
166
|
signal?: AbortSignal;
|
|
153
167
|
}): Promise<string | null>;
|
|
168
|
+
/** Forget every relay's sealed transport. For tests. */
|
|
169
|
+
export declare function forgetRelaySealing(): void;
|
|
170
|
+
/**
|
|
171
|
+
* The `fetch` a delegated session uses for `relayUrl`: sealed to its attested
|
|
172
|
+
* TD, one per relay so its session is shared. `undefined` for a loopback relay,
|
|
173
|
+
* a dev rig answering mock quotes, which keeps the global `fetch`.
|
|
174
|
+
*/
|
|
175
|
+
export declare function relayTransportFetch(relayUrl: string): typeof fetch | undefined;
|
|
154
176
|
/** Forget what was learned about which methods read. For tests. */
|
|
155
177
|
export declare function forgetMethodKinds(): void;
|
|
156
178
|
/**
|
package/dist/account/session.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* the same application code in both.
|
|
15
15
|
*/
|
|
16
16
|
import { attestRelayNodeKey, } from '../relay-attestation/index.js';
|
|
17
|
-
import { cloudNodeReleaseUrl, createSignedReleaseVerifier, DEFAULT_RELEASE_MIRROR, fetchNodeRelease, fetchNodeReleaseVersion, } from '../sealed/index.js';
|
|
17
|
+
import { cloudNodeReleaseUrl, createAttestedSealedFetch, createSignedReleaseVerifier, DEFAULT_RELEASE_MIRROR, fetchNodeRelease, fetchNodeReleaseVersion, } from '../sealed/index.js';
|
|
18
18
|
import { createMeroClient, defaultAudience, } from '../transport/index.js';
|
|
19
19
|
import { login, } from '../login/index.js';
|
|
20
20
|
import { SseClient, } from '../events/index.js';
|
|
@@ -222,15 +222,6 @@ function isLoopback(relayUrl) {
|
|
|
222
222
|
return false;
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
|
-
/**
|
|
226
|
-
* The relay's node key: the pinned one, or else learned from the relay's TEE
|
|
227
|
-
* attestation (see mero-js `attestRelayNodeKey`) and pinned for next time.
|
|
228
|
-
*
|
|
229
|
-
* A mock quote proves nothing about hardware, so one is accepted only from a
|
|
230
|
-
* loopback relay — a local rig. A real quote needs signature and measurement
|
|
231
|
-
* verification this client does not do yet, so a real relay still needs its
|
|
232
|
-
* key pinned; `null` then, and admin reads and events stay off.
|
|
233
|
-
*/
|
|
234
225
|
/** The one relay image trusted: no shell, so nobody reads the TD. */
|
|
235
226
|
const TRUSTED_RELAY_PROFILE = 'locked-read-only';
|
|
236
227
|
/**
|
|
@@ -342,8 +333,20 @@ function relayQuoteVerifier(relayUrl, fetchRelay) {
|
|
|
342
333
|
};
|
|
343
334
|
return Object.assign(verify, { includeCollateral: true });
|
|
344
335
|
}
|
|
345
|
-
|
|
346
|
-
|
|
336
|
+
/**
|
|
337
|
+
* The relay's node key: the pinned one, or else learned from the relay's TEE
|
|
338
|
+
* attestation (see mero-js `attestRelayNodeKey`) and pinned for next time.
|
|
339
|
+
*
|
|
340
|
+
* A mock quote proves nothing about hardware, so one is accepted only from a
|
|
341
|
+
* loopback relay — a local rig. A real one must be a signed release of the
|
|
342
|
+
* trusted profile ({@link relayQuoteVerifier}).
|
|
343
|
+
*
|
|
344
|
+
* `fresh` skips the pin and attests now, for a step that hands the relay
|
|
345
|
+
* something it keeps — founding a namespace through it. A pinned key was
|
|
346
|
+
* checked once, on some earlier day, against whatever answered then.
|
|
347
|
+
*/
|
|
348
|
+
export async function attemptRelayNodeKey(relayUrl, options = {}) {
|
|
349
|
+
const pinned = options.fresh ? null : readPinnedRelayNodeKey(relayUrl);
|
|
347
350
|
if (pinned)
|
|
348
351
|
return { kind: 'learned', nodeKey: pinned };
|
|
349
352
|
const marked = { failure: null };
|
|
@@ -435,6 +438,82 @@ function jwtExpiryMs(token) {
|
|
|
435
438
|
return null;
|
|
436
439
|
}
|
|
437
440
|
}
|
|
441
|
+
// ------------------------------------------------------------------- sealing
|
|
442
|
+
//
|
|
443
|
+
// Everything a delegated session sends a hosted relay is sealed to the relay's
|
|
444
|
+
// attested TD (mero-js `createAttestedSealedFetch`), so TLS that ends at the
|
|
445
|
+
// relay's ingress, and whoever holds its certificate, reads none of it: not the
|
|
446
|
+
// login, not the token, not a query or its result, not an event.
|
|
447
|
+
//
|
|
448
|
+
// A relay whose node leaves auth to its proxy, and has no
|
|
449
|
+
// `[server.sealed] forward_auth`, cannot guard a logged-in route inside the
|
|
450
|
+
// envelope and answers `403 sealed_route_unguarded` from inside it. Only then
|
|
451
|
+
// does a request go out plain, as it always did before sealing: that answer is
|
|
452
|
+
// sealed under the session, so nobody on the way can forge it to force the
|
|
453
|
+
// downgrade. Anything else that goes wrong, an attestation refused or a relay
|
|
454
|
+
// that does not answer, fails the request; it never falls back.
|
|
455
|
+
const sealedFetches = new Map();
|
|
456
|
+
/** Forget every relay's sealed transport. For tests. */
|
|
457
|
+
export function forgetRelaySealing() {
|
|
458
|
+
sealedFetches.clear();
|
|
459
|
+
}
|
|
460
|
+
/** Whether a sealed answer is the TD saying it cannot guard this route sealed. */
|
|
461
|
+
async function isUnguardedRefusal(response) {
|
|
462
|
+
if (response.status !== 403)
|
|
463
|
+
return false;
|
|
464
|
+
try {
|
|
465
|
+
const body = (await response.clone().json());
|
|
466
|
+
return body.error?.code === 'sealed_route_unguarded';
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/** Whether `request` needs a session the relay's proxy checks: a token, or a login. */
|
|
473
|
+
function needsSession(request) {
|
|
474
|
+
return request.headers.has('authorization') || new URL(request.url).pathname.startsWith('/auth/');
|
|
475
|
+
}
|
|
476
|
+
function sealedRelayFetch(relayUrl) {
|
|
477
|
+
const plain = (input, init) => globalThis.fetch(input, init);
|
|
478
|
+
const sealed = createAttestedSealedFetch({
|
|
479
|
+
baseUrl: relayUrl,
|
|
480
|
+
verify: relayQuoteVerifier(relayUrl, plain),
|
|
481
|
+
fetch: plain,
|
|
482
|
+
});
|
|
483
|
+
// Set once the TD has said it cannot guard a logged-in call: those go plain
|
|
484
|
+
// straight away instead of paying a refused round trip each.
|
|
485
|
+
let unguarded = false;
|
|
486
|
+
return async (input, init) => {
|
|
487
|
+
const request = new Request(input, init);
|
|
488
|
+
if (unguarded && needsSession(request))
|
|
489
|
+
return plain(request);
|
|
490
|
+
const response = await sealed(request.clone());
|
|
491
|
+
if (!(await isUnguardedRefusal(response)))
|
|
492
|
+
return response;
|
|
493
|
+
if (!unguarded) {
|
|
494
|
+
unguarded = true;
|
|
495
|
+
console.warn(`[mero-js] ${relayOrigin(relayUrl)} cannot guard logged-in calls sealed (no forward_auth); ` +
|
|
496
|
+
'they go over its TLS until it does');
|
|
497
|
+
}
|
|
498
|
+
return plain(request);
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* The `fetch` a delegated session uses for `relayUrl`: sealed to its attested
|
|
503
|
+
* TD, one per relay so its session is shared. `undefined` for a loopback relay,
|
|
504
|
+
* a dev rig answering mock quotes, which keeps the global `fetch`.
|
|
505
|
+
*/
|
|
506
|
+
export function relayTransportFetch(relayUrl) {
|
|
507
|
+
if (isLoopback(relayUrl))
|
|
508
|
+
return undefined;
|
|
509
|
+
const origin = relayOrigin(relayUrl);
|
|
510
|
+
let found = sealedFetches.get(origin);
|
|
511
|
+
if (!found) {
|
|
512
|
+
found = sealedRelayFetch(relayUrl);
|
|
513
|
+
sealedFetches.set(origin, found);
|
|
514
|
+
}
|
|
515
|
+
return found;
|
|
516
|
+
}
|
|
438
517
|
/**
|
|
439
518
|
* A lazily minted `account_proof` session on the relay.
|
|
440
519
|
*
|
|
@@ -455,6 +534,8 @@ function relaySession(s, nodeKey) {
|
|
|
455
534
|
if (!inflight) {
|
|
456
535
|
inflight = login({
|
|
457
536
|
nodeUrl: s.relayUrl,
|
|
537
|
+
// Sealed: the token is the session, and the TLS ends outside the TD.
|
|
538
|
+
fetch: relayTransportFetch(s.relayUrl),
|
|
458
539
|
node: nodeKey,
|
|
459
540
|
deviceSecret: s.deviceSecret,
|
|
460
541
|
accountProof: s.credential,
|
|
@@ -610,6 +691,9 @@ _contextId) {
|
|
|
610
691
|
const nodeKey = readPinnedRelayNodeKey(s.relayUrl);
|
|
611
692
|
const client = createMeroClient({
|
|
612
693
|
transport: 'relay',
|
|
694
|
+
// Every request sealed to the relay's attested TD: intents, admin reads,
|
|
695
|
+
// queries, the login and events (see `relayTransportFetch`).
|
|
696
|
+
fetch: relayTransportFetch(s.relayUrl),
|
|
613
697
|
relay: {
|
|
614
698
|
relayUrl: s.relayUrl,
|
|
615
699
|
authorAccount: s.account,
|
|
@@ -1896,6 +1896,8 @@ export interface AdminApiClientConfig {
|
|
|
1896
1896
|
baseUrl: string;
|
|
1897
1897
|
getAuthToken?: () => Promise<string | undefined>;
|
|
1898
1898
|
timeoutMs?: number;
|
|
1899
|
+
/** Fetch every request goes through. Defaults to global `fetch`. */
|
|
1900
|
+
fetch?: typeof fetch;
|
|
1899
1901
|
}
|
|
1900
1902
|
/** A join signed by its subject, handed to an admitter to publish. */
|
|
1901
1903
|
export interface AdmitJoinRequest {
|