@calimero-network/mero-js 24.1.3 → 24.2.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.
@@ -63,10 +63,8 @@ export declare function createDelegatedPrivateContext(s: DelegatedSession, req:
63
63
  * TEE relay admits itself as its first TEE), so contexts can be created in it
64
64
  * through the same relay straight away.
65
65
  *
66
- * The relay's executor account cannot be asked about a namespace that does
67
- * not exist yet, so it is the session's `executorAccount` when the app named
68
- * one, else learned from a namespace the account already has on that relay. A
69
- * brand-new account with neither is told how to get one.
66
+ * The relay's executor account and key cannot be asked about a namespace that
67
+ * does not exist yet; see {@link foundingExecutor} for where they come from.
70
68
  */
71
69
  export declare function foundDelegatedNamespace(s: DelegatedSession, req?: {
72
70
  readonly defaultCapabilities?: number;
@@ -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 { knownRelays, markContextNonceSpent, readRelayMap, rememberRelay } from './session.js';
16
+ import { attemptRelayNodeKey, knownRelays, markContextNonceSpent, readRelayMap, 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
@@ -115,29 +115,20 @@ export async function createDelegatedPrivateContext(s, req, deps = {}) {
115
115
  * TEE relay admits itself as its first TEE), so contexts can be created in it
116
116
  * through the same relay straight away.
117
117
  *
118
- * The relay's executor account cannot be asked about a namespace that does
119
- * not exist yet, so it is the session's `executorAccount` when the app named
120
- * one, else learned from a namespace the account already has on that relay. A
121
- * brand-new account with neither is told how to get one.
118
+ * The relay's executor account and key cannot be asked about a namespace that
119
+ * does not exist yet; see {@link foundingExecutor} for where they come from.
122
120
  */
123
121
  export async function foundDelegatedNamespace(s, req = {}, deps = {}) {
124
122
  const relays = knownRelays(s).map((u) => u.replace(/\/+$/, ''));
125
123
  const relay = relays[0];
126
124
  if (!relay)
127
125
  throw new Error('no relay is known for this account, so there is nowhere to found a namespace');
128
- const known = Object.entries(readRelayMap(s.account).namespaces).find(([, url]) => url.replace(/\/+$/, '') === relay)?.[0];
129
- // The session's relay may come with its account (the cloud names it), which
130
- // is all founding needs; otherwise it is learned from a namespace on it.
131
- const named = s.executorAccount && s.relayUrl && s.relayUrl.replace(/\/+$/, '') === relay ? s.executorAccount : null;
132
- if (!named && !known) {
133
- throw new Error("the executor account of this relay is not known: join a namespace on it first, or connect with the relay's executor account (the cloud shows it beside the relay)");
134
- }
135
- const executorAccount = named ?? (await client(s, relay, governanceNonce(relay, known), deps.fetch).describeGovernance(known)).executorAccount;
126
+ const executor = await foundingExecutor(s, relay, deps.fetch);
136
127
  // The founding warrant and, with a default mask, a second one are both spent
137
128
  // in the NEW namespace's window, which is empty: any rising pair will do.
138
129
  let next = BigInt(Date.now());
139
130
  const founded = await client(s, relay, { next: async () => next++ }, deps.fetch).foundNamespace({
140
- executorAccount,
131
+ executor,
141
132
  ...(req.defaultCapabilities !== undefined ? { defaultCapabilities: req.defaultCapabilities } : {}),
142
133
  ...(req.application ? { application: req.application } : {}),
143
134
  });
@@ -165,6 +156,35 @@ export async function foundDelegatedNamespace(s, req = {}, deps = {}) {
165
156
  };
166
157
  return { namespaceId: founded.namespaceId, teeEnabled: founded.teeEnabled, ...ha };
167
158
  }
159
+ /**
160
+ * The relay's pinned or attested node key, which is the key it signs with, and
161
+ * its account: the session's (the cloud names it), else a remembered namespace's.
162
+ */
163
+ async function foundingExecutor(s, relay, fetch) {
164
+ const named = s.executorAccount && s.relayUrl?.replace(/\/+$/, '') === relay ? s.executorAccount : null;
165
+ if (named)
166
+ return { executorAccount: named, executorKey: await attestedNodeKey(relay) };
167
+ const known = Object.entries(readRelayMap(s.account).namespaces).find(([, url]) => url.replace(/\/+$/, '') === relay)?.[0];
168
+ if (!known) {
169
+ throw new Error("the executor account of this relay is not known: join a namespace on it first, or connect with the relay's executor account (the cloud shows it beside the relay)");
170
+ }
171
+ const executorKey = await attestedNodeKey(relay);
172
+ // Discovery is unauthenticated: it may name the account, never the key.
173
+ const described = await client(s, relay, governanceNonce(relay, known), fetch).describeGovernance(known);
174
+ if (described.executorKey?.toLowerCase() !== executorKey) {
175
+ throw new Error(`the relay's discovery names signing key ${described.executorKey}, not the relay's node key ${executorKey}: refusing to found through it`);
176
+ }
177
+ return { executorAccount: described.executorAccount, executorKey };
178
+ }
179
+ /** The relay's pinned or attested node key; any failed attestation throws. */
180
+ async function attestedNodeKey(relay) {
181
+ const key = await attemptRelayNodeKey(relay);
182
+ if (key.kind === 'learned')
183
+ return key.nodeKey;
184
+ throw new Error(key.kind === 'unavailable'
185
+ ? "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 (only a key from its operator, pinned with pinRelayNodeKey, can stand in)");
187
+ }
168
188
  /** What an app can tell a person whose account the cloud cannot place. */
169
189
  export const HA_ACCOUNT_NOT_LINKED_MESSAGE = 'link this account to your cloud user in the wallet so invitees can find this namespace';
170
190
  /**
@@ -47,8 +47,9 @@ export interface DelegatedSession extends DelegatedCredential {
47
47
  * The relay's executor account, hex, when the app knows it — the cloud's
48
48
  * machine page names it beside the relay's address.
49
49
  *
50
- * Founding a namespace through a relay names the relay's account in the
51
- * warrant, and a namespace that does not exist yet cannot be asked for it.
50
+ * Founding a namespace through a relay names the relay's account (and its
51
+ * node key) in the warrant, and a namespace that does not exist yet cannot be
52
+ * asked for them.
52
53
  * Without this the account learns it from a namespace it is already in, so a
53
54
  * brand-new account would have to join one first; with it, the account founds
54
55
  * on `relayUrl` directly.
@@ -231,12 +231,12 @@ export declare class AdminApiClient {
231
231
  * anything.
232
232
  *
233
233
  * The two facts {@link performIntent} needs and a client cannot derive: whose
234
- * account goes in the warrant's `executor`, and whether this node holds
235
- * `CAN_AUTHOR_ON_BEHALF` on the owning group. `canAuthorOnBehalf: false` comes
236
- * back as an answer rather than an error — it is the default state of every
237
- * context — so a caller can say "an admin of `groupId` must grant it to
238
- * `executorAccount`" instead of presenting a warrant that will be refused
239
- * after the author has spent a nonce on it.
234
+ * account and key go in the warrant's `executor` and `executor_key`, and
235
+ * whether this node holds `CAN_AUTHOR_ON_BEHALF` on the owning group.
236
+ * `canAuthorOnBehalf: false` comes back as an answer rather than an error, as
237
+ * it is the default state of every context, so a caller can say "an admin of
238
+ * `groupId` must grant it to `executorAccount`" instead of presenting a
239
+ * warrant that will be refused after the author has spent a nonce on it.
240
240
  *
241
241
  * `404` means the context belongs to no group, or this node holds no account
242
242
  * yet; either way it can be named as no warrant's executor.
@@ -563,12 +563,12 @@ export class AdminApiClient {
563
563
  * anything.
564
564
  *
565
565
  * The two facts {@link performIntent} needs and a client cannot derive: whose
566
- * account goes in the warrant's `executor`, and whether this node holds
567
- * `CAN_AUTHOR_ON_BEHALF` on the owning group. `canAuthorOnBehalf: false` comes
568
- * back as an answer rather than an error — it is the default state of every
569
- * context — so a caller can say "an admin of `groupId` must grant it to
570
- * `executorAccount`" instead of presenting a warrant that will be refused
571
- * after the author has spent a nonce on it.
566
+ * account and key go in the warrant's `executor` and `executor_key`, and
567
+ * whether this node holds `CAN_AUTHOR_ON_BEHALF` on the owning group.
568
+ * `canAuthorOnBehalf: false` comes back as an answer rather than an error, as
569
+ * it is the default state of every context, so a caller can say "an admin of
570
+ * `groupId` must grant it to `executorAccount`" instead of presenting a
571
+ * warrant that will be refused after the author has spent a nonce on it.
572
572
  *
573
573
  * `404` means the context belongs to no group, or this node holds no account
574
574
  * yet; either way it can be named as no warrant's executor.
@@ -435,9 +435,8 @@ export interface PerformIntentRequest {
435
435
  * Hex-encoded borsh of the author's `AccountProof<DeviceCert>`, proving the
436
436
  * key that signed the warrant is a device of the account it names.
437
437
  *
438
- * Only the author's own half. The node attaches its own credential, so a
439
- * caller never learns which of the node's processes runs the intent — and a
440
- * re-key on its side does not void a warrant already issued.
438
+ * Only the author's own half. The node attaches its own credential, which must
439
+ * match the `executor` and `executor_key` the warrant names.
441
440
  */
442
441
  authorProof: string;
443
442
  }
@@ -477,22 +476,22 @@ export interface PerformIntentResponseData {
477
476
  * warrant, not after being refused one.
478
477
  *
479
478
  * Every input to a warrant is something the author already holds except two:
480
- * whose name goes in `executor`, and whether that node may act here at all. Both
481
- * belong to the node, so both are answered here, on the path the intent will be
482
- * presented to.
479
+ * whose account and key go in `executor` and `executor_key`, and whether that
480
+ * node may act here at all. Both belong to the node, so both are answered here,
481
+ * on the path the intent will be presented to.
483
482
  *
484
483
  * Reading it first is what lets a client fail before signing. A warrant spends a
485
484
  * nonce from a monotonic per-device sequence, and one naming the wrong executor
486
485
  * is unspendable — the number is gone and the write never happened.
487
486
  */
488
487
  export interface IntentRelayInfo {
488
+ /** The account a warrant for this node must name as its `executor`, 64 hex. */
489
+ executorAccount: string;
489
490
  /**
490
- * The account a warrant for this node must name as its `executor`, 64 hex.
491
- *
492
- * An account, not the node's signing key: one of its processes re-keying must
493
- * not void warrants already issued to it.
491
+ * The signing key a warrant for this node must name as its `executor_key`, 64
492
+ * hex: the one device that may spend it. A re-key voids unspent warrants.
494
493
  */
495
- executorAccount: string;
494
+ executorKey: string;
496
495
  /**
497
496
  * Whether this node holds `CAN_AUTHOR_ON_BEHALF` on the group owning the
498
497
  * context.