@idos-network/client 1.5.0 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -3,28 +3,38 @@ import type {
3
3
  EncryptionPasswordStore,
4
4
  PublicEncryptionProfile,
5
5
  } from "@idos-network/enclave";
6
- import type { KwilSigner } from "@idos-network/kwil-js";
7
6
 
8
7
  import {
9
- buildInsertableIDOSCredential,
8
+ buildEphemeralSignedCredentialContentReference,
9
+ buildPreliminaryIDOSCredential,
10
+ mapPreliminaryToIDOSCredential,
10
11
  matchLevelOrHigher,
12
+ type PreliminaryIDOSCredential,
11
13
  recordFilter,
14
+ type BaseLevel,
15
+ type Addon,
12
16
  } from "@idos-network/credentials/utils";
13
17
  import {
14
18
  createClientKwilSigner,
19
+ createKgwAuthenticatedBlobGateway,
20
+ createNodeKwilClient,
15
21
  createWebKwilClient,
22
+ isMmTokenAuth,
16
23
  type KwilActionClient,
24
+ mmTokenCredentialContentUri,
17
25
  signNearMessage,
18
26
  type Wallet,
19
27
  } from "@idos-network/kwil-infra";
20
28
  import {
21
29
  type AddAttributeInput,
22
30
  type AddWalletInput,
31
+ type AddWalletMessageInput,
23
32
  addWallet,
33
+ addWalletMessage as requestAddWalletMessage,
24
34
  addWallets,
25
35
  addAttribute as createAttribute,
26
- type DagMessageInput,
27
- dagMessage,
36
+ createPreliminaryCredential,
37
+ type CreatePreliminaryCredentialInput,
28
38
  dwgMessage,
29
39
  type GetAccessGrantsGrantedInput,
30
40
  type GetWalletsOutput,
@@ -41,6 +51,8 @@ import {
41
51
  type idOSCredential,
42
52
  type idOSCredentialListItem,
43
53
  type idOSDelegatedWriteGrant,
54
+ type DelegatedWriteGrantBaseParams,
55
+ toDelegatedWriteGrantBaseParams,
44
56
  type idOSGrant,
45
57
  type idOSUser,
46
58
  type idOSUserAttribute,
@@ -48,18 +60,24 @@ import {
48
60
  removeCredential,
49
61
  removeWallet,
50
62
  revokeAccessGrant,
51
- type ShareCredentialInput,
52
- shareCredential,
63
+ type SharePreliminaryCredentialInput,
64
+ sharePreliminaryCredential,
53
65
  type WalletType,
54
66
  } from "@idos-network/kwil-infra/actions";
67
+ import { NodeKwil, type KwilSigner } from "@idos-network/kwil-js";
68
+ import {
69
+ BlobGateway,
70
+ createBlobContentReference,
71
+ requireAccessTokenForUkycContent,
72
+ resolveCredentialEncryptedContent,
73
+ } from "@idos-network/utils/blob-gateway";
55
74
  import {
56
75
  base64Decode,
57
76
  base64Encode,
58
77
  hexEncodeSha256Hash,
59
78
  utf8Decode,
60
- utf8Encode,
61
79
  } from "@idos-network/utils/codecs";
62
- import { LocalStorageStore, type Store } from "@idos-network/utils/store";
80
+ import { LocalStorageStore, MemoryStore, type Store } from "@idos-network/utils/store";
63
81
  import invariant from "tiny-invariant";
64
82
 
65
83
  import { IframeEnclave } from "./enclave/iframe-enclave";
@@ -70,6 +88,226 @@ type Properties<T> = {
70
88
  [K in keyof T as Exclude<T[K], Function> extends never ? never : K]: T[K];
71
89
  };
72
90
 
91
+ function isBrowserRuntime(): boolean {
92
+ return typeof window !== "undefined";
93
+ }
94
+
95
+ function usesNodeKwil(kwilClient: KwilActionClient): boolean {
96
+ return kwilClient.client instanceof NodeKwil;
97
+ }
98
+
99
+ function idleBlobGateway(url: string): BlobGateway {
100
+ if (!isBrowserRuntime()) {
101
+ return new BlobGateway({ url });
102
+ }
103
+
104
+ return new BlobGateway({
105
+ url,
106
+ // Browser blob reads need the KGW HttpOnly session cookie. That only works if the
107
+ // blob gateway is covered by the cookie's Domain (typically same host as nodeUrl/KGW).
108
+ fetchFn: (input, init) => fetch(input, { ...init, credentials: "include" }),
109
+ });
110
+ }
111
+
112
+ function blobGatewayForSigner(idle: idOSClientIdle, kwilSigner: KwilSigner): BlobGateway {
113
+ if (usesNodeKwil(idle.kwilClient)) {
114
+ return createKgwAuthenticatedBlobGateway({
115
+ url: idle.blobGateway.url,
116
+ kwilClient: idle.kwilClient,
117
+ signer: kwilSigner,
118
+ });
119
+ }
120
+
121
+ return isMmTokenAuth(kwilSigner)
122
+ ? idle.blobGateway.withAccessToken(kwilSigner.accessToken)
123
+ : idle.blobGateway;
124
+ }
125
+
126
+ function contentUriForWallet(
127
+ walletType: WalletType,
128
+ kwilSigner: KwilSigner,
129
+ credentialId: string,
130
+ ): string | undefined {
131
+ if (walletType !== "MM") {
132
+ return undefined;
133
+ }
134
+ return mmTokenCredentialContentUri(kwilSigner, credentialId);
135
+ }
136
+
137
+ const STALE_CLIENT =
138
+ "This idOS client is stale: another signer was set or it was logged out. Use the client returned by the latest `withUserSigner()`.";
139
+
140
+ // Wrappers from one idle client share its Kwil client and enclave. `rawOf` unwraps the
141
+ // guard below; `turns` keeps signer replacement and enclave reconfiguration from interleaving.
142
+ const rawOf = new WeakMap<KwilActionClient, KwilActionClient>();
143
+ const boundOf = new WeakMap<KwilActionClient, WeakMap<KwilSigner, KwilActionClient>>();
144
+ const rawEnclaveOf = new WeakMap<BaseProvider, BaseProvider>();
145
+ const boundEnclaveOf = new WeakMap<BaseProvider, WeakMap<KwilSigner, BaseProvider>>();
146
+ const turns = new WeakMap<KwilActionClient, Promise<void>>();
147
+ const resetPending = new WeakSet<KwilActionClient>();
148
+
149
+ function sharedKwilClient(kwilClient: KwilActionClient): KwilActionClient {
150
+ return rawOf.get(kwilClient) ?? kwilClient;
151
+ }
152
+
153
+ function assertSigner(kwilClient: KwilActionClient, kwilSigner: KwilSigner): void {
154
+ if (kwilClient.signer !== kwilSigner) throw new Error(STALE_CLIENT);
155
+ }
156
+
157
+ // shortcut: guards call/execute/waitForTx/setSigner only; wrap any new KwilActionClient method here.
158
+ function boundKwilClient(kwilClient: KwilActionClient, kwilSigner: KwilSigner): KwilActionClient {
159
+ const raw = sharedKwilClient(kwilClient);
160
+ const cached = kwilSigner ? boundOf.get(raw)?.get(kwilSigner) : undefined;
161
+ if (cached) return cached;
162
+
163
+ const guard = (): void => assertSigner(raw, kwilSigner);
164
+ const bound = {
165
+ get signer() {
166
+ guard();
167
+ return raw.signer;
168
+ },
169
+ get client() {
170
+ guard();
171
+ return raw.client;
172
+ },
173
+ call: async (...args: Parameters<KwilActionClient["call"]>) => {
174
+ guard();
175
+ return raw.call(...args);
176
+ },
177
+ execute: async (...args: Parameters<KwilActionClient["execute"]>) => {
178
+ guard();
179
+ return raw.execute(...args);
180
+ },
181
+ waitForTx: async (txHash: string) => {
182
+ guard();
183
+ return raw.waitForTx(txHash);
184
+ },
185
+ setSigner: (signer: KwilSigner | undefined) => {
186
+ guard();
187
+ raw.setSigner(signer);
188
+ },
189
+ } as KwilActionClient;
190
+
191
+ rawOf.set(bound, raw);
192
+ if (kwilSigner) {
193
+ const bySigner = boundOf.get(raw) ?? new WeakMap<KwilSigner, KwilActionClient>();
194
+ bySigner.set(kwilSigner, bound);
195
+ boundOf.set(raw, bySigner);
196
+ }
197
+ return bound;
198
+ }
199
+
200
+ function sharedEnclave(enclave: BaseProvider): BaseProvider {
201
+ return (enclave && rawEnclaveOf.get(enclave)) || enclave;
202
+ }
203
+
204
+ function boundEnclave(
205
+ enclave: BaseProvider,
206
+ kwilClient: KwilActionClient,
207
+ kwilSigner: KwilSigner,
208
+ ): BaseProvider {
209
+ const raw = sharedEnclave(enclave);
210
+ const cached = kwilSigner ? boundEnclaveOf.get(raw)?.get(kwilSigner) : undefined;
211
+ if (cached) return cached;
212
+
213
+ const bound = new Proxy(raw, {
214
+ get(target, prop) {
215
+ if (prop === "then" || typeof prop === "symbol") return undefined;
216
+ const value = Reflect.get(target, prop, target);
217
+ if (typeof value !== "function") {
218
+ assertSigner(kwilClient, kwilSigner);
219
+ return value;
220
+ }
221
+ return (...args: unknown[]) => {
222
+ assertSigner(kwilClient, kwilSigner);
223
+ return value.apply(target, args);
224
+ };
225
+ },
226
+ }) as BaseProvider;
227
+
228
+ rawEnclaveOf.set(bound, raw);
229
+ if (kwilSigner) {
230
+ const bySigner = boundEnclaveOf.get(raw) ?? new WeakMap<KwilSigner, BaseProvider>();
231
+ bySigner.set(kwilSigner, bound);
232
+ boundEnclaveOf.set(raw, bySigner);
233
+ }
234
+ return bound;
235
+ }
236
+
237
+ function exclusive<T>(kwilClient: KwilActionClient, run: () => Promise<T>): Promise<T> {
238
+ const raw = sharedKwilClient(kwilClient);
239
+ const previous = turns.get(raw) ?? Promise.resolve();
240
+ const result = previous.then(run, run);
241
+ turns.set(
242
+ raw,
243
+ result.then(
244
+ () => undefined,
245
+ () => undefined,
246
+ ),
247
+ );
248
+ return result;
249
+ }
250
+
251
+ // `setSigner` is synchronous on the real clients. A rejected promise here is not awaited
252
+ // by `withUserSigner` or the enclave constructor, and Node can terminate on it.
253
+ function detachedSurface<T extends object>(): T {
254
+ return new Proxy({} as T, {
255
+ get(_target, prop) {
256
+ if (prop === "signer") return undefined;
257
+ if (prop === "then" || typeof prop === "symbol") return undefined;
258
+ if (prop === "client") throw new Error(STALE_CLIENT);
259
+ if (prop === "setSigner") {
260
+ return () => {
261
+ throw new Error(STALE_CLIENT);
262
+ };
263
+ }
264
+ return async () => {
265
+ throw new Error(STALE_CLIENT);
266
+ };
267
+ },
268
+ });
269
+ }
270
+
271
+ function staleIdle(store: Store, blobUrl: string): idOSClientIdle {
272
+ return new idOSClientIdle(
273
+ store,
274
+ detachedSurface<KwilActionClient>(),
275
+ detachedSurface<BaseProvider>(),
276
+ idleBlobGateway(blobUrl),
277
+ );
278
+ }
279
+
280
+ // A replaced signer keeps the session. The signer that actually logged out must finish
281
+ // `reset` on a later attempt; success is only returned once that reset completes.
282
+ function finishLogOut(
283
+ store: Store,
284
+ kwilClient: KwilActionClient,
285
+ kwilSigner: KwilSigner,
286
+ enclaveProvider: BaseProvider,
287
+ blobUrl: string,
288
+ ): Promise<idOSClientIdle> {
289
+ const raw = sharedKwilClient(kwilClient);
290
+ return exclusive(raw, async () => {
291
+ const current = raw.signer;
292
+ if (current !== undefined && current !== kwilSigner) return staleIdle(store, blobUrl);
293
+
294
+ if (current === kwilSigner) {
295
+ raw.setSigner(undefined);
296
+ resetPending.add(raw);
297
+ }
298
+
299
+ if (resetPending.has(raw)) {
300
+ await enclaveProvider.reset();
301
+ resetPending.delete(raw);
302
+ }
303
+
304
+ // A signer installed while `reset` was in progress must not ride back on this idle client.
305
+ if (raw.signer !== undefined && raw.signer !== kwilSigner) return staleIdle(store, blobUrl);
306
+
307
+ return new idOSClientIdle(store, raw, enclaveProvider, idleBlobGateway(blobUrl));
308
+ });
309
+ }
310
+
73
311
  export type idOSClient =
74
312
  | idOSClientConfiguration
75
313
  | idOSClientIdle
@@ -80,6 +318,7 @@ export class idOSClientConfiguration<Provider extends BaseProvider = IframeEncla
80
318
  readonly state: "configuration";
81
319
  readonly chainId?: string;
82
320
  readonly nodeUrl: string;
321
+ readonly blobGatewayUrl?: string;
83
322
  readonly enclaveOptions: Omit<Provider["options"], "mode">;
84
323
  readonly store: Store;
85
324
  readonly enclaveProvider: BaseProvider;
@@ -87,6 +326,12 @@ export class idOSClientConfiguration<Provider extends BaseProvider = IframeEncla
87
326
  constructor(params: {
88
327
  chainId?: string;
89
328
  nodeUrl: string;
329
+ /**
330
+ * Optional blob gateway base URL. Defaults to `nodeUrl`.
331
+ * In the browser this must share KGW's session-cookie Domain
332
+ * (typically the same host), because reads use `credentials: "include"`.
333
+ */
334
+ blobGatewayUrl?: string;
90
335
  enclaveOptions: Omit<Provider["options"], "mode">;
91
336
  enclaveProvider?: new (options: Omit<Provider["options"], "mode">) => Provider;
92
337
  store?: Store;
@@ -94,8 +339,9 @@ export class idOSClientConfiguration<Provider extends BaseProvider = IframeEncla
94
339
  this.state = "configuration";
95
340
  this.chainId = params.chainId;
96
341
  this.nodeUrl = params.nodeUrl;
342
+ this.blobGatewayUrl = params.blobGatewayUrl;
97
343
  this.enclaveOptions = params.enclaveOptions;
98
- this.store = params.store ?? new LocalStorageStore();
344
+ this.store = params.store ?? (isBrowserRuntime() ? new LocalStorageStore() : new MemoryStore());
99
345
 
100
346
  // TODO: This is a mess because of types...
101
347
  if (params.enclaveProvider) {
@@ -120,23 +366,37 @@ export class idOSClientIdle {
120
366
  readonly store: Store;
121
367
  readonly kwilClient: KwilActionClient;
122
368
  readonly enclaveProvider: BaseProvider;
369
+ readonly blobGateway: BlobGateway;
123
370
 
124
- constructor(store: Store, kwilClient: KwilActionClient, enclaveProvider: BaseProvider) {
371
+ constructor(
372
+ store: Store,
373
+ kwilClient: KwilActionClient,
374
+ enclaveProvider: BaseProvider,
375
+ blobGateway: BlobGateway,
376
+ ) {
125
377
  this.state = "idle";
126
378
  this.store = store;
127
379
  this.kwilClient = kwilClient;
128
380
  this.enclaveProvider = enclaveProvider;
381
+ this.blobGateway = blobGateway;
129
382
  }
130
383
 
131
384
  static async fromConfig(params: idOSClientConfiguration<BaseProvider>): Promise<idOSClientIdle> {
132
- const kwilClient = await createWebKwilClient({
385
+ const createKwilClient = isBrowserRuntime() ? createWebKwilClient : createNodeKwilClient;
386
+ const kwilClient = await createKwilClient({
133
387
  nodeUrl: params.nodeUrl,
134
388
  chainId: params.chainId,
135
389
  });
136
390
 
137
391
  await params.enclaveProvider.load();
138
392
 
139
- return new idOSClientIdle(params.store, kwilClient, params.enclaveProvider);
393
+ const blobGatewayUrl = params.blobGatewayUrl ?? params.nodeUrl;
394
+ return new idOSClientIdle(
395
+ params.store,
396
+ kwilClient,
397
+ params.enclaveProvider,
398
+ idleBlobGateway(blobGatewayUrl),
399
+ );
140
400
  }
141
401
 
142
402
  async addressHasProfile(address: string): Promise<boolean> {
@@ -148,8 +408,6 @@ export class idOSClientIdle {
148
408
  const [kwilSigner, walletIdentifier, walletPublicKey, walletType] =
149
409
  await createClientKwilSigner(this.store, this.kwilClient, signer);
150
410
 
151
- this.kwilClient.setSigner(kwilSigner);
152
-
153
411
  if (walletType === "NEAR") {
154
412
  const originalSigner = signer;
155
413
  signer = {
@@ -163,14 +421,18 @@ export class idOSClientIdle {
163
421
  } as unknown as Wallet;
164
422
  }
165
423
 
166
- return new idOSClientWithUserSigner(
167
- this,
168
- signer,
169
- kwilSigner,
170
- walletIdentifier,
171
- walletPublicKey,
172
- walletType,
173
- );
424
+ return exclusive(this.kwilClient, async () => {
425
+ this.kwilClient.setSigner(kwilSigner);
426
+ return new idOSClientWithUserSigner(
427
+ this,
428
+ signer,
429
+ kwilSigner,
430
+ walletIdentifier,
431
+ walletPublicKey,
432
+ walletType,
433
+ blobGatewayForSigner(this, kwilSigner),
434
+ );
435
+ });
174
436
  }
175
437
 
176
438
  async logOut(): Promise<idOSClientIdle> {
@@ -181,13 +443,14 @@ export class idOSClientIdle {
181
443
  export class idOSClientWithUserSigner implements Omit<Properties<idOSClientIdle>, "state"> {
182
444
  readonly state: "with-user-signer";
183
445
  readonly store: Store;
184
- readonly kwilClient: KwilActionClient;
185
- readonly enclaveProvider: BaseProvider;
446
+ readonly #kwilClient: KwilActionClient;
447
+ readonly #enclaveProvider: BaseProvider;
186
448
  readonly signer: Wallet;
187
449
  readonly kwilSigner: KwilSigner;
188
450
  readonly walletIdentifier: string;
189
451
  readonly walletPublicKey: string | undefined;
190
452
  readonly walletType: WalletType;
453
+ readonly blobGateway: BlobGateway;
191
454
 
192
455
  constructor(
193
456
  idOSClientIdle: idOSClientIdle,
@@ -196,23 +459,38 @@ export class idOSClientWithUserSigner implements Omit<Properties<idOSClientIdle>
196
459
  walletIdentifier: string,
197
460
  walletPublicKey: string | undefined,
198
461
  walletType: WalletType,
462
+ blobGateway: BlobGateway,
199
463
  ) {
200
464
  this.state = "with-user-signer";
201
465
  this.store = idOSClientIdle.store;
202
- this.kwilClient = idOSClientIdle.kwilClient;
203
- this.enclaveProvider = idOSClientIdle.enclaveProvider;
466
+ this.#kwilClient = idOSClientIdle.kwilClient;
467
+ this.#enclaveProvider = idOSClientIdle.enclaveProvider;
204
468
  this.signer = signer;
205
469
  this.kwilSigner = kwilSigner;
206
470
  this.walletIdentifier = walletIdentifier;
207
471
  this.walletPublicKey = walletPublicKey;
208
472
  this.walletType = walletType;
473
+ this.blobGateway = blobGateway;
209
474
  // @ts-expect-error - TODO: Fix this
210
- this.enclaveProvider.setSigner(this.signer);
475
+ this.#enclaveProvider.setSigner(this.signer);
476
+ }
477
+
478
+ get kwilClient(): KwilActionClient {
479
+ return boundKwilClient(this.#kwilClient, this.kwilSigner);
480
+ }
481
+
482
+ get enclaveProvider(): BaseProvider {
483
+ return boundEnclave(this.#enclaveProvider, this.#kwilClient, this.kwilSigner);
211
484
  }
212
485
 
213
486
  async logOut(): Promise<idOSClientIdle> {
214
- this.kwilClient.setSigner(undefined);
215
- return new idOSClientIdle(this.store, this.kwilClient, this.enclaveProvider);
487
+ return finishLogOut(
488
+ this.store,
489
+ this.#kwilClient,
490
+ this.kwilSigner,
491
+ this.#enclaveProvider,
492
+ this.blobGateway.url,
493
+ );
216
494
  }
217
495
 
218
496
  async hasProfile(): Promise<boolean> {
@@ -225,16 +503,20 @@ export class idOSClientWithUserSigner implements Omit<Properties<idOSClientIdle>
225
503
  userId: string,
226
504
  forceEncryptionPasswordStore?: EncryptionPasswordStore,
227
505
  ): Promise<PublicEncryptionProfile> {
228
- await this.enclaveProvider.reconfigure({
229
- mode: "new",
230
- userId,
231
- walletAddress: this.walletIdentifier,
232
- walletPublicKey: this.walletPublicKey,
233
- walletType: this.walletType,
234
- encryptionPasswordStore: forceEncryptionPasswordStore,
235
- expectedUserEncryptionPublicKey: undefined,
506
+ await exclusive(this.#kwilClient, async () => {
507
+ assertSigner(this.#kwilClient, this.kwilSigner);
508
+ await this.enclaveProvider.reconfigure({
509
+ mode: "new",
510
+ userId,
511
+ walletAddress: this.walletIdentifier,
512
+ walletPublicKey: this.walletPublicKey,
513
+ walletType: this.walletType,
514
+ encryptionPasswordStore: forceEncryptionPasswordStore,
515
+ expectedUserEncryptionPublicKey: undefined,
516
+ });
517
+ assertSigner(this.#kwilClient, this.kwilSigner);
236
518
  });
237
-
519
+ assertSigner(this.#kwilClient, this.kwilSigner);
238
520
  return this.enclaveProvider.ensureUserEncryptionProfile();
239
521
  }
240
522
 
@@ -243,56 +525,133 @@ export class idOSClientWithUserSigner implements Omit<Properties<idOSClientIdle>
243
525
 
244
526
  const kwilUser = await getUser(this.kwilClient);
245
527
 
246
- await this.enclaveProvider.reconfigure({
247
- mode: "existing",
248
- userId: kwilUser.id,
249
- expectedUserEncryptionPublicKey: kwilUser.recipient_encryption_public_key,
250
- walletAddress: this.walletIdentifier,
251
- walletType: this.walletType,
252
- encryptionPasswordStore: kwilUser.encryption_password_store,
253
- walletPublicKey: this.walletPublicKey,
528
+ return exclusive(this.#kwilClient, async () => {
529
+ assertSigner(this.#kwilClient, this.kwilSigner);
530
+ await this.enclaveProvider.reconfigure({
531
+ mode: "existing",
532
+ userId: kwilUser.id,
533
+ expectedUserEncryptionPublicKey: kwilUser.recipient_encryption_public_key,
534
+ walletAddress: this.walletIdentifier,
535
+ walletType: this.walletType,
536
+ encryptionPasswordStore: kwilUser.encryption_password_store,
537
+ walletPublicKey: this.walletPublicKey,
538
+ });
539
+ assertSigner(this.#kwilClient, this.kwilSigner);
540
+ return new idOSClientLoggedIn(this, kwilUser);
254
541
  });
255
-
256
- return new idOSClientLoggedIn(this, kwilUser);
257
542
  }
258
543
  }
259
544
 
260
545
  export class idOSClientLoggedIn implements Omit<Properties<idOSClientWithUserSigner>, "state"> {
261
546
  readonly state: "logged-in";
262
547
  readonly store: Store;
263
- readonly kwilClient: KwilActionClient;
264
- readonly enclaveProvider: BaseProvider;
548
+ readonly #kwilClient: KwilActionClient;
549
+ readonly #enclaveProvider: BaseProvider;
265
550
  readonly signer: Wallet;
266
551
  readonly kwilSigner: KwilSigner;
267
552
  readonly walletIdentifier: string;
268
553
  readonly walletPublicKey: string | undefined;
269
554
  readonly walletType: WalletType;
270
555
  readonly user: idOSUser;
556
+ readonly blobGateway: BlobGateway;
271
557
 
272
558
  constructor(idOSClientWithUserSigner: idOSClientWithUserSigner, user: idOSUser) {
273
559
  this.state = "logged-in";
274
560
  this.store = idOSClientWithUserSigner.store;
275
- this.kwilClient = idOSClientWithUserSigner.kwilClient;
276
- this.enclaveProvider = idOSClientWithUserSigner.enclaveProvider;
561
+ this.#kwilClient = sharedKwilClient(idOSClientWithUserSigner.kwilClient);
562
+ this.#enclaveProvider = sharedEnclave(idOSClientWithUserSigner.enclaveProvider);
277
563
  this.signer = idOSClientWithUserSigner.signer;
278
564
  this.kwilSigner = idOSClientWithUserSigner.kwilSigner;
279
565
  this.walletIdentifier = idOSClientWithUserSigner.walletIdentifier;
280
566
  this.walletPublicKey = idOSClientWithUserSigner.walletPublicKey;
281
567
  this.walletType = idOSClientWithUserSigner.walletType;
282
568
  this.user = user;
569
+ this.blobGateway = idOSClientWithUserSigner.blobGateway;
570
+ }
571
+
572
+ get kwilClient(): KwilActionClient {
573
+ return boundKwilClient(this.#kwilClient, this.kwilSigner);
574
+ }
575
+
576
+ get enclaveProvider(): BaseProvider {
577
+ return boundEnclave(this.#enclaveProvider, this.#kwilClient, this.kwilSigner);
283
578
  }
284
579
 
285
580
  async logOut(): Promise<idOSClientIdle> {
286
- this.kwilClient.setSigner(undefined);
287
- return new idOSClientIdle(this.store, this.kwilClient, this.enclaveProvider);
581
+ return finishLogOut(
582
+ this.store,
583
+ this.#kwilClient,
584
+ this.kwilSigner,
585
+ this.#enclaveProvider,
586
+ this.blobGateway.url,
587
+ );
288
588
  }
289
589
 
290
590
  async requestDWGMessage(params: idOSDelegatedWriteGrant): Promise<string> {
291
591
  return dwgMessage(this.kwilClient, params).then((res) => res.message);
292
592
  }
293
593
 
594
+ async createCredential(
595
+ publicNotes: string,
596
+ plaintextContent: Uint8Array<ArrayBufferLike>,
597
+ // In case of credential creation on users behalf, this would be a issuer key
598
+ issuerSigningSecretKey?: Uint8Array,
599
+ ): Promise<Omit<idOSCredential, "user_id" | "content" | "inserter_type" | "inserter_id">> {
600
+ const credentialId = crypto.randomUUID();
601
+ const preliminaryCredential: PreliminaryIDOSCredential = {
602
+ ...(await buildPreliminaryIDOSCredential({
603
+ publicNotes,
604
+ plaintextContent,
605
+ recipientEncryptionPublicKey: base64Decode(this.user.recipient_encryption_public_key),
606
+ issuerSigningSecretKey,
607
+ contentUri: contentUriForWallet(this.walletType, this.kwilSigner, credentialId),
608
+ })),
609
+ id: credentialId,
610
+ };
611
+ requireAccessTokenForUkycContent(
612
+ preliminaryCredential.contentUri,
613
+ this.blobGateway.hasAccessToken,
614
+ );
615
+
616
+ const requestId = crypto.randomUUID();
617
+
618
+ const payload: CreatePreliminaryCredentialInput = {
619
+ request_id: requestId,
620
+ credential_id: preliminaryCredential.id,
621
+ issuer_auth_public_key: preliminaryCredential.issuerAuthPublicKey,
622
+ encryptor_public_key: preliminaryCredential.encryptorPublicKey,
623
+ content_uri: preliminaryCredential.contentUri,
624
+ content_size: preliminaryCredential.contentSize,
625
+ content_hash: hexEncodeSha256Hash(plaintextContent),
626
+ public_notes: preliminaryCredential.publicNotes,
627
+ public_notes_signature: preliminaryCredential.publicNotesSignature,
628
+ broader_signature: preliminaryCredential.broaderSignature,
629
+ };
630
+
631
+ await createPreliminaryCredential(this.kwilClient, payload);
632
+
633
+ await this.blobGateway.uploadCredentialBlobs({
634
+ requestId,
635
+ original: preliminaryCredential.encryptedContent,
636
+ });
637
+
638
+ return mapPreliminaryToIDOSCredential(preliminaryCredential);
639
+ }
640
+
294
641
  async removeCredential(id: string): Promise<{ id: string }> {
642
+ const credential = await this.getCredentialById(id);
643
+ invariant(credential, `"idOSCredential" with id ${id} not found`);
644
+
645
+ const contentUri = credential.content_uri ?? null;
646
+ requireAccessTokenForUkycContent(contentUri, this.blobGateway.hasAccessToken);
647
+
295
648
  await removeCredential(this.kwilClient, { id });
649
+
650
+ if (!contentUri) {
651
+ return { id };
652
+ }
653
+
654
+ await this.blobGateway.deleteCredentialBlob({ credentialId: id });
296
655
  return { id };
297
656
  }
298
657
 
@@ -300,8 +659,10 @@ export class idOSClientLoggedIn implements Omit<Properties<idOSClientWithUserSig
300
659
  return getCredentialOwned(this.kwilClient, { id }).then((res) => res[0]);
301
660
  }
302
661
 
303
- async shareCredential(credential: ShareCredentialInput): Promise<ShareCredentialInput> {
304
- await shareCredential(this.kwilClient, credential);
662
+ async shareCredential(
663
+ credential: SharePreliminaryCredentialInput,
664
+ ): Promise<SharePreliminaryCredentialInput> {
665
+ await sharePreliminaryCredential(this.kwilClient, credential);
305
666
  return credential;
306
667
  }
307
668
 
@@ -326,13 +687,7 @@ export class idOSClientLoggedIn implements Omit<Properties<idOSClientWithUserSig
326
687
  const credential = await this.getCredentialById(id);
327
688
 
328
689
  invariant(credential, `"idOSCredential" with id ${id} not found`);
329
-
330
- await this.enclaveProvider.ensureUserEncryptionProfile();
331
-
332
- const plaintext = await this.enclaveProvider.decrypt(
333
- base64Decode(credential.content),
334
- base64Decode(credential.encryptor_public_key),
335
- );
690
+ const plaintext = await this.#decryptCredentialContent(credential);
336
691
 
337
692
  return hexEncodeSha256Hash(plaintext);
338
693
  }
@@ -341,19 +696,24 @@ export class idOSClientLoggedIn implements Omit<Properties<idOSClientWithUserSig
341
696
  const credential = await this.getCredentialById(id);
342
697
 
343
698
  invariant(credential, `"idOSCredential" with id ${id} not found`);
344
-
345
- await this.enclaveProvider.ensureUserEncryptionProfile();
346
-
347
- const plaintext = await this.enclaveProvider.decrypt(
348
- base64Decode(credential.content),
349
- base64Decode(credential.encryptor_public_key),
350
- );
699
+ const plaintext = await this.#decryptCredentialContent(credential);
351
700
 
352
701
  return utf8Decode(plaintext);
353
702
  }
354
703
 
355
- async requestDAGMessage(params: DagMessageInput): Promise<string> {
356
- return dagMessage(this.kwilClient, params).then((res) => res.message);
704
+ async getCredentialWithEncryptedContent(id: string): Promise<idOSCredential> {
705
+ const credential = await this.getCredentialById(id);
706
+ invariant(credential, `"idOSCredential" with id ${id} not found`);
707
+ return this.#credentialWithInlineContent(credential);
708
+ }
709
+
710
+ async getCredentialSharedContent(id: string): Promise<string> {
711
+ const credential = await this.getCredentialShared(id);
712
+
713
+ invariant(credential, `Shared credential with id ${id} not found`);
714
+ const plaintext = await this.#decryptCredentialContent(credential);
715
+
716
+ return utf8Decode(plaintext);
357
717
  }
358
718
 
359
719
  async getGrants(
@@ -385,6 +745,11 @@ export class idOSClientLoggedIn implements Omit<Properties<idOSClientWithUserSig
385
745
  return { id };
386
746
  }
387
747
 
748
+ async addWalletMessage(params: AddWalletMessageInput): Promise<string> {
749
+ const result = await requestAddWalletMessage(this.kwilClient, params);
750
+ return result.message;
751
+ }
752
+
388
753
  async addWallet(params: AddWalletInput): Promise<AddWalletInput> {
389
754
  await addWallet(this.kwilClient, params);
390
755
 
@@ -485,8 +850,8 @@ export class idOSClientLoggedIn implements Omit<Properties<idOSClientWithUserSig
485
850
  omit: Parameters<typeof recordFilter>[2];
486
851
  };
487
852
  credentialLevelOrHigherFilter?: {
488
- userLevel: "basic" | "plus";
489
- requiredAddons: ("liveness" | "email" | "phoneNumber")[];
853
+ userLevel: BaseLevel;
854
+ requiredAddons: Addon[];
490
855
  };
491
856
  }): Promise<idOSCredentialListItem[]> {
492
857
  const credentials = await this.getAllCredentials();
@@ -563,7 +928,9 @@ export class idOSClientLoggedIn implements Omit<Properties<idOSClientWithUserSig
563
928
  );
564
929
 
565
930
  const matches = await this.enclaveProvider.filterCredentials(
566
- fullCredentials,
931
+ await Promise.all(
932
+ fullCredentials.map((credential) => this.#credentialWithInlineContent(credential)),
933
+ ),
567
934
  privateFieldFilters,
568
935
  );
569
936
 
@@ -586,49 +953,90 @@ export class idOSClientLoggedIn implements Omit<Properties<idOSClientWithUserSig
586
953
  consumerAuthPublicKey: string;
587
954
  lockedUntil?: number;
588
955
  },
589
- ): Promise<ShareCredentialInput> {
956
+ ): Promise<SharePreliminaryCredentialInput> {
590
957
  const credential = await this.getCredentialById(credentialId);
591
- const contentHash = await this.getCredentialContentSha256Hash(credentialId);
592
958
 
593
959
  invariant(credential, `"idOSCredential" with id ${credentialId} not found`);
594
960
 
595
- const plaintextContent = utf8Decode(
596
- await this.enclaveProvider.decrypt(
597
- base64Decode(credential.content),
598
- base64Decode(credential.encryptor_public_key),
599
- ),
600
- );
961
+ const plaintextContent = await this.#decryptCredentialContent(credential);
962
+ const contentHash = hexEncodeSha256Hash(plaintextContent);
601
963
 
602
964
  await this.enclaveProvider.ensureUserEncryptionProfile();
603
965
 
604
966
  const { content, encryptorPublicKey } = await this.enclaveProvider.encrypt(
605
- utf8Encode(plaintextContent),
967
+ plaintextContent,
606
968
  base64Decode(consumerEncryptionPublicKey),
607
969
  );
608
970
 
609
- const insertableCredential: ShareCredentialInput = {
610
- ...credential,
611
- ...buildInsertableIDOSCredential(
612
- credential.user_id,
613
- "",
614
- base64Encode(content),
615
- base64Encode(encryptorPublicKey),
616
- ),
617
- original_credential_id: credential.id,
618
- id: crypto.randomUUID(),
619
- grantee_wallet_identifier: consumerAuthPublicKey,
971
+ // User issues the shared copy: sign content_uri with an ephemeral user-side key.
972
+ // Copy issuer_auth_public_key need not match the original credential's issuer.
973
+ const copyId = crypto.randomUUID();
974
+ const ukycContentUri = contentUriForWallet(this.walletType, this.kwilSigner, copyId);
975
+ requireAccessTokenForUkycContent(ukycContentUri, this.blobGateway.hasAccessToken);
976
+ const copyReference = ukycContentUri
977
+ ? { uri: ukycContentUri, size: content.byteLength }
978
+ : await createBlobContentReference(content);
979
+ const signedReference = buildEphemeralSignedCredentialContentReference("", copyReference.uri);
980
+
981
+ // Integrator-supplied. ed25519 @caller is lowercase hex; after exact grantee
982
+ // matching, uppercase hex would store a grant the consumer cannot read. Other
983
+ // identifier encodings (NEAR, XRPL, Stellar, MM) are case-sensitive, so leave them.
984
+ const granteeWalletIdentifier = /^[0-9a-fA-F]{64}$/.test(consumerAuthPublicKey)
985
+ ? consumerAuthPublicKey.toLowerCase()
986
+ : consumerAuthPublicKey;
987
+
988
+ const preliminaryCredential: SharePreliminaryCredentialInput = {
989
+ ...signedReference,
990
+ request_id: crypto.randomUUID(),
991
+ copy_id: copyId,
992
+ original_id: credential.id,
993
+ content_uri: copyReference.uri,
994
+ content_size: copyReference.size,
995
+ encryptor_public_key: base64Encode(encryptorPublicKey),
996
+ grantee_wallet_identifier: granteeWalletIdentifier,
620
997
  locked_until: lockedUntil,
621
998
  content_hash: contentHash,
622
999
  };
623
1000
 
624
- await this.shareCredential(insertableCredential);
1001
+ await this.shareCredential(preliminaryCredential);
1002
+ await this.blobGateway.uploadCredentialBlobs({
1003
+ requestId: preliminaryCredential.request_id,
1004
+ copy: content,
1005
+ });
1006
+
1007
+ return preliminaryCredential;
1008
+ }
1009
+
1010
+ async #credentialWithInlineContent(credential: idOSCredential): Promise<idOSCredential> {
1011
+ if (credential.content) {
1012
+ return credential;
1013
+ }
1014
+
1015
+ return {
1016
+ ...credential,
1017
+ content: base64Encode(await this.#getCredentialEncryptedContent(credential)),
1018
+ content_uri: null,
1019
+ content_size: null,
1020
+ };
1021
+ }
1022
+
1023
+ async #decryptCredentialContent(credential: idOSCredential): Promise<Uint8Array> {
1024
+ await this.enclaveProvider.ensureUserEncryptionProfile();
1025
+
1026
+ return this.enclaveProvider.decrypt(
1027
+ await this.#getCredentialEncryptedContent(credential),
1028
+ base64Decode(credential.encryptor_public_key),
1029
+ );
1030
+ }
625
1031
 
626
- return insertableCredential;
1032
+ async #getCredentialEncryptedContent(credential: idOSCredential): Promise<Uint8Array> {
1033
+ return resolveCredentialEncryptedContent(credential, this.blobGateway);
627
1034
  }
628
1035
  }
629
1036
 
630
1037
  export type {
631
1038
  idOSDelegatedWriteGrant,
1039
+ DelegatedWriteGrantBaseParams,
632
1040
  idOSCredential,
633
1041
  idOSCredentialListItem,
634
1042
  idOSGrant,
@@ -637,14 +1045,21 @@ export type {
637
1045
  idOSWallet,
638
1046
  };
639
1047
 
640
- export { signNearMessage };
1048
+ export { signNearMessage, toDelegatedWriteGrantBaseParams };
641
1049
 
642
1050
  export function createIDOSClient(params: {
643
1051
  nodeUrl: string;
1052
+ /**
1053
+ * Optional blob gateway base URL. Defaults to `nodeUrl`.
1054
+ * In the browser this must share KGW's session-cookie Domain
1055
+ * (typically the same host), because reads use `credentials: "include"`.
1056
+ */
1057
+ blobGatewayUrl?: string;
644
1058
  enclaveOptions: Omit<IframeEnclave["options"], "mode">;
645
1059
  }): idOSClientConfiguration<IframeEnclave> {
646
1060
  return new idOSClientConfiguration({
647
1061
  nodeUrl: params.nodeUrl,
1062
+ blobGatewayUrl: params.blobGatewayUrl,
648
1063
  enclaveOptions: params.enclaveOptions,
649
1064
  });
650
1065
  }