@calimero-network/mero-js 23.2.0 → 23.4.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.
@@ -0,0 +1,56 @@
1
+ /**
2
+ * An account's ephemeral presence, through its relay.
3
+ *
4
+ * A node publishes and heartbeats its own presence; an account has no node, so
5
+ * this client signs each update with the device key, posts it to the relay's
6
+ * `presence-intents`, and resends the last state every 2.5 s until
7
+ * `set(contextId, null)` or `close()`. The relay checks the update and seals
8
+ * and gossips it; other members verify the device's signature and certificate
9
+ * themselves, so the relay cannot invent or alter an account's presence.
10
+ */
11
+ import type { SseClient } from '../events/index.js';
12
+ import type { Codec, EphemeralEntry } from '../ephemeral/types.js';
13
+ import type { RelayClient } from '../relay/relay-client.js';
14
+ type PresenceRelay = Pick<RelayClient, 'authorProof' | 'authorSigner' | 'presenceIntent'>;
15
+ export declare class RelayPresenceClient {
16
+ private readonly relay;
17
+ private readonly events;
18
+ private readonly heartbeatMs;
19
+ private readonly now;
20
+ private readonly timers;
21
+ private readonly lastSeq;
22
+ private readonly refused;
23
+ /**
24
+ * Bumped by every `set` and `close`, per context. A `set` installs its
25
+ * resends only if nothing newer happened while its first send was in flight,
26
+ * so a retract (or a newer state) can never be overtaken by an older one.
27
+ */
28
+ private readonly generation;
29
+ /**
30
+ * @param opts.events the relay's event stream, asked for only on `subscribe`:
31
+ * publishing needs none, so a client with no node key can still publish.
32
+ */
33
+ constructor(opts: {
34
+ relay: PresenceRelay;
35
+ events: () => SseClient;
36
+ heartbeatMs?: number;
37
+ now?: () => number;
38
+ });
39
+ /**
40
+ * Set this account's presence in `contextId`, or retract it with `null`.
41
+ * Throws when the relay refuses this first send for good (a 4xx other than
42
+ * 429), which also stops the resends. A rate limit, a network failure or a
43
+ * 5xx is not thrown: the next resend covers it.
44
+ */
45
+ set<T>(contextId: string, state: T | null, codec?: Codec<T>): Promise<void>;
46
+ subscribe<T>(contextId: string, handler: (entry: EphemeralEntry<T>) => void, codec?: Codec<T>): () => void;
47
+ /** Stop every resend. Entries expire on the relay and its peers within 7 s. */
48
+ close(): void;
49
+ /** Stop the context's resends and invalidate any `set` still in flight. */
50
+ private bump;
51
+ private stop;
52
+ /** Wall-clock seeded and strictly rising, so a relay restart never sees an old seq. */
53
+ private nextSeq;
54
+ private send;
55
+ }
56
+ export {};
@@ -0,0 +1,107 @@
1
+ import { hex } from '../crypto/internal.js';
2
+ import { jsonCodec, subscribePresence } from '../ephemeral/index.js';
3
+ import { presenceStatementBytes } from './statement.js';
4
+ /** The node's own heartbeat interval, so a relayed entry never outlives the 7 s TTL. */
5
+ const HEARTBEAT_MS = 2500;
6
+ export class RelayPresenceClient {
7
+ /**
8
+ * @param opts.events the relay's event stream, asked for only on `subscribe`:
9
+ * publishing needs none, so a client with no node key can still publish.
10
+ */
11
+ constructor(opts) {
12
+ this.timers = new Map();
13
+ this.lastSeq = new Map();
14
+ this.refused = new Set();
15
+ /**
16
+ * Bumped by every `set` and `close`, per context. A `set` installs its
17
+ * resends only if nothing newer happened while its first send was in flight,
18
+ * so a retract (or a newer state) can never be overtaken by an older one.
19
+ */
20
+ this.generation = new Map();
21
+ this.relay = opts.relay;
22
+ this.events = opts.events;
23
+ this.heartbeatMs = opts.heartbeatMs ?? HEARTBEAT_MS;
24
+ this.now = opts.now ?? Date.now;
25
+ }
26
+ /**
27
+ * Set this account's presence in `contextId`, or retract it with `null`.
28
+ * Throws when the relay refuses this first send for good (a 4xx other than
29
+ * 429), which also stops the resends. A rate limit, a network failure or a
30
+ * 5xx is not thrown: the next resend covers it.
31
+ */
32
+ async set(contextId, state, codec = jsonCodec()) {
33
+ const generation = this.bump(contextId);
34
+ this.refused.delete(contextId);
35
+ const bytes = state === null ? null : new Uint8Array(codec.encode(state));
36
+ await this.send(contextId, bytes);
37
+ if (bytes === null || this.refused.has(contextId))
38
+ return;
39
+ if (this.generation.get(contextId) !== generation)
40
+ return; // a newer set or close won
41
+ this.timers.set(contextId, setInterval(() => void this.send(contextId, bytes).catch(() => undefined), this.heartbeatMs));
42
+ }
43
+ subscribe(contextId, handler, codec = jsonCodec()) {
44
+ return subscribePresence(this.events(), contextId, handler, codec);
45
+ }
46
+ /** Stop every resend. Entries expire on the relay and its peers within 7 s. */
47
+ close() {
48
+ for (const contextId of new Set([...this.timers.keys(), ...this.generation.keys()])) {
49
+ this.bump(contextId);
50
+ }
51
+ }
52
+ /** Stop the context's resends and invalidate any `set` still in flight. */
53
+ bump(contextId) {
54
+ this.stop(contextId);
55
+ const next = (this.generation.get(contextId) ?? 0) + 1;
56
+ this.generation.set(contextId, next);
57
+ return next;
58
+ }
59
+ stop(contextId) {
60
+ const timer = this.timers.get(contextId);
61
+ if (timer !== undefined)
62
+ clearInterval(timer);
63
+ this.timers.delete(contextId);
64
+ }
65
+ /** Wall-clock seeded and strictly rising, so a relay restart never sees an old seq. */
66
+ nextSeq(contextId) {
67
+ const seq = Math.max(this.now(), (this.lastSeq.get(contextId) ?? 0) + 1);
68
+ this.lastSeq.set(contextId, seq);
69
+ return seq;
70
+ }
71
+ async send(contextId, state) {
72
+ if (this.refused.has(contextId))
73
+ return;
74
+ const signer = await this.relay.authorSigner();
75
+ const seq = this.nextSeq(contextId);
76
+ const sentAtMs = this.now();
77
+ const statement = await presenceStatementBytes({
78
+ contextId,
79
+ author: signer.publicKey,
80
+ seq: BigInt(seq),
81
+ sentAtMs: BigInt(sentAtMs),
82
+ state,
83
+ });
84
+ const signature = hex(await signer.sign(statement));
85
+ try {
86
+ await this.relay.presenceIntent(contextId, {
87
+ state: state === null ? null : hex(state),
88
+ seq,
89
+ sentAtMs,
90
+ signature,
91
+ authorProof: this.relay.authorProof,
92
+ });
93
+ }
94
+ catch (err) {
95
+ const status = err.status;
96
+ // Rate limited, a network failure, or the relay unable right now (5xx,
97
+ // e.g. no current key mid-rotation): the next resend covers it.
98
+ if (status === undefined || status === 0 || status === 429 || status >= 500)
99
+ return;
100
+ // Any other refusal (403: not a member or revoked; 400: malformed or a
101
+ // clock outside the relay's window) cannot be fixed by resending.
102
+ this.refused.add(contextId);
103
+ this.stop(contextId);
104
+ throw err;
105
+ }
106
+ }
107
+ }
@@ -0,0 +1,12 @@
1
+ /** core's `PRESENCE_DOMAIN`: part of the signed bytes. */
2
+ export declare const PRESENCE_DOMAIN: Uint8Array;
3
+ /** `sha256(borsh(Option<Vec<u8>>))`: `None` is `[0]`, `Some(v)` is `[1] ‖ u32le(len) ‖ v`. */
4
+ export declare function stateHash(state: Uint8Array | null): Promise<Uint8Array>;
5
+ /** What the device key signs, raw: domain ‖ context ‖ author ‖ seq ‖ sentAtMs ‖ stateHash. */
6
+ export declare function presenceStatementBytes(input: {
7
+ contextId: string;
8
+ author: string;
9
+ seq: bigint;
10
+ sentAtMs: bigint;
11
+ state: Uint8Array | null;
12
+ }): Promise<Uint8Array>;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The bytes a device signs for one presence update: the borsh of core's
3
+ * `PresenceStatement` (`calimero_node_primitives::presence`). Pinned to core's
4
+ * vector in `statement.test.ts`, so the two encoders cannot drift.
5
+ */
6
+ import { concat, fromHex, u32le, u64le } from '../crypto/internal.js';
7
+ /** core's `PRESENCE_DOMAIN`: part of the signed bytes. */
8
+ export const PRESENCE_DOMAIN = new TextEncoder().encode('calimero/presence/1');
9
+ /** `sha256(borsh(Option<Vec<u8>>))`: `None` is `[0]`, `Some(v)` is `[1] ‖ u32le(len) ‖ v`. */
10
+ export async function stateHash(state) {
11
+ const borsh = state === null ? new Uint8Array([0]) : concat(new Uint8Array([1]), u32le(state.length), state);
12
+ return new Uint8Array(await crypto.subtle.digest('SHA-256', borsh));
13
+ }
14
+ /** What the device key signs, raw: domain ‖ context ‖ author ‖ seq ‖ sentAtMs ‖ stateHash. */
15
+ export async function presenceStatementBytes(input) {
16
+ return concat(PRESENCE_DOMAIN, fromHex(input.contextId, 'contextId', 32), fromHex(input.author, 'author', 32), u64le(input.seq), u64le(input.sentAtMs), await stateHash(input.state));
17
+ }
@@ -210,6 +210,17 @@ export interface FoundNamespaceInput {
210
210
  * signed.
211
211
  */
212
212
  defaultCapabilities?: number;
213
+ /**
214
+ * The application the namespace runs, set right after founding. Without one
215
+ * a founded namespace targets no application, and no context can be created
216
+ * in it. Set through the relay with a delegated `TargetApplicationSet`, which
217
+ * core accepts only as a group's first application.
218
+ */
219
+ application?: {
220
+ applicationId: string;
221
+ package: string;
222
+ version: string;
223
+ };
213
224
  }
214
225
  /** A namespace founded through a relay. */
215
226
  export interface FoundedNamespace {
@@ -237,6 +248,14 @@ export interface FoundedNamespace {
237
248
  defaultCapabilitiesSet?: boolean;
238
249
  /** Why `defaultCapabilitiesSet` is `false`. */
239
250
  defaultCapabilitiesError?: string;
251
+ /**
252
+ * Present only when `application` was asked for: whether the namespace now
253
+ * runs it. `false` leaves it with no application; retry with
254
+ * `govern({ groupId: namespaceId, op: targetApplicationSetOp(application) })`.
255
+ */
256
+ applicationSet?: boolean;
257
+ /** Why `applicationSet` is `false`. */
258
+ applicationError?: string;
240
259
  }
241
260
  /** Where an accepted intent landed. */
242
261
  export interface IntentResult<T = unknown> {
@@ -324,6 +343,19 @@ export declare class RelayClient {
324
343
  * apply. Nothing accumulates and nothing is reusable.
325
344
  */
326
345
  execute<T = unknown>(contextId: string, method: string, argsJson?: unknown): Promise<IntentResult<T>>;
346
+ /**
347
+ * Publish one presence statement through the relay (`presence-intents`).
348
+ *
349
+ * Not retried on 429: presence resends every 2.5 s anyway, and a retry would
350
+ * only queue behind the next resend. A refusal throws with its `status`.
351
+ */
352
+ presenceIntent(contextId: string, body: {
353
+ state: string | null;
354
+ seq: number;
355
+ sentAtMs: number;
356
+ signature: string;
357
+ authorProof: string;
358
+ }): Promise<void>;
327
359
  /**
328
360
  * Ask the relay what it can do about creating contexts in `groupId`.
329
361
  *
@@ -31,7 +31,7 @@ import { signWarrant } from '../warrant/warrant.js';
31
31
  import { resolveSigner } from '../signer/signer.js';
32
32
  import { signCreationWarrant } from '../warrant/creation-warrant.js';
33
33
  import { signGovernanceWarrant } from '../warrant/governance-warrant.js';
34
- import { defaultCapabilitiesSetOp, foundedNamespaceId, namespaceCreatedOp, } from '../warrant/governance-op.js';
34
+ import { defaultCapabilitiesSetOp, targetApplicationSetOp, foundedNamespaceId, namespaceCreatedOp, } from '../warrant/governance-op.js';
35
35
  import { HTTPError } from '../http-client/web-client.js';
36
36
  import { fromHex, hex } from '../crypto/internal.js';
37
37
  /** How long a freshly minted warrant stays presentable, in seconds. */
@@ -169,6 +169,15 @@ export class RelayClient {
169
169
  const body = await this.json('POST', `/admin-api/contexts/${encodeURIComponent(contextId)}/intents`, { method, argsJson, warrant, authorProof: this.config.authorProof });
170
170
  return { rootHash: body.data.rootHash, returns: body.data.returns ?? null };
171
171
  }
172
+ /**
173
+ * Publish one presence statement through the relay (`presence-intents`).
174
+ *
175
+ * Not retried on 429: presence resends every 2.5 s anyway, and a retry would
176
+ * only queue behind the next resend. A refusal throws with its `status`.
177
+ */
178
+ async presenceIntent(contextId, body) {
179
+ await this.jsonOnce('POST', `/admin-api/contexts/${encodeURIComponent(contextId)}/presence-intents`, body);
180
+ }
172
181
  /**
173
182
  * Ask the relay what it can do about creating contexts in `groupId`.
174
183
  *
@@ -342,6 +351,7 @@ export class RelayClient {
342
351
  const capabilitiesOp = input.defaultCapabilities === undefined
343
352
  ? undefined
344
353
  : defaultCapabilitiesSetOp(input.defaultCapabilities);
354
+ const applicationOp = input.application ? targetApplicationSetOp(input.application) : undefined;
345
355
  // Resolved before the nonce, so a misconfigured key burns none.
346
356
  const signer = await this.authorSigner();
347
357
  const nonce = await this.config.nonces.next();
@@ -362,23 +372,32 @@ export class RelayClient {
362
372
  teeEnabled: data.teeEnabled ?? false,
363
373
  ...(data.teeError ? { teeError: data.teeError } : {}),
364
374
  };
365
- if (!capabilitiesOp)
366
- return founded;
367
- // A second op, under its own warrant, on the namespace root: the founder is
368
- // its admin and the relay was seated to act for members, so it goes through
369
- // the ordinary `govern` path. The namespace exists whatever happens here, so
370
- // a failure is reported rather than thrown.
371
- try {
372
- await this.govern({ groupId: founded.namespaceId, op: capabilitiesOp });
373
- return { ...founded, defaultCapabilitiesSet: true };
375
+ // Follow-up ops, each under its own warrant, on the namespace root: the
376
+ // founder is its admin and the relay was seated to act for members, so they
377
+ // go through the ordinary `govern` path. The namespace exists whatever
378
+ // happens here, so a failure is reported rather than thrown. The
379
+ // application first: without it the namespace can hold no context at all.
380
+ let result = founded;
381
+ const message = (err) => (err instanceof Error ? err.message : String(err));
382
+ if (applicationOp) {
383
+ try {
384
+ await this.govern({ groupId: founded.namespaceId, op: applicationOp });
385
+ result = { ...result, applicationSet: true };
386
+ }
387
+ catch (err) {
388
+ result = { ...result, applicationSet: false, applicationError: message(err) };
389
+ }
374
390
  }
375
- catch (err) {
376
- return {
377
- ...founded,
378
- defaultCapabilitiesSet: false,
379
- defaultCapabilitiesError: err instanceof Error ? err.message : String(err),
380
- };
391
+ if (capabilitiesOp) {
392
+ try {
393
+ await this.govern({ groupId: founded.namespaceId, op: capabilitiesOp });
394
+ result = { ...result, defaultCapabilitiesSet: true };
395
+ }
396
+ catch (err) {
397
+ result = { ...result, defaultCapabilitiesSet: false, defaultCapabilitiesError: message(err) };
398
+ }
381
399
  }
400
+ return result;
382
401
  }
383
402
  async postGovernance(groupId, op, warrant) {
384
403
  const body = await this.json('POST', `/admin-api/groups/${encodeURIComponent(groupId)}/governance-intents`, {
@@ -466,6 +485,9 @@ export class RelayClient {
466
485
  }
467
486
  throw new HTTPError(response.status, response.statusText, `${this.baseUrl}${path}`, response.headers, text);
468
487
  }
488
+ // `presence-intents` answers an accepted update with no body.
489
+ if (response.status === 204)
490
+ return undefined;
469
491
  return (await response.json());
470
492
  }
471
493
  }
@@ -59,6 +59,7 @@ import { SseClient } from '../events/sse.js';
59
59
  import type { AuthApiClient } from '../auth-api/index.js';
60
60
  import type { WsClient } from '../events/ws.js';
61
61
  import type { EphemeralClient } from '../ephemeral/index.js';
62
+ import { RelayPresenceClient } from '../presence/relay-presence.js';
62
63
  import type { CloudClient } from '../cloud/cloud-client.js';
63
64
  /** The node transport: everything `createMeroJs` takes, plus an explicit opt-in. */
64
65
  export interface NodeTransportConfig extends MeroJsConfig {
@@ -155,6 +156,7 @@ export declare class MeroClient {
155
156
  private readonly proofAuthorizer;
156
157
  private readonly relayUrl;
157
158
  private proofEvents;
159
+ private relayPresence;
158
160
  constructor(config: MeroClientConfig);
159
161
  /**
160
162
  * The write surface. Identical on both transports — this is the whole point.
@@ -234,19 +236,16 @@ export declare class MeroClient {
234
236
  /**
235
237
  * Ephemeral presence. Node transport only.
236
238
  *
237
- * Not extended to the relay even though a logged-in session could reach both
238
- * halves of it mechanically. Publishing resolves the author from an *owned
239
- * context identity on the node*, and whether a delegated session has one has
240
- * not been established — a presence client that silently published nothing,
241
- * or published as the wrong identity, is worse than one that says it is not
242
- * available. The read half is not lost: presence arrives on `client.events`
243
- * like any other context event, and can be filtered there.
244
- *
245
- * The rejected alternative was to expose it over the relay session and let
246
- * `set` fail at the node. That trades a clear construction-time answer for a
247
- * runtime failure inside a UI that has already rendered.
239
+ * On a relay client this is an account's presence client
240
+ * ({@link RelayPresenceClient}): `set` signs each update with the device key
241
+ * and posts it to the relay's `presence-intents`, resending every 2.5 s, and
242
+ * `subscribe` reads the relay's event stream. The relay checks and forwards
243
+ * the update; other members verify the device's signature and certificate
244
+ * themselves, so it can drop or delay an account's presence but not invent or
245
+ * alter it. Only `subscribe` needs the relay's node key (for the event
246
+ * stream), so publishing works on any relay client.
248
247
  */
249
- get ephemeral(): EphemeralClient;
248
+ get ephemeral(): EphemeralClient | RelayPresenceClient;
250
249
  /**
251
250
  * Calimero Cloud.
252
251
  *
@@ -55,6 +55,7 @@ import { createAdminApiClient, createAdminApiClientFromHttpClient } from '../adm
55
55
  import { createHttpClient } from '../http-client/index.js';
56
56
  import { createProofAuthorizer } from '../request-proof/index.js';
57
57
  import { SseClient } from '../events/sse.js';
58
+ import { RelayPresenceClient } from '../presence/relay-presence.js';
58
59
  /** Message shared by every node-only accessor, so the reason is stated once. */
59
60
  function relayHasNo(surface, why) {
60
61
  return new Error(`this client was constructed with the relay transport, which has no ${surface}: ${why}. ` +
@@ -127,6 +128,7 @@ export class MeroClient {
127
128
  this.proofAuthorizer = null;
128
129
  this.relayUrl = null;
129
130
  this.proofEvents = null;
131
+ this.relayPresence = null;
130
132
  if (config.transport === 'relay') {
131
133
  this.transport = 'relay';
132
134
  this.nodeClient = null;
@@ -308,23 +310,20 @@ export class MeroClient {
308
310
  /**
309
311
  * Ephemeral presence. Node transport only.
310
312
  *
311
- * Not extended to the relay even though a logged-in session could reach both
312
- * halves of it mechanically. Publishing resolves the author from an *owned
313
- * context identity on the node*, and whether a delegated session has one has
314
- * not been established — a presence client that silently published nothing,
315
- * or published as the wrong identity, is worse than one that says it is not
316
- * available. The read half is not lost: presence arrives on `client.events`
317
- * like any other context event, and can be filtered there.
318
- *
319
- * The rejected alternative was to expose it over the relay session and let
320
- * `set` fail at the node. That trades a clear construction-time answer for a
321
- * runtime failure inside a UI that has already rendered.
313
+ * On a relay client this is an account's presence client
314
+ * ({@link RelayPresenceClient}): `set` signs each update with the device key
315
+ * and posts it to the relay's `presence-intents`, resending every 2.5 s, and
316
+ * `subscribe` reads the relay's event stream. The relay checks and forwards
317
+ * the update; other members verify the device's signature and certificate
318
+ * themselves, so it can drop or delay an account's presence but not invent or
319
+ * alter it. Only `subscribe` needs the relay's node key (for the event
320
+ * stream), so publishing works on any relay client.
322
321
  */
323
322
  get ephemeral() {
324
- if (!this.nodeClient) {
325
- throw relayHasNo('ephemeral presence', 'publishing resolves the author from an owned context identity on the node, which a delegated session is not known to have — read presence off `client.events` instead');
326
- }
327
- return this.nodeClient.ephemeral;
323
+ if (this.nodeClient)
324
+ return this.nodeClient.ephemeral;
325
+ this.relayPresence ?? (this.relayPresence = new RelayPresenceClient({ relay: this.relay, events: () => this.events }));
326
+ return this.relayPresence;
328
327
  }
329
328
  /**
330
329
  * Calimero Cloud.
@@ -64,13 +64,22 @@ export declare function memberRoleSetOp(member: string, memberRole: GovernanceMe
64
64
  */
65
65
  export declare function createdSubgroupId(admin: string, parentId: string, restricted: boolean, salt: string): Promise<string>;
66
66
  /**
67
- * Set the group's default capability mask (`MemberCapabilities` bits, a `u32`),
68
- * the capabilities every member holds without a per-member grant. Needs admin.
67
+ * Choose the application a group runs (`GroupOp::TargetApplicationSet`), in
68
+ * its delegable form: `bytecode_id` is left for the relay to fill (32 zero
69
+ * bytes), resolved from `package@version` in the registry. The member signs
70
+ * the application id, package and version, so the relay can pick no other
71
+ * build; the application id binds package and signer, and every node checks
72
+ * the bundle's signature.
69
73
  *
70
- * The relay refuses any change to `CAN_AUTHOR_ON_BEHALF` (bit 9) through this
71
- * path, and no namespace starts with it in its default mask, so a mask that
72
- * sets it is refused here rather than burning a nonce on a certain 403.
74
+ * Through a relay this is accepted only as a group's FIRST application, on a
75
+ * group that targets none yet (a namespace an account founded). Moving an
76
+ * existing target is an upgrade, which never goes through a relay.
73
77
  */
78
+ export declare function targetApplicationSetOp(input: {
79
+ applicationId: string;
80
+ package: string;
81
+ version: string;
82
+ }): GovernanceOp;
74
83
  export declare function defaultCapabilitiesSetOp(capabilities: number): GovernanceOp;
75
84
  /** What a subgroup is created with. */
76
85
  export interface GroupCreatedInput {
@@ -38,6 +38,7 @@ const GROUP_OP = {
38
38
  MemberRoleSet: 4,
39
39
  MemberCapabilitySet: 5,
40
40
  DefaultCapabilitiesSet: 6,
41
+ TargetApplicationSet: 7,
41
42
  ContextDetached: 9,
42
43
  SubgroupVisibilitySet: 10,
43
44
  GroupMetadataSet: 11,
@@ -137,6 +138,31 @@ export async function createdSubgroupId(admin, parentId, restricted, salt) {
137
138
  * path, and no namespace starts with it in its default mask, so a mask that
138
139
  * sets it is refused here rather than burning a nonce on a certain 403.
139
140
  */
141
+ /** A borsh `String`: a u32 byte length, then the UTF-8 bytes. */
142
+ function borshString(value, what) {
143
+ if (value.length === 0)
144
+ throw new Error(`${what} must not be empty`);
145
+ const bytes = new TextEncoder().encode(value);
146
+ return concat(u32le(bytes.length), bytes);
147
+ }
148
+ /**
149
+ * Choose the application a group runs (`GroupOp::TargetApplicationSet`), in
150
+ * its delegable form: `bytecode_id` is left for the relay to fill (32 zero
151
+ * bytes), resolved from `package@version` in the registry. The member signs
152
+ * the application id, package and version, so the relay can pick no other
153
+ * build; the application id binds package and signer, and every node checks
154
+ * the bundle's signature.
155
+ *
156
+ * Through a relay this is accepted only as a group's FIRST application, on a
157
+ * group that targets none yet (a namespace an account founded). Moving an
158
+ * existing target is an upgrade, which never goes through a relay.
159
+ */
160
+ export function targetApplicationSetOp(input) {
161
+ return {
162
+ kind: 'group',
163
+ bytes: concat(tag(GROUP_OP.TargetApplicationSet), CLEARED_HASH, fromHex(input.applicationId, 'applicationId', 32), borshString(input.package, 'package'), borshString(input.version, 'version')),
164
+ };
165
+ }
140
166
  export function defaultCapabilitiesSetOp(capabilities) {
141
167
  if (!Number.isInteger(capabilities) || capabilities < 0 || capabilities > 4294967295) {
142
168
  throw new Error(`capabilities must be a u32 bit mask, got ${String(capabilities)}`);
@@ -4,5 +4,5 @@ export { signCreationWarrant, parseCreationWarrant, creationInitHash, } from './
4
4
  export type { CreationWarrantInput, CreationWarrantFields, SignedCreationWarrant, } from './creation-warrant.js';
5
5
  export { signGovernanceWarrant, parseGovernanceWarrant, governanceOpHash, } from './governance-warrant.js';
6
6
  export type { GovernanceWarrantInput, GovernanceWarrantFields, } from './governance-warrant.js';
7
- export { memberAddedOp, memberRemovedOp, memberLeftOp, memberRoleSetOp, groupCreatedOp, createdSubgroupId, subgroupCreation, groupReparentedOp, groupDeletedOp, memberCapabilitySetOp, contextDetachedOp, subgroupVisibilitySetOp, groupMetadataSetOp, memberMetadataSetOp, contextMetadataSetOp, contextCapabilityGrantedOp, contextCapabilityRevokedOp, memberJoinedOpenOp, namespaceCreatedOp, defaultCapabilitiesSetOp, foundedNamespaceId, } from './governance-op.js';
7
+ export { memberAddedOp, memberRemovedOp, memberLeftOp, memberRoleSetOp, groupCreatedOp, createdSubgroupId, subgroupCreation, groupReparentedOp, groupDeletedOp, memberCapabilitySetOp, contextDetachedOp, subgroupVisibilitySetOp, groupMetadataSetOp, memberMetadataSetOp, contextMetadataSetOp, contextCapabilityGrantedOp, contextCapabilityRevokedOp, memberJoinedOpenOp, namespaceCreatedOp, defaultCapabilitiesSetOp, targetApplicationSetOp, foundedNamespaceId, } from './governance-op.js';
8
8
  export type { GovernanceOp, GovernanceOpKind, GovernanceMemberRole, GroupCreatedInput, SubgroupCreation, NamespaceCreatedInput, } from './governance-op.js';
@@ -2,4 +2,4 @@
2
2
  export { signWarrant, intentHash } from './warrant.js';
3
3
  export { signCreationWarrant, parseCreationWarrant, creationInitHash, } from './creation-warrant.js';
4
4
  export { signGovernanceWarrant, parseGovernanceWarrant, governanceOpHash, } from './governance-warrant.js';
5
- export { memberAddedOp, memberRemovedOp, memberLeftOp, memberRoleSetOp, groupCreatedOp, createdSubgroupId, subgroupCreation, groupReparentedOp, groupDeletedOp, memberCapabilitySetOp, contextDetachedOp, subgroupVisibilitySetOp, groupMetadataSetOp, memberMetadataSetOp, contextMetadataSetOp, contextCapabilityGrantedOp, contextCapabilityRevokedOp, memberJoinedOpenOp, namespaceCreatedOp, defaultCapabilitiesSetOp, foundedNamespaceId, } from './governance-op.js';
5
+ export { memberAddedOp, memberRemovedOp, memberLeftOp, memberRoleSetOp, groupCreatedOp, createdSubgroupId, subgroupCreation, groupReparentedOp, groupDeletedOp, memberCapabilitySetOp, contextDetachedOp, subgroupVisibilitySetOp, groupMetadataSetOp, memberMetadataSetOp, contextMetadataSetOp, contextCapabilityGrantedOp, contextCapabilityRevokedOp, memberJoinedOpenOp, namespaceCreatedOp, defaultCapabilitiesSetOp, targetApplicationSetOp, foundedNamespaceId, } from './governance-op.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calimero-network/mero-js",
3
- "version": "23.2.0",
3
+ "version": "23.4.0",
4
4
  "description": "Pure JavaScript SDK for Calimero",
5
5
  "type": "module",
6
6
  "exports": {