@calimero-network/mero-js 22.6.0 → 22.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.
@@ -1,4 +1,4 @@
1
1
  export { RelayClient, IntentRefusedError } from './relay-client.js';
2
- export type { RelayClientConfig, RelayDescription, IntentResult, CreationDescription, CreateContextInput, CreatedContext, GovernanceDescription, GovernInput, GovernResult, } from './relay-client.js';
2
+ export type { RelayClientConfig, RelayDescription, IntentResult, CreationDescription, CreateContextInput, CreatedContext, GovernanceDescription, GovernInput, GovernResult, FoundNamespaceInput, FoundedNamespace, } from './relay-client.js';
3
3
  export { createMemoryNonceSource, createLocalStorageNonceSource } from './nonce-source.js';
4
4
  export type { NonceSource } from './nonce-source.js';
@@ -27,7 +27,7 @@
27
27
  * monotonic per-device sequence, and one minted against the wrong executor is
28
28
  * unspendable: the number is gone and the write never happened.
29
29
  */
30
- import type { GovernanceOp } from '../warrant/governance-op.js';
30
+ import { type GovernanceOp } from '../warrant/governance-op.js';
31
31
  import { type Signer } from '../signer/signer.js';
32
32
  import type { NonceSource } from './nonce-source.js';
33
33
  /** Where and as whom to write, plus the key that consents. */
@@ -169,6 +169,61 @@ export interface GovernResult {
169
169
  /** The group the op acted on, hex: the new subgroup, for a creation. */
170
170
  groupId: string;
171
171
  }
172
+ /** What {@link RelayClient.foundNamespace} founds with. */
173
+ export interface FoundNamespaceInput {
174
+ /**
175
+ * The salt the namespace id is derived from, hex (32 bytes). Random when
176
+ * absent, which is what almost every caller wants: one account founds many
177
+ * namespaces by varying it.
178
+ */
179
+ salt?: string;
180
+ /**
181
+ * The relay's account, hex: the warrant's `executor`. The namespace does not
182
+ * exist yet, so there is nothing to ask `describeGovernance` about. Falls back
183
+ * to the client's configured or already-learned `executorAccount`.
184
+ */
185
+ executorAccount?: string;
186
+ /**
187
+ * The namespace root's default capability mask (`MemberCapabilities` bits),
188
+ * set right after founding. A namespace is founded with core's minimal
189
+ * default (`CAN_JOIN_OPEN_SUBGROUPS` only), so an app that wants members to
190
+ * create contexts or invite, for example, names its own mask here.
191
+ *
192
+ * Set through the relay with a delegated `DefaultCapabilitiesSet`, applied as
193
+ * the founder, who is the namespace's admin. It cannot include
194
+ * `CAN_AUTHOR_ON_BEHALF` (bit 9): which relays may write for members is never
195
+ * decided through a relay, and such a mask is refused before anything is
196
+ * signed.
197
+ */
198
+ defaultCapabilities?: number;
199
+ }
200
+ /** A namespace founded through a relay. */
201
+ export interface FoundedNamespace {
202
+ /** The new namespace's id, hex: `foundedNamespaceId(author, salt)`. */
203
+ namespaceId: string;
204
+ /**
205
+ * The salt it was derived with, hex. Keep it if anyone will need to be shown
206
+ * who founded the namespace: `(founder, salt)` reproduces the id.
207
+ */
208
+ salt: string;
209
+ /**
210
+ * Whether the relay admitted itself as the namespace's first TEE, which also
211
+ * sets its default relay-mode admission policy. `false` from a relay that is
212
+ * not a TEE, or whose attestation failed (see `teeError`). The namespace is
213
+ * founded either way.
214
+ */
215
+ teeEnabled: boolean;
216
+ /** Why `teeEnabled` is `false`, when the relay tried and failed. */
217
+ teeError?: string;
218
+ /**
219
+ * Present only when `defaultCapabilities` was asked for: whether the mask was
220
+ * set. `false` leaves the namespace founded with core's default; retry with
221
+ * `govern({ groupId: namespaceId, op: defaultCapabilitiesSetOp(mask) })`.
222
+ */
223
+ defaultCapabilitiesSet?: boolean;
224
+ /** Why `defaultCapabilitiesSet` is `false`. */
225
+ defaultCapabilitiesError?: string;
226
+ }
172
227
  /** Where an accepted intent landed. */
173
228
  export interface IntentResult<T = unknown> {
174
229
  /** The context's scope root after the run — did this change anything? */
@@ -284,6 +339,39 @@ export declare class RelayClient {
284
339
  * other warrant.
285
340
  */
286
341
  govern(input: GovernInput): Promise<GovernResult>;
342
+ /**
343
+ * Found a namespace through the relay, with the author as its founder, owner
344
+ * and admin. For an account with no node, this is how it gets a namespace at
345
+ * all.
346
+ *
347
+ * The author signs the exact genesis (`NamespaceCreatedV2`, carrying the
348
+ * author's own credential and the salt) under a root-plane governance
349
+ * warrant scoped to the new namespace's id, which is derived from the
350
+ * author's account and the salt, so the relay can neither choose another id
351
+ * nor found it for anyone else. The relay takes an identity in the new
352
+ * namespace, mints its key, publishes the genesis, and is seated as the
353
+ * founding relay (a `Member` holding `CAN_AUTHOR_ON_BEHALF`), so later
354
+ * `govern`, `createContext` and `execute` calls in the namespace work
355
+ * through it straight away. A TEE relay then admits itself as the
356
+ * namespace's first TEE; `teeEnabled` reports whether it did.
357
+ *
358
+ * The executor cannot be learned from the relay first: the namespace does not
359
+ * exist, so `describeGovernance` has nothing to answer about. It comes from
360
+ * `input.executorAccount`, the configured one, or one an earlier call learned
361
+ * (`describe`, `createContext`, `govern`), and without any of them this
362
+ * throws before taking a nonce.
363
+ * The nonce is spent in the new namespace's ledger.
364
+ *
365
+ * With `defaultCapabilities`, the founder then sets the namespace's default
366
+ * capability mask through the same relay (a second warrant, a second nonce)
367
+ * and `defaultCapabilitiesSet` reports whether it landed. A failure there
368
+ * does not throw, since the namespace is already founded.
369
+ *
370
+ * Refusals (`400`/`403`) come back as {@link IntentRefusedError}; a salt
371
+ * that names an existing namespace is a `409`, an `HTTPError`.
372
+ */
373
+ foundNamespace(input?: FoundNamespaceInput): Promise<FoundedNamespace>;
374
+ private postGovernance;
287
375
  /**
288
376
  * The executor a warrant must name: the one the relay answered with, which a
289
377
  * configured account must agree with. Remembered for later calls.
@@ -30,8 +30,9 @@
30
30
  import { signWarrant } from '../warrant/warrant.js';
31
31
  import { signCreationWarrant } from '../warrant/creation-warrant.js';
32
32
  import { signGovernanceWarrant } from '../warrant/governance-warrant.js';
33
+ import { defaultCapabilitiesSetOp, foundedNamespaceId, namespaceCreatedOp, } from '../warrant/governance-op.js';
33
34
  import { HTTPError } from '../http-client/web-client.js';
34
- import { hex } from '../crypto/internal.js';
35
+ import { fromHex, hex } from '../crypto/internal.js';
35
36
  import { resolveSigner } from '../signer/signer.js';
36
37
  /** How long a freshly minted warrant stays presentable, in seconds. */
37
38
  const DEFAULT_TTL_SECONDS = 300;
@@ -237,8 +238,107 @@ export class RelayClient {
237
238
  notAfter: BigInt(Math.floor(Date.now() / 1000)) + BigInt(ttl),
238
239
  signer,
239
240
  });
240
- const body = await this.json('POST', `/admin-api/groups/${encodeURIComponent(input.groupId)}/governance-intents`, { warrant, authorProof: this.config.authorProof, op: hex(input.op.bytes) });
241
- return { groupId: body.data.groupId };
241
+ const data = await this.postGovernance(input.groupId, input.op, warrant);
242
+ return { groupId: data.groupId };
243
+ }
244
+ /**
245
+ * Found a namespace through the relay, with the author as its founder, owner
246
+ * and admin. For an account with no node, this is how it gets a namespace at
247
+ * all.
248
+ *
249
+ * The author signs the exact genesis (`NamespaceCreatedV2`, carrying the
250
+ * author's own credential and the salt) under a root-plane governance
251
+ * warrant scoped to the new namespace's id, which is derived from the
252
+ * author's account and the salt, so the relay can neither choose another id
253
+ * nor found it for anyone else. The relay takes an identity in the new
254
+ * namespace, mints its key, publishes the genesis, and is seated as the
255
+ * founding relay (a `Member` holding `CAN_AUTHOR_ON_BEHALF`), so later
256
+ * `govern`, `createContext` and `execute` calls in the namespace work
257
+ * through it straight away. A TEE relay then admits itself as the
258
+ * namespace's first TEE; `teeEnabled` reports whether it did.
259
+ *
260
+ * The executor cannot be learned from the relay first: the namespace does not
261
+ * exist, so `describeGovernance` has nothing to answer about. It comes from
262
+ * `input.executorAccount`, the configured one, or one an earlier call learned
263
+ * (`describe`, `createContext`, `govern`), and without any of them this
264
+ * throws before taking a nonce.
265
+ * The nonce is spent in the new namespace's ledger.
266
+ *
267
+ * With `defaultCapabilities`, the founder then sets the namespace's default
268
+ * capability mask through the same relay (a second warrant, a second nonce)
269
+ * and `defaultCapabilitiesSet` reports whether it landed. A failure there
270
+ * does not throw, since the namespace is already founded.
271
+ *
272
+ * Refusals (`400`/`403`) come back as {@link IntentRefusedError}; a salt
273
+ * that names an existing namespace is a `409`, an `HTTPError`.
274
+ */
275
+ async foundNamespace(input = {}) {
276
+ const executor = input.executorAccount
277
+ ? this.checkedExecutor(input.executorAccount)
278
+ : this.executorAccount;
279
+ if (!executor) {
280
+ throw new Error("foundNamespace needs the relay's account: pass executorAccount or configure it " +
281
+ '(checked before signing, no nonce was spent)');
282
+ }
283
+ const salt = input.salt
284
+ ? hex(fromHex(input.salt, 'salt', 32))
285
+ : hex(crypto.getRandomValues(new Uint8Array(32)));
286
+ const namespaceId = await foundedNamespaceId(this.config.authorAccount, salt);
287
+ const op = namespaceCreatedOp({
288
+ founder: this.config.authorAccount,
289
+ credential: this.config.authorProof,
290
+ salt,
291
+ });
292
+ // Encoded (and so validated) before anything is signed: a mask the relay
293
+ // would refuse must not cost the founding a nonce.
294
+ const capabilitiesOp = input.defaultCapabilities === undefined
295
+ ? undefined
296
+ : defaultCapabilitiesSetOp(input.defaultCapabilities);
297
+ // Resolved before the nonce, so a misconfigured key burns none.
298
+ const signer = await this.authorSigner();
299
+ const nonce = await this.config.nonces.next();
300
+ const ttl = this.config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
301
+ const warrant = await signGovernanceWarrant({
302
+ scope: namespaceId,
303
+ op,
304
+ authorAccount: this.config.authorAccount,
305
+ executor,
306
+ nonce,
307
+ notAfter: BigInt(Math.floor(Date.now() / 1000)) + BigInt(ttl),
308
+ signer,
309
+ });
310
+ const data = await this.postGovernance(namespaceId, op, warrant);
311
+ const founded = {
312
+ namespaceId: data.groupId,
313
+ salt,
314
+ teeEnabled: data.teeEnabled ?? false,
315
+ ...(data.teeError ? { teeError: data.teeError } : {}),
316
+ };
317
+ if (!capabilitiesOp)
318
+ return founded;
319
+ // A second op, under its own warrant, on the namespace root: the founder is
320
+ // its admin and the relay was seated to act for members, so it goes through
321
+ // the ordinary `govern` path. The namespace exists whatever happens here, so
322
+ // a failure is reported rather than thrown.
323
+ try {
324
+ await this.govern({ groupId: founded.namespaceId, op: capabilitiesOp });
325
+ return { ...founded, defaultCapabilitiesSet: true };
326
+ }
327
+ catch (err) {
328
+ return {
329
+ ...founded,
330
+ defaultCapabilitiesSet: false,
331
+ defaultCapabilitiesError: err instanceof Error ? err.message : String(err),
332
+ };
333
+ }
334
+ }
335
+ async postGovernance(groupId, op, warrant) {
336
+ const body = await this.json('POST', `/admin-api/groups/${encodeURIComponent(groupId)}/governance-intents`, {
337
+ warrant,
338
+ authorProof: this.config.authorProof,
339
+ op: hex(op.bytes),
340
+ });
341
+ return body.data;
242
342
  }
243
343
  /**
244
344
  * The executor a warrant must name: the one the relay answered with, which a
@@ -15,7 +15,8 @@
15
15
  *
16
16
  * Sources in core, for the next person to check a discriminant:
17
17
  * `crates/governance-types/src/lib.rs` (`GroupOp`, `RootOp`),
18
- * `crates/primitives/src/context.rs` (`GroupMemberRole`), and the vectors in
18
+ * `crates/primitives/src/context.rs` (`GroupMemberRole`),
19
+ * `crates/account/src/namespace_id.rs` (`founded_namespace_id`), and the vectors in
19
20
  * `crates/governance-types/src/tests.rs` (`delegated_governance_op_vectors_are_stable`).
20
21
  */
21
22
  /**
@@ -51,6 +52,15 @@ export declare function memberRemovedOp(member: string): GovernanceOp;
51
52
  export declare function memberLeftOp(member: string): GovernanceOp;
52
53
  /** Set `member`'s role (an account, hex). */
53
54
  export declare function memberRoleSetOp(member: string, memberRole: GovernanceMemberRole): GovernanceOp;
55
+ /**
56
+ * Set the group's default capability mask (`MemberCapabilities` bits, a `u32`),
57
+ * the capabilities every member holds without a per-member grant. Needs admin.
58
+ *
59
+ * The relay refuses any change to `CAN_AUTHOR_ON_BEHALF` (bit 9) through this
60
+ * path, and no namespace starts with it in its default mask, so a mask that
61
+ * sets it is refused here rather than burning a nonce on a certain 403.
62
+ */
63
+ export declare function defaultCapabilitiesSetOp(capabilities: number): GovernanceOp;
54
64
  /** What a subgroup is created with. */
55
65
  export interface GroupCreatedInput {
56
66
  /** The new subgroup's id, hex (32 bytes). Choose it fresh; a taken id is refused. */
@@ -79,3 +89,39 @@ export declare function groupReparentedOp(childGroupId: string, newParentId: str
79
89
  * empty, for the relay to enumerate.
80
90
  */
81
91
  export declare function groupDeletedOp(rootGroupId: string): GovernanceOp;
92
+ /**
93
+ * The id of the namespace `founder` founds with `salt`: core's
94
+ * `founded_namespace_id`, `domain_hash("calimero.namespace.id.v1", [founder, salt])`.
95
+ *
96
+ * Anyone holding the pair can recompute it, and only the founder's account
97
+ * hashes to it, which is why a relay founding a namespace for a member cannot
98
+ * choose the id: the member computes it and signs it.
99
+ */
100
+ export declare function foundedNamespaceId(founder: string, salt: string): Promise<string>;
101
+ /** What a namespace is founded with. */
102
+ export interface NamespaceCreatedInput {
103
+ /** The founding account, hex: the **author's**. It becomes founder, owner and admin. */
104
+ founder: string;
105
+ /**
106
+ * The founder's `AccountProof<DeviceCert>`, hex: the credential the author
107
+ * already presents as `authorProof`. Its account must be `founder` and its
108
+ * device key the one that signs the warrant; the genesis apply checks both
109
+ * and binds the founder's device from it.
110
+ */
111
+ credential: string;
112
+ /**
113
+ * The salt the namespace id is derived with, hex (32 bytes). Choose it at
114
+ * random; {@link foundedNamespaceId} gives the id it yields.
115
+ */
116
+ salt: string;
117
+ }
118
+ /**
119
+ * Found a namespace: core's `RootOp::NamespaceCreatedV2` genesis, a root op
120
+ * published on the new namespace's own log.
121
+ *
122
+ * Its delegable form is the op itself, nothing cleared, so the author signs the
123
+ * exact genesis. Borsh is `[9] || founder || credential || salt`: the credential
124
+ * is a nested struct, not a length-prefixed byte string, so it is written
125
+ * inline. The warrant's scope must be {@link foundedNamespaceId}`(founder, salt)`.
126
+ */
127
+ export declare function namespaceCreatedOp(input: NamespaceCreatedInput): GovernanceOp;
@@ -15,10 +15,12 @@
15
15
  *
16
16
  * Sources in core, for the next person to check a discriminant:
17
17
  * `crates/governance-types/src/lib.rs` (`GroupOp`, `RootOp`),
18
- * `crates/primitives/src/context.rs` (`GroupMemberRole`), and the vectors in
18
+ * `crates/primitives/src/context.rs` (`GroupMemberRole`),
19
+ * `crates/account/src/namespace_id.rs` (`founded_namespace_id`), and the vectors in
19
20
  * `crates/governance-types/src/tests.rs` (`delegated_governance_op_vectors_are_stable`).
20
21
  */
21
- import { concat, fromHex, u32le } from '../crypto/internal.js';
22
+ import { CAPABILITIES, hasCap } from '../capabilities.js';
23
+ import { concat, domainHash, fromHex, fromHexUnsized, hex, u32le } from '../crypto/internal.js';
22
24
  /**
23
25
  * `GroupMemberRole` discriminants. The two TEE roles (3 and 4) are minted only
24
26
  * by attestation admission and never by a member's op, so they are not offered.
@@ -34,13 +36,17 @@ const GROUP_OP = {
34
36
  MemberRemoved: 2,
35
37
  MemberLeft: 3,
36
38
  MemberRoleSet: 4,
39
+ DefaultCapabilitiesSet: 6,
37
40
  };
38
41
  /** `RootOp` discriminants, by position in core's enum. */
39
42
  const ROOT_OP = {
40
43
  GroupCreated: 0,
41
44
  GroupReparented: 1,
42
45
  GroupDeleted: 2,
46
+ NamespaceCreatedV2: 9,
43
47
  };
48
+ /** core's `NAMESPACE_ID_DOMAIN`, which a founded namespace id is hashed under. */
49
+ const NAMESPACE_ID_DOMAIN = new TextEncoder().encode('calimero.namespace.id.v1');
44
50
  /** A post-state hash left for the relay to compute: 32 zero bytes. */
45
51
  const CLEARED_HASH = new Uint8Array(32);
46
52
  /** An empty borsh `Vec`: a zero `u32` count. */
@@ -94,6 +100,27 @@ export function memberRoleSetOp(member, memberRole) {
94
100
  bytes: concat(tag(GROUP_OP.MemberRoleSet), fromHex(member, 'member', 32), role(memberRole)),
95
101
  };
96
102
  }
103
+ /**
104
+ * Set the group's default capability mask (`MemberCapabilities` bits, a `u32`),
105
+ * the capabilities every member holds without a per-member grant. Needs admin.
106
+ *
107
+ * The relay refuses any change to `CAN_AUTHOR_ON_BEHALF` (bit 9) through this
108
+ * path, and no namespace starts with it in its default mask, so a mask that
109
+ * sets it is refused here rather than burning a nonce on a certain 403.
110
+ */
111
+ export function defaultCapabilitiesSetOp(capabilities) {
112
+ if (!Number.isInteger(capabilities) || capabilities < 0 || capabilities > 4294967295) {
113
+ throw new Error(`capabilities must be a u32 bit mask, got ${String(capabilities)}`);
114
+ }
115
+ // Never changed through a relay.
116
+ if (hasCap(capabilities, CAPABILITIES.CAN_AUTHOR_ON_BEHALF)) {
117
+ throw new Error('capabilities may not include CAN_AUTHOR_ON_BEHALF (512): a relay never carries a change to it');
118
+ }
119
+ return {
120
+ kind: 'group',
121
+ bytes: concat(tag(GROUP_OP.DefaultCapabilitiesSet), u32le(capabilities)),
122
+ };
123
+ }
97
124
  /**
98
125
  * Create a subgroup under `parentId`, a root op published on the namespace.
99
126
  *
@@ -123,3 +150,36 @@ export function groupDeletedOp(rootGroupId) {
123
150
  bytes: concat(tag(ROOT_OP.GroupDeleted), fromHex(rootGroupId, 'rootGroupId', 32), EMPTY_VEC, EMPTY_VEC),
124
151
  };
125
152
  }
153
+ /**
154
+ * The id of the namespace `founder` founds with `salt`: core's
155
+ * `founded_namespace_id`, `domain_hash("calimero.namespace.id.v1", [founder, salt])`.
156
+ *
157
+ * Anyone holding the pair can recompute it, and only the founder's account
158
+ * hashes to it, which is why a relay founding a namespace for a member cannot
159
+ * choose the id: the member computes it and signs it.
160
+ */
161
+ export async function foundedNamespaceId(founder, salt) {
162
+ return hex(await domainHash(NAMESPACE_ID_DOMAIN, [
163
+ fromHex(founder, 'founder', 32),
164
+ fromHex(salt, 'salt', 32),
165
+ ]));
166
+ }
167
+ /**
168
+ * Found a namespace: core's `RootOp::NamespaceCreatedV2` genesis, a root op
169
+ * published on the new namespace's own log.
170
+ *
171
+ * Its delegable form is the op itself, nothing cleared, so the author signs the
172
+ * exact genesis. Borsh is `[9] || founder || credential || salt`: the credential
173
+ * is a nested struct, not a length-prefixed byte string, so it is written
174
+ * inline. The warrant's scope must be {@link foundedNamespaceId}`(founder, salt)`.
175
+ */
176
+ export function namespaceCreatedOp(input) {
177
+ const credential = fromHexUnsized(input.credential, 'credential');
178
+ if (credential.length === 0) {
179
+ throw new Error('credential must be the founder\'s AccountProof<DeviceCert>, got nothing');
180
+ }
181
+ return {
182
+ kind: 'root',
183
+ bytes: concat(tag(ROOT_OP.NamespaceCreatedV2), fromHex(input.founder, 'founder', 32), credential, fromHex(input.salt, 'salt', 32)),
184
+ };
185
+ }
@@ -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, groupReparentedOp, groupDeletedOp, } from './governance-op.js';
8
- export type { GovernanceOp, GovernanceOpKind, GovernanceMemberRole, GroupCreatedInput, } from './governance-op.js';
7
+ export { memberAddedOp, memberRemovedOp, memberLeftOp, memberRoleSetOp, groupCreatedOp, groupReparentedOp, groupDeletedOp, namespaceCreatedOp, defaultCapabilitiesSetOp, foundedNamespaceId, } from './governance-op.js';
8
+ export type { GovernanceOp, GovernanceOpKind, GovernanceMemberRole, GroupCreatedInput, 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, groupReparentedOp, groupDeletedOp, } from './governance-op.js';
5
+ export { memberAddedOp, memberRemovedOp, memberLeftOp, memberRoleSetOp, groupCreatedOp, groupReparentedOp, groupDeletedOp, namespaceCreatedOp, defaultCapabilitiesSetOp, foundedNamespaceId, } from './governance-op.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calimero-network/mero-js",
3
- "version": "22.6.0",
3
+ "version": "22.7.0",
4
4
  "description": "Pure JavaScript SDK for Calimero",
5
5
  "type": "module",
6
6
  "exports": {