@cofhe/sdk 0.8.0 → 1.0.0-alpha.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.
Files changed (84) hide show
  1. package/acps/acp.ts +25 -6
  2. package/acps/index.ts +20 -15
  3. package/acps/registry.ts +166 -8
  4. package/acps/sealing.ts +9 -3
  5. package/acps/share-metadata/appKey.ts +8 -0
  6. package/acps/share-metadata/decode.ts +120 -0
  7. package/acps/share-metadata/describe.ts +198 -0
  8. package/acps/share-metadata/encode.ts +147 -0
  9. package/acps/share-metadata/erc7984.ts +26 -0
  10. package/acps/share-metadata/index.ts +9 -0
  11. package/acps/share-metadata/layout.ts +69 -0
  12. package/acps/share-metadata/presets.ts +116 -0
  13. package/acps/share-metadata/schema.ts +47 -0
  14. package/acps/share-metadata/test/codec.test.ts +140 -0
  15. package/acps/share-metadata/test/describe.test.ts +112 -0
  16. package/acps/share-metadata/test/fixtures.ts +157 -0
  17. package/acps/share-metadata/test/live.test.ts +56 -0
  18. package/acps/share-metadata/test/presets.test.ts +119 -0
  19. package/acps/share-metadata/test/verify.test.ts +144 -0
  20. package/acps/share-metadata/verify.ts +155 -0
  21. package/acps/store.ts +48 -18
  22. package/acps/test/acp.test.ts +102 -7
  23. package/acps/test/localstorage.test.ts +28 -1
  24. package/acps/test/registry.test.ts +144 -0
  25. package/acps/test/sealing.test.ts +23 -0
  26. package/acps/test/store.test.ts +71 -0
  27. package/acps/test/utils.test.ts +63 -0
  28. package/acps/test/validation.test.ts +0 -4
  29. package/acps/types.ts +43 -6
  30. package/acps/utils.ts +15 -2
  31. package/acps/validation.ts +3 -1
  32. package/core/acps.ts +123 -74
  33. package/core/client.ts +33 -7
  34. package/core/clientTypes.ts +25 -7
  35. package/core/config.ts +8 -0
  36. package/core/decrypt/apiError.ts +4 -1
  37. package/core/decrypt/verifyDecryptResult.ts +4 -1
  38. package/core/encrypt/cofheMocksZkVerifySign.ts +19 -2
  39. package/core/test/acpDefaults.test.ts +108 -19
  40. package/core/test/acpStoreAccountCase.test.ts +80 -0
  41. package/core/test/acps.test.ts +38 -3
  42. package/core/test/apiError.test.ts +8 -0
  43. package/core/test/appKeyConfig.test.ts +94 -0
  44. package/core/test/config.test.ts +8 -0
  45. package/core/test/decryptBuilders.test.ts +110 -2
  46. package/core/test/incomingShares.test.ts +84 -0
  47. package/core/test/mocksEncryptRange.test.ts +90 -0
  48. package/core/test/utils.test.ts +128 -0
  49. package/core/test/verifyDecryptResult.test.ts +51 -0
  50. package/core/utils.ts +16 -9
  51. package/dist/acps.cjs +674 -33
  52. package/dist/acps.d.cts +367 -35
  53. package/dist/acps.d.ts +367 -35
  54. package/dist/acps.js +2 -2
  55. package/dist/chunk-6ZW76ZKB.js +228 -0
  56. package/dist/{chunk-UJYOBHYA.js → chunk-BQWHPIYU.js} +660 -198
  57. package/dist/{chunk-GGI7Y6AW.js → chunk-OL3DZN7A.js} +110 -269
  58. package/dist/chunk-XQ32JG3T.js +174 -0
  59. package/dist/{clientTypes-D3aIXmUp.d.cts → clientTypes-C0A9ji5O.d.cts} +61 -21
  60. package/dist/{clientTypes-5YPPR16m.d.ts → clientTypes-CcU5WvdV.d.ts} +61 -21
  61. package/dist/core.cjs +3239 -2731
  62. package/dist/core.d.cts +6 -3
  63. package/dist/core.d.ts +6 -3
  64. package/dist/core.js +4 -3
  65. package/dist/node.cjs +3046 -2538
  66. package/dist/node.d.cts +2 -2
  67. package/dist/node.d.ts +2 -2
  68. package/dist/node.js +18 -17
  69. package/dist/{onchain-utils-CUAK8glx.d.cts → onchain-utils-D9Z33IP7.d.cts} +136 -12
  70. package/dist/{onchain-utils-CUAK8glx.d.ts → onchain-utils-D9Z33IP7.d.ts} +136 -12
  71. package/dist/web.cjs +3074 -2573
  72. package/dist/web.d.cts +2 -2
  73. package/dist/web.d.ts +2 -2
  74. package/dist/web.js +4 -10
  75. package/dist/zkProve.worker.cjs +14 -7
  76. package/dist/zkProve.worker.js +2 -8
  77. package/node/index.ts +1 -10
  78. package/node/storage.ts +18 -7
  79. package/node/test/storage-permissions.test.ts +40 -0
  80. package/package.json +4 -4
  81. package/web/index.ts +1 -10
  82. package/web/test/threadPoolBenchHelper.ts +1 -7
  83. package/web/zkProve.worker.ts +1 -10
  84. package/dist/chunk-NMKMCAI3.js +0 -14
package/core/acps.ts CHANGED
@@ -16,7 +16,12 @@ import {
16
16
  ACP_SHARE_REGISTRY_ABI,
17
17
  toChainShare,
18
18
  computeShareId,
19
- shareIdOfChainShare,
19
+ readSharesFor,
20
+ readShare,
21
+ decodeShareAppKey,
22
+ encodeShareMetadata,
23
+ type PostedShare,
24
+ type ShareLabel,
20
25
  getAclServedAddresses,
21
26
  clearAclServedAddresses,
22
27
  } from '@/acps';
@@ -98,13 +103,28 @@ export type ImportActivationOptions = {
98
103
  activate?: boolean;
99
104
  };
100
105
 
106
+ /** Options for importing a shared acp: activation, and the recipient's own name for it. */
107
+ export type ImportSharedOptions = ImportActivationOptions & {
108
+ /**
109
+ * The name of the imported acp. Defaults to "Shared by 0x1234…abcd" (its issuer); the issuer's
110
+ * own name for the share is not exported and is ignored if present.
111
+ */
112
+ name?: string;
113
+ };
114
+
101
115
  const importShared = async (
102
116
  options: ImportSharedACPOptions | string,
103
117
  publicClient: PublicClient,
104
118
  walletClient: WalletClient,
105
- { activate = true }: ImportActivationOptions = {}
119
+ { activate = true, name }: ImportSharedOptions = {}
106
120
  ): Promise<RecipientACP> => {
107
- return createACPWithSign(options, publicClient, walletClient, ACPUtils.importSharedAndSign, activate);
121
+ return createACPWithSign(
122
+ options,
123
+ publicClient,
124
+ walletClient,
125
+ (o, pc, wc) => ACPUtils.importSharedAndSign(o, pc, wc, { name }),
126
+ activate
127
+ );
108
128
  };
109
129
 
110
130
  // ACP UTILS
@@ -222,15 +242,14 @@ const getOrCreateSharingACP = async (
222
242
  // CONFIG DEFAULTS
223
243
 
224
244
  /**
225
- * Applies the config's ACP acp defaults to creation options (pure).
226
- * Explicit user options always win:
227
- * - revoker: injected only when the options carry NO revoker pair —
228
- * revokerContract = config default, revokerData = creation timestamp
229
- * ("every acp revocable by default")
245
+ * Applies the config's ACP defaults to creation options. Explicit user options always win:
246
+ * - revoker: injected only when the options carry NO revoker pair. The contract is the
247
+ * config's `defaultRevoker` for the chain, else the one the ACL serves; the id
248
+ * (`revokerData`) is the latest block's timestamp ("every acp revocable by default")
230
249
  * - contracts: injected only when the options carry NO scope fields at all —
231
250
  * injecting scope makes the created acp non-global by default
232
251
  */
233
- const applyACPDefaults = <
252
+ const applyACPDefaultsFromChain = async <
234
253
  T extends {
235
254
  revokerData?: number;
236
255
  revokerContract?: string;
@@ -241,20 +260,19 @@ const applyACPDefaults = <
241
260
  >(
242
261
  options: T,
243
262
  acpConfig: { defaultRevoker?: Record<number, Hex>; defaultContractScopes?: Record<number, Hex[]> } | undefined,
263
+ publicClient: PublicClient,
244
264
  chainId: number
245
- ): T => {
265
+ ): Promise<T> => {
246
266
  const result = { ...options };
247
267
 
248
- const defaultRevoker = acpConfig?.defaultRevoker?.[chainId];
249
- const hasValidatorOptions = options.revokerData != null || options.revokerContract != null;
250
- if (defaultRevoker != null && !hasValidatorOptions) {
251
- result.revokerContract = defaultRevoker;
252
- // Creation timestamp minus a clock-skew allowance: the revoker rejects
253
- // future-dated ids (vs block.timestamp of the LAST block), so a local clock
254
- // ahead of the chain — or a chain with sparse blocks — would otherwise make
255
- // a fresh acp temporarily unusable. 60s of backdating costs nothing
256
- // (revokeAllExisting at time T still kills this acp for any T >= id).
257
- result.revokerData = Math.round(Date.now() / 1000) - 60;
268
+ const hasRevokerOptions = options.revokerData != null || options.revokerContract != null;
269
+ if (!hasRevokerOptions) {
270
+ const defaultRevoker =
271
+ acpConfig?.defaultRevoker?.[chainId] ?? (await getAclServedAddresses(publicClient, chainId)).defaultRevoker;
272
+ if (defaultRevoker != null) {
273
+ result.revokerContract = defaultRevoker;
274
+ result.revokerData = await getBlockRevokerData(publicClient);
275
+ }
258
276
  }
259
277
 
260
278
  const defaultContracts = acpConfig?.defaultContractScopes?.[chainId];
@@ -267,34 +285,20 @@ const applyACPDefaults = <
267
285
  };
268
286
 
269
287
  /**
270
- * `applyACPDefaults` with the ACL consulted for the default revoker when
271
- * `acp.defaultRevoker` config does not name one for this chain — explicit
272
- * config wins over the ACL-served address.
288
+ * The chain-aware revoker id source: the latest block timestamp, not the local
289
+ * clock. The revoker rejects future-dated ids against `block.timestamp` of the
290
+ * LAST block, so a local clock ahead of the chain (or a chain with sparse
291
+ * blocks) would otherwise make a freshly created acp temporarily unusable.
273
292
  */
274
- const applyACPDefaultsFromChain = async <
275
- T extends {
276
- revokerData?: number;
277
- revokerContract?: string;
278
- scope?: number;
279
- contracts?: string[];
280
- handles?: (bigint | number | string)[];
281
- },
282
- >(
283
- options: T,
284
- acpConfig: { defaultRevoker?: Record<number, Hex>; defaultContractScopes?: Record<number, Hex[]> } | undefined,
285
- publicClient: PublicClient,
286
- chainId: number
287
- ): Promise<T> => {
288
- const hasExplicitRevoker =
289
- acpConfig?.defaultRevoker?.[chainId] != null || options.revokerData != null || options.revokerContract != null;
290
- if (hasExplicitRevoker) return applyACPDefaults(options, acpConfig, chainId);
291
-
292
- const served = await getAclServedAddresses(publicClient, chainId);
293
- const effectiveConfig =
294
- served.defaultRevoker != null
295
- ? { ...acpConfig, defaultRevoker: { ...acpConfig?.defaultRevoker, [chainId]: served.defaultRevoker } }
296
- : acpConfig;
297
- return applyACPDefaults(options, effectiveConfig, chainId);
293
+ const getBlockRevokerData = async (publicClient: PublicClient): Promise<number> => {
294
+ const block = await publicClient.getBlock();
295
+ const revokerData = Number(block.timestamp);
296
+ if (!Number.isSafeInteger(revokerData)) {
297
+ throw new Error(
298
+ `ACP revokerData cannot represent the chain block timestamp ${block.timestamp.toString()} as a safe integer`
299
+ );
300
+ }
301
+ return revokerData;
298
302
  };
299
303
 
300
304
  // REVOKE (on-chain, via the acp's revoker contract)
@@ -390,14 +394,33 @@ const isACPRevoked = async (acp: ACP, publicClient: PublicClient): Promise<boole
390
394
  });
391
395
  };
392
396
 
397
+ /**
398
+ * What `shareOnChain` posts with a share: the labels of its handles (one per handle, in order;
399
+ * encoded with `encodeShareMetadata`), or an already encoded metadata blob. Written once, with the
400
+ * share. Omitted, the labels the acp was created with are posted (`createSharing({ labels })`),
401
+ * or none. `appKey` goes with given labels; a client configured with `acp: { app }` fills it in.
402
+ */
403
+ export type ShareOnChainOptions = { labels?: readonly ShareLabel[]; metadata?: Hex; appKey?: Hex };
404
+
405
+ const metadataOf = (acp: ACP, { labels, metadata, appKey }: ShareOnChainOptions): Hex => {
406
+ if (labels != null && metadata != null) throw new Error('Pass either labels or metadata, not both');
407
+ if (labels == null) return metadata ?? acp.metadata ?? '0x';
408
+ if (labels.length !== acp.handles.length) {
409
+ throw new Error(`Cannot label a share of ${acp.handles.length} handles with ${labels.length} labels`);
410
+ }
411
+ return encodeShareMetadata(labels, { issuer: acp.issuer, appKey });
412
+ };
413
+
393
414
  /**
394
415
  * Post a signed sharing ACP to the on-chain share registry for its recipient
395
- * to discover and import — the on-chain alternative to `export()`.
416
+ * to discover and import — the on-chain alternative to `export()` — optionally
417
+ * with the labels of its handles.
396
418
  */
397
419
  const shareOnChain = async (
398
420
  acp: ACP,
399
421
  walletClient: WalletClient,
400
- registry: Hex
422
+ registry: Hex,
423
+ options: ShareOnChainOptions = {}
401
424
  ): Promise<{ txHash: Hex; shareId: Hex }> => {
402
425
  if (acp.type !== 'sharing') {
403
426
  throw new Error(`Cannot share a '${acp.type}' ACP on-chain — only 'sharing' ACPs are shareable.`);
@@ -409,12 +432,13 @@ const shareOnChain = async (
409
432
  if (walletClient.account.address.toLowerCase() !== acp.issuer.toLowerCase()) {
410
433
  throw new Error('Only the ACP issuer can share it on-chain');
411
434
  }
435
+ const metadata = metadataOf(acp, options);
412
436
 
413
437
  const txHash = await walletClient.writeContract({
414
438
  address: registry,
415
439
  abi: ACP_SHARE_REGISTRY_ABI,
416
440
  functionName: 'share',
417
- args: [toChainShare(acp)],
441
+ args: [toChainShare(acp), metadata],
418
442
  account: walletClient.account,
419
443
  chain: walletClient.chain ?? null,
420
444
  });
@@ -422,46 +446,71 @@ const shareOnChain = async (
422
446
  return { txHash, shareId: computeShareId(acp) };
423
447
  };
424
448
 
425
- /** All importable shares addressed to `recipient` (unexpired, not revoked). */
449
+ const toIncomingShare = ({ shareId, share: s, metadata }: PostedShare): IncomingShare => ({
450
+ shareId,
451
+ issuer: s.issuer,
452
+ expiration: Number(s.expiration),
453
+ recipient: s.recipient,
454
+ revokerData: Number(s.revokerData),
455
+ revokerContract: s.revokerContract,
456
+ scope: Number(s.scope),
457
+ contracts: [...s.contracts],
458
+ handles: [...s.handles],
459
+ issuerSignature: s.issuerSignature,
460
+ metadata,
461
+ ...(appKeyOf(metadata, s.handles) && { appKey: appKeyOf(metadata, s.handles) }),
462
+ });
463
+
464
+ /** The app key of a posted share; undefined when it has none, or its metadata does not decode. */
465
+ const appKeyOf = (metadata: Hex, handles: readonly Hex[]): Hex | undefined => {
466
+ try {
467
+ return decodeShareAppKey(metadata, handles);
468
+ } catch {
469
+ return undefined;
470
+ }
471
+ };
472
+
473
+ /**
474
+ * All importable shares addressed to `recipient` (unexpired, not revoked): the headers from
475
+ * `sharesFor`, then each share's `Shared` event from the block its header names (one
476
+ * single-block `getLogs` per block). A share whose event the node cannot serve yet is left out
477
+ * and turns up on a later read; it never fails the others (`readSharesFor` lists it as
478
+ * unreadable). Given an `appKey`, only the shares posted with that key.
479
+ */
426
480
  const getIncomingShares = async (
427
481
  publicClient: PublicClient,
428
482
  registry: Hex,
429
- recipient: Hex
483
+ recipient: Hex,
484
+ { appKey }: { appKey?: Hex } = {}
430
485
  ): Promise<IncomingShare[]> => {
431
- const raw = await publicClient.readContract({
432
- address: registry,
433
- abi: ACP_SHARE_REGISTRY_ABI,
434
- functionName: 'sharesFor',
435
- args: [recipient],
436
- });
486
+ const shares = (await readSharesFor(publicClient, registry, recipient)).posted.map(toIncomingShare);
487
+ return appKey == null ? shares : shares.filter((share) => share.appKey === appKey);
488
+ };
437
489
 
438
- return raw.map((s) => ({
439
- shareId: shareIdOfChainShare(s),
440
- issuer: s.issuer,
441
- expiration: Number(s.expiration),
442
- recipient: s.recipient,
443
- revokerData: Number(s.revokerData),
444
- revokerContract: s.revokerContract,
445
- scope: Number(s.scope),
446
- contracts: [...s.contracts],
447
- handles: [...s.handles],
448
- issuerSignature: s.issuerSignature,
449
- }));
490
+ /** One share by id, as posted; null when unknown or removed. Expired or revoked shares are returned as is. */
491
+ const getShareFromChain = async (
492
+ publicClient: PublicClient,
493
+ registry: Hex,
494
+ shareId: Hex
495
+ ): Promise<IncomingShare | null> => {
496
+ const posted = await readShare(publicClient, registry, shareId);
497
+ return posted && toIncomingShare(posted);
450
498
  };
451
499
 
452
500
  /**
453
501
  * Import a share read from the registry: fills the recipient's sealing key,
454
502
  * signs, stores and (unless `activate: false`) activates — the on-chain
455
- * counterpart of importing an exported JSON blob. The share stays on-chain
503
+ * counterpart of importing an exported JSON blob. Its metadata is kept on the
504
+ * recipient acp, as with an import of exported JSON. The share stays on-chain
456
505
  * until dismissed.
457
506
  */
458
507
  const importFromChain = async (
459
508
  share: IncomingShare,
460
509
  publicClient: PublicClient,
461
510
  walletClient: WalletClient,
462
- importOptions: ImportActivationOptions = {}
511
+ importOptions: ImportSharedOptions = {}
463
512
  ): Promise<RecipientACP> => {
464
- const { shareId: _shareId, ...options } = share;
513
+ const { shareId: _shareId, appKey: _appKey, ...options } = share;
465
514
  return importShared({ ...options, type: 'sharing' }, publicClient, walletClient, importOptions);
466
515
  };
467
516
 
@@ -519,11 +568,11 @@ export const acps = {
519
568
 
520
569
  shareOnChain,
521
570
  getIncomingShares,
571
+ getShareFromChain,
522
572
  importFromChain,
523
573
  removeShareOnChain,
524
574
  computeShareId,
525
575
 
526
- applyACPDefaults,
527
576
  applyACPDefaultsFromChain,
528
577
  getAclServedAddresses,
529
578
  clearAclServedAddresses,
package/core/client.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { CreateSelfACPOptions, CreateSharingACPOptions, ImportSharedACPOptions } from '@/acps';
2
+ import { decodeShareMetadata, shareAppKey, verifyShareLabels } from '@/acps';
2
3
 
3
4
  import { createStore } from 'zustand/vanilla';
4
5
  import { type Hex, type PublicClient, type WalletClient } from 'viem';
@@ -50,6 +51,10 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
50
51
 
51
52
  // Share registry resolution: explicit `acp.sharingRegistry` config wins,
52
53
  // otherwise the address the chain's ACL serves.
54
+ /** `{ appKey }` of `config.acp.app`, for the shares this client creates, posts and lists; `{}` without an app. */
55
+ const withAppKey = (): { appKey?: `0x${string}` } =>
56
+ opts.config.acp?.app == null ? {} : { appKey: shareAppKey(opts.config.acp.app) };
57
+
53
58
  const _resolveSharingRegistry = async (publicClient: PublicClient, chainId: number): Promise<`0x${string}`> => {
54
59
  const configured = opts.config.acp?.sharingRegistry?.[chainId];
55
60
  if (configured != null) return configured;
@@ -254,7 +259,7 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
254
259
  const { publicClient, walletClient } = clients ?? connectStore.getState();
255
260
  const chainId = await publicClient!.getChainId();
256
261
  return acps.createSharing(
257
- await acps.applyACPDefaultsFromChain(options, opts.config.acp, publicClient!, chainId),
262
+ await acps.applyACPDefaultsFromChain({ ...withAppKey(), ...options }, opts.config.acp, publicClient!, chainId),
258
263
  publicClient!,
259
264
  walletClient!
260
265
  );
@@ -262,9 +267,9 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
262
267
 
263
268
  importShared: async (options: ImportSharedACPOptions | string, importOptions = {}) => {
264
269
  _requireConnected();
265
- const { activate, ...clients } = importOptions;
270
+ const { activate, name, ...clients } = importOptions;
266
271
  const { publicClient, walletClient } = clients.publicClient ? clients : connectStore.getState();
267
- return acps.importShared(options, publicClient!, walletClient!, { activate });
272
+ return acps.importShared(options, publicClient!, walletClient!, { activate, name });
268
273
  },
269
274
 
270
275
  // Get or create methods (require connection)
@@ -288,7 +293,7 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
288
293
  return acps.getOrCreateSharingACP(
289
294
  publicClient!,
290
295
  walletClient!,
291
- await acps.applyACPDefaultsFromChain(options, opts.config.acp, publicClient!, _chainId),
296
+ await acps.applyACPDefaultsFromChain({ ...withAppKey(), ...options }, opts.config.acp, publicClient!, _chainId),
292
297
  _chainId,
293
298
  _account
294
299
  );
@@ -320,12 +325,12 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
320
325
  },
321
326
 
322
327
  // On-chain sharing (require connection + config.acp.sharingRegistry)
323
- shareOnChain: async (acp) => {
328
+ shareOnChain: async (acp, options) => {
324
329
  _requireConnected();
325
330
  const { publicClient, walletClient } = connectStore.getState();
326
331
  const chainId = await publicClient!.getChainId();
327
332
  const sharingRegistry = await _resolveSharingRegistry(publicClient!, chainId);
328
- return acps.shareOnChain(acp, walletClient!, sharingRegistry);
333
+ return acps.shareOnChain(acp, walletClient!, sharingRegistry, { ...withAppKey(), ...options });
329
334
  },
330
335
 
331
336
  getIncomingShares: async () => {
@@ -334,7 +339,28 @@ export function createCofheClientBase<TConfig extends CofheConfig>(
334
339
  const chainId = await publicClient!.getChainId();
335
340
  const account = walletClient!.account!.address;
336
341
  const sharingRegistry = await _resolveSharingRegistry(publicClient!, chainId);
337
- return acps.getIncomingShares(publicClient!, sharingRegistry, account);
342
+ return acps.getIncomingShares(publicClient!, sharingRegistry, account, withAppKey());
343
+ },
344
+
345
+ getShareFromChain: async (shareId) => {
346
+ _requireConnected();
347
+ const { publicClient } = connectStore.getState();
348
+ const chainId = await publicClient!.getChainId();
349
+ const sharingRegistry = await _resolveSharingRegistry(publicClient!, chainId);
350
+ return acps.getShareFromChain(publicClient!, sharingRegistry, shareId);
351
+ },
352
+
353
+ verifyShareLabels: async (share, options) => {
354
+ _requireConnected();
355
+ const { publicClient } = connectStore.getState();
356
+ if (share.metadata == null || share.metadata === '0x') return null;
357
+ return verifyShareLabels({
358
+ labels: decodeShareMetadata(share.metadata, share.handles),
359
+ handles: share.handles,
360
+ issuer: share.issuer,
361
+ publicClient: publicClient!,
362
+ verify: options?.verify,
363
+ });
338
364
  },
339
365
 
340
366
  importFromChain: async (share, options) => {
@@ -6,7 +6,7 @@ import { type DecryptForTxBuilderUnset } from './decrypt/decryptForTxBuilder.js'
6
6
  import { type EncryptInputsBuilderUnset } from './encrypt/encryptInputsBuilder.js';
7
7
  import { type ZkBuilderAndCrsGenerator, type ZkProveWorkerFunction } from './encrypt/zkPackProveVerify.js';
8
8
  import { type FheKeyDeserializer } from './fetchKeys.js';
9
- import { acps, type ImportActivationOptions } from './acps.js';
9
+ import { acps, type ImportSharedOptions, type ShareOnChainOptions } from './acps.js';
10
10
  import type { EncryptableItem, FheTypes, TfheInitializer } from './types.js';
11
11
  import type { ACPUtils } from 'acps/acp.js';
12
12
  import type {
@@ -19,7 +19,9 @@ import type {
19
19
  RecipientACP,
20
20
  SelfACP,
21
21
  IncomingShare,
22
+ LabelledShare,
22
23
  } from 'acps/types.js';
24
+ import type { ShareLabelCheck, ShareLabelVerifyMode } from 'acps/share-metadata/verify.js';
23
25
 
24
26
  // CLIENT
25
27
 
@@ -77,9 +79,10 @@ export type CofheClientACPsClients = {
77
79
 
78
80
  /**
79
81
  * Options for `importShared`. `activate: false` stores the imported acp without making it the active
80
- * one (default `true`). Clients default to the connected ones; pass both to override.
82
+ * one (default `true`). `name` names it (default "Shared by 0x1234…abcd"). Clients default to the
83
+ * connected ones; pass both to override.
81
84
  */
82
- export type CofheClientImportSharedOptions = ImportActivationOptions &
85
+ export type CofheClientImportSharedOptions = ImportSharedOptions &
83
86
  (CofheClientACPsClients | { publicClient?: undefined; walletClient?: undefined });
84
87
 
85
88
  export type CofheClientACPs = {
@@ -121,12 +124,27 @@ export type CofheClientACPs = {
121
124
  */
122
125
  checkAccess: (acp: ACP, handle?: bigint | `0x${string}`) => Promise<ACPAccessStatus>;
123
126
 
124
- /** Post a signed sharing ACP to the on-chain share registry (ACL-served, or config `acp.sharingRegistry`). */
125
- shareOnChain: (acp: ACP) => Promise<{ txHash: `0x${string}`; shareId: `0x${string}` }>;
126
- /** Importable shares addressed to the connected account (unexpired, not revoked). */
127
+ /**
128
+ * Post a signed sharing ACP to the on-chain share registry (ACL-served, or config `acp.sharingRegistry`),
129
+ * optionally with the labels of its handles (`options.labels`, one per handle) or an encoded blob.
130
+ */
131
+ shareOnChain: (acp: ACP, options?: ShareOnChainOptions) => Promise<{ txHash: `0x${string}`; shareId: `0x${string}` }>;
132
+ /** Importable shares addressed to the connected account (unexpired, not revoked), with their metadata. */
127
133
  getIncomingShares: () => Promise<IncomingShare[]>;
134
+ /** One share by id, as posted; null when unknown or removed. */
135
+ getShareFromChain: (shareId: `0x${string}`) => Promise<IncomingShare | null>;
136
+ /**
137
+ * Check a share's labels against the chain, index for index with its handles, as far as
138
+ * `verify` says (default 'all'). Takes a share read from the registry or an acp imported from
139
+ * exported JSON (or the issuer's sharing acp). Null for a share without metadata. Throws on a
140
+ * malformed blob.
141
+ */
142
+ verifyShareLabels: (
143
+ share: LabelledShare,
144
+ options?: { verify?: ShareLabelVerifyMode }
145
+ ) => Promise<ShareLabelCheck[] | null>;
128
146
  /** Import a share read from the registry: sign as recipient, store and (unless `activate: false`) activate. */
129
- importFromChain: (share: IncomingShare, options?: ImportActivationOptions) => Promise<RecipientACP>;
147
+ importFromChain: (share: IncomingShare, options?: ImportSharedOptions) => Promise<RecipientACP>;
130
148
  /** Recipient-side: remove a share from the registry (after import, or to decline). */
131
149
  dismissShare: (shareId: `0x${string}`) => Promise<`0x${string}`>;
132
150
  /** Issuer-side: retract a pending share from the registry. */
package/core/config.ts CHANGED
@@ -73,6 +73,13 @@ export type CofheConfig = {
73
73
  * names a registry.
74
74
  */
75
75
  sharingRegistry?: Record<number, `0x${string}`>;
76
+ /**
77
+ * The name of this app, e.g. 'explorer.fhenix.io'. Set, every share the client creates
78
+ * carries the app's key (`shareAppKey(app)`, kept in the share metadata), and
79
+ * `getIncomingShares` lists only the shares posted for this app. Unset, shares carry no
80
+ * key and every importable share is listed.
81
+ */
82
+ app?: string;
76
83
  };
77
84
  /** Mocks configs */
78
85
  mocks: {
@@ -138,6 +145,7 @@ export const CofheConfigSchema = z.object({
138
145
  defaultRevoker: z.custom<Record<number, `0x${string}`>>().optional(),
139
146
  defaultContractScopes: z.custom<Record<number, `0x${string}`[]>>().optional(),
140
147
  sharingRegistry: z.custom<Record<number, `0x${string}`>>().optional(),
148
+ app: z.string().min(1).optional(),
141
149
  })
142
150
  .optional()
143
151
  .default({}),
@@ -48,7 +48,10 @@ const BACKEND_ERROR_CODE_TO_COFHE_ERROR_CODE: Record<BackendApiErrorCode, CofheE
48
48
  };
49
49
 
50
50
  function isBackendApiErrorCode(value: string): value is BackendApiErrorCode {
51
- return value in BACKEND_ERROR_CODE_TO_COFHE_ERROR_CODE;
51
+ // Own-property check, not `in`: `in` also matches Object.prototype keys, so codes like
52
+ // `toString` or `constructor` would be treated as recognised and the lookup would return a
53
+ // function instead of a CofheErrorCode.
54
+ return Object.prototype.hasOwnProperty.call(BACKEND_ERROR_CODE_TO_COFHE_ERROR_CODE, value);
52
55
  }
53
56
 
54
57
  export type ParsedApiError = {
@@ -30,6 +30,9 @@ const buildDecryptResultHash = (ctHash: bigint, cleartext: bigint, chainId: numb
30
30
  *
31
31
  * The recovered signer must equal the on-chain configured `decryptResultSigner`.
32
32
  *
33
+ * Fails closed: if the chain has no `decryptResultSigner` set (zero address), this returns `false`.
34
+ * The on-chain `verifyDecryptResult` instead accepts any signature in that case.
35
+ *
33
36
  * This mirrors the TaskManager decrypt-result hash format:
34
37
  * `keccak256(abi.encodePacked(result, encType, chainId, ctHash))`
35
38
  *
@@ -52,7 +55,7 @@ export async function verifyDecryptResult(
52
55
  args: [],
53
56
  });
54
57
 
55
- if (isAddressEqual(expectedSigner, zeroAddress)) return true;
58
+ if (isAddressEqual(expectedSigner, zeroAddress)) return false;
56
59
 
57
60
  const ctHash = BigInt(handle);
58
61
  const messageHash = buildDecryptResultHash(ctHash, cleartext, chainId);
@@ -1,5 +1,14 @@
1
1
  import { type EncryptableItem, FheTypes } from '../types.js';
2
- import { type VerifyBatchResult } from './zkPackProveVerify.js';
2
+ import {
3
+ type VerifyBatchResult,
4
+ MAX_UINT8,
5
+ MAX_UINT16,
6
+ MAX_UINT32,
7
+ MAX_UINT64,
8
+ MAX_UINT128,
9
+ MAX_UINT160,
10
+ } from './zkPackProveVerify.js';
11
+ import { toBigIntOrThrow, validateBigIntInRange } from '../utils.js';
3
12
  import { createWalletClient, http, encodePacked, keccak256, type PublicClient, type WalletClient } from 'viem';
4
13
  import { MockZkVerifierAbi } from './MockZkVerifierAbi.js';
5
14
  import { hardhat } from 'viem/chains';
@@ -19,7 +28,9 @@ function createMockZkVerifierSigner() {
19
28
  });
20
29
  }
21
30
  /**
22
- * The mocks don't use a tfhe builder, so we check the encryptable bits here to preserve parity
31
+ * The mocks don't use a tfhe builder, so we check the encryptable bits and value ranges here to
32
+ * preserve parity with `zkPack`. Without the range check, MockCoFHE masks an out-of-range value
33
+ * with the utype mask (e.g. uint8(256) -> 0) instead of rejecting it like the production path.
23
34
  */
24
35
  export async function cofheMocksCheckEncryptableBits(items: EncryptableItem[]): Promise<void> {
25
36
  let totalBits = 0;
@@ -30,22 +41,27 @@ export async function cofheMocksCheckEncryptableBits(items: EncryptableItem[]):
30
41
  break;
31
42
  }
32
43
  case FheTypes.Uint8: {
44
+ validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT8);
33
45
  totalBits += 8;
34
46
  break;
35
47
  }
36
48
  case FheTypes.Uint16: {
49
+ validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT16);
37
50
  totalBits += 16;
38
51
  break;
39
52
  }
40
53
  case FheTypes.Uint32: {
54
+ validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT32);
41
55
  totalBits += 32;
42
56
  break;
43
57
  }
44
58
  case FheTypes.Uint64: {
59
+ validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT64);
45
60
  totalBits += 64;
46
61
  break;
47
62
  }
48
63
  case FheTypes.Uint128: {
64
+ validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT128);
49
65
  totalBits += 128;
50
66
  break;
51
67
  }
@@ -55,6 +71,7 @@ export async function cofheMocksCheckEncryptableBits(items: EncryptableItem[]):
55
71
  // break;
56
72
  // }
57
73
  case FheTypes.Uint160: {
74
+ validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT160);
58
75
  totalBits += 160;
59
76
  break;
60
77
  }