@idos-network/kwil-infra 4.0.0 → 5.0.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.
@@ -435,6 +435,100 @@ export const actionSchema: Record<string, ActionSchemaElement[]> = {
435
435
  type: DataType.Text,
436
436
  },
437
437
  ],
438
+ get_user_as_mm_operator: [
439
+ {
440
+ name: "storage_token",
441
+ type: DataType.Text,
442
+ },
443
+ ],
444
+ create_user_as_mm_operator: [
445
+ {
446
+ name: "id",
447
+ type: DataType.Uuid,
448
+ },
449
+ {
450
+ name: "recipient_encryption_public_key",
451
+ type: DataType.Text,
452
+ },
453
+ {
454
+ name: "wallet_id",
455
+ type: DataType.Uuid,
456
+ },
457
+ {
458
+ name: "storage_token",
459
+ type: DataType.Text,
460
+ },
461
+ ],
462
+ get_credentials_as_mm_operator: [
463
+ {
464
+ name: "user_id",
465
+ type: DataType.Uuid,
466
+ },
467
+ {
468
+ name: "storage_token",
469
+ type: DataType.Text,
470
+ },
471
+ ],
472
+ create_preliminary_credential_as_mm_operator: [
473
+ {
474
+ name: "request_id",
475
+ type: DataType.Uuid,
476
+ },
477
+ {
478
+ name: "user_id",
479
+ type: DataType.Uuid,
480
+ },
481
+ {
482
+ name: "credential_id",
483
+ type: DataType.Uuid,
484
+ },
485
+ {
486
+ name: "issuer_auth_public_key",
487
+ type: DataType.Text,
488
+ },
489
+ {
490
+ name: "encryptor_public_key",
491
+ type: DataType.Text,
492
+ },
493
+ {
494
+ name: "content_uri",
495
+ type: DataType.Text,
496
+ },
497
+ {
498
+ name: "content_size",
499
+ type: DataType.Int,
500
+ },
501
+ {
502
+ name: "content_hash",
503
+ type: DataType.Text,
504
+ },
505
+ {
506
+ name: "public_notes",
507
+ type: DataType.Text,
508
+ },
509
+ {
510
+ name: "public_notes_signature",
511
+ type: DataType.Text,
512
+ },
513
+ {
514
+ name: "broader_signature",
515
+ type: DataType.Text,
516
+ },
517
+ {
518
+ name: "storage_token",
519
+ type: DataType.Text,
520
+ },
521
+ ],
522
+ request_user_wipe_as_mm_operator: [
523
+ {
524
+ name: "user_id",
525
+ type: DataType.Uuid,
526
+ },
527
+ {
528
+ name: "storage_token",
529
+ type: DataType.Text,
530
+ },
531
+ ],
438
532
  add_attribute_as_inserter: [
439
533
  {
440
534
  name: "id",
@@ -770,33 +864,27 @@ export const UpsertWalletAsInserterInputSchema: z.ZodObject<{
770
864
  user_id: z.ZodUUID;
771
865
  address: z.ZodString;
772
866
  public_key: z.ZodOptional<z.ZodNullable<z.ZodString>>;
773
- wallet_type: z.ZodType<WalletType>;
774
- not_before: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
775
- not_after: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
867
+ wallet_type: z.ZodType<AddWalletType>;
868
+ not_before: z.ZodISODateTime;
869
+ not_after: z.ZodISODateTime;
776
870
  signature: z.ZodString;
777
- }> = z
778
- .object({
779
- id: z.uuid(),
780
- user_id: z.uuid(),
781
- address: z.string(),
782
- public_key: z.string().nullish(),
783
- wallet_type: walletTypeSchema,
784
- not_before: rfc3339DateTimeSchema.nullish(),
785
- not_after: rfc3339DateTimeSchema.nullish(),
786
- signature: z.string(),
787
- })
788
- .refine((v) => (v.wallet_type === "MM") === (v.not_before == null), {
789
- path: ["not_before"],
790
- message: "not_before must be null when wallet_type is MM, and set otherwise",
791
- })
792
- .refine((v) => (v.wallet_type === "MM") === (v.not_after == null), {
793
- path: ["not_after"],
794
- message: "not_after must be null when wallet_type is MM, and set otherwise",
795
- });
871
+ }> = z.object({
872
+ id: z.uuid(),
873
+ user_id: z.uuid(),
874
+ address: z.string(),
875
+ public_key: z.string().nullish(),
876
+ wallet_type: addWalletTypeSchema,
877
+ not_before: rfc3339DateTimeSchema,
878
+ not_after: rfc3339DateTimeSchema,
879
+ signature: z.string(),
880
+ });
796
881
 
797
882
  export type UpsertWalletAsInserterInput = z.infer<typeof UpsertWalletAsInserterInputSchema>;
798
883
 
799
- /** WALLET ACTIONS */
884
+ /**
885
+ * WALLET ACTIONS
886
+ * Existing MM wallets count too: a key can't be both an MM wallet and another wallet.
887
+ */
800
888
  export async function upsertWalletAsInserter(
801
889
  kwilClient: KwilActionClient,
802
890
  params: UpsertWalletAsInserterInput,
@@ -918,6 +1006,10 @@ export const RemoveWalletInputSchema: z.ZodObject<{
918
1006
 
919
1007
  export type RemoveWalletInput = z.infer<typeof RemoveWalletInputSchema>;
920
1008
 
1009
+ /**
1010
+ * A user may have an MM wallet next to others (created before MM operators). Without it
1011
+ * the user is no longer an MM user, and its ukyc:// credentials could never be wiped.
1012
+ */
921
1013
  export async function removeWallet(
922
1014
  kwilClient: KwilActionClient,
923
1015
  params: RemoveWalletInput,
@@ -995,6 +1087,10 @@ export type CreatePreliminaryCredentialInput = z.infer<
995
1087
  typeof CreatePreliminaryCredentialInputSchema
996
1088
  >;
997
1089
 
1090
+ /**
1091
+ * Refused for MM users, also when signed in with another wallet: their MM operator stages
1092
+ * their credentials (create_preliminary_credential_as_mm_operator), with their storage token.
1093
+ */
998
1094
  export async function createPreliminaryCredential(
999
1095
  kwilClient: KwilActionClient,
1000
1096
  params: CreatePreliminaryCredentialInput,
@@ -1194,6 +1290,7 @@ export const SharePreliminaryCredentialInputSchema: z.ZodObject<{
1194
1290
 
1195
1291
  export type SharePreliminaryCredentialInput = z.infer<typeof SharePreliminaryCredentialInputSchema>;
1196
1292
 
1293
+ /** Refused for MM users, also when signed in with another wallet: their credentials can't be shared. */
1197
1294
  export async function sharePreliminaryCredential(
1198
1295
  kwilClient: KwilActionClient,
1199
1296
  params: SharePreliminaryCredentialInput,
@@ -1266,6 +1363,7 @@ export type CreatePreliminaryCredentialsByDwgInput = z.infer<
1266
1363
  * Delegated write credential actions
1267
1364
  * For access grant
1268
1365
  * We will use the same ids for preliminary credentials as for credentials, so we need to check if they already exist
1366
+ * MM users are managed by their MM operator only.
1269
1367
  * We capture gas upfront to prevent the real credential from being created if the gas is not enough
1270
1368
  * The gas can be refunded fully or partially
1271
1369
  * Temporary: an issuer is not an inserter/caller. A user grants a DWG to an issuer_key;
@@ -1445,6 +1543,198 @@ export async function getSiblingCredentialId(
1445
1543
  .then((result) => result[0]);
1446
1544
  }
1447
1545
 
1546
+ export const GetUserAsMmOperatorInputSchema: z.ZodObject<{
1547
+ storage_token: z.ZodString;
1548
+ }> = z.object({
1549
+ storage_token: z.string(),
1550
+ });
1551
+
1552
+ export type GetUserAsMmOperatorInput = z.infer<typeof GetUserAsMmOperatorInputSchema>;
1553
+
1554
+ export const GetUserAsMmOperatorOutputSchema: z.ZodObject<{
1555
+ id: z.ZodUUID;
1556
+ recipient_encryption_public_key: z.ZodString;
1557
+ frozen: z.ZodBoolean;
1558
+ }> = z.object({
1559
+ id: z.uuid(),
1560
+ recipient_encryption_public_key: z.string(),
1561
+ frozen: z.boolean(),
1562
+ });
1563
+
1564
+ export type GetUserAsMmOperatorOutput = z.infer<typeof GetUserAsMmOperatorOutputSchema>;
1565
+
1566
+ /**
1567
+ * MM OPERATOR ACTIONS
1568
+ * An MM operator (see mm_operators) creates MM users, stages their ukyc:// credentials and
1569
+ * freezes them for a data wipe. It only acts on the MM users it created, and every action
1570
+ * also needs a valid UKYC storage token ($storage_token, the bare base64url envelope) minted
1571
+ * by the user's app: the operator key alone can't do anything to an MM user. Tokens are
1572
+ * verified at @block_timestamp (the last block's time in view calls).
1573
+ */
1574
+ export async function getUserAsMmOperator(
1575
+ kwilClient: KwilActionClient,
1576
+ params: GetUserAsMmOperatorInput,
1577
+ ): Promise<GetUserAsMmOperatorOutput[]> {
1578
+ const inputs = GetUserAsMmOperatorInputSchema.parse(params);
1579
+ return await kwilClient.call<GetUserAsMmOperatorOutput[]>({
1580
+ name: "get_user_as_mm_operator",
1581
+ inputs,
1582
+ });
1583
+ }
1584
+
1585
+ export const CreateUserAsMmOperatorInputSchema: z.ZodObject<{
1586
+ id: z.ZodUUID;
1587
+ recipient_encryption_public_key: z.ZodString;
1588
+ wallet_id: z.ZodUUID;
1589
+ storage_token: z.ZodString;
1590
+ }> = z.object({
1591
+ id: z.uuid(),
1592
+ recipient_encryption_public_key: z.string(),
1593
+ wallet_id: z.uuid(),
1594
+ storage_token: z.string(),
1595
+ });
1596
+
1597
+ export type CreateUserAsMmOperatorInput = z.infer<typeof CreateUserAsMmOperatorInputSchema>;
1598
+
1599
+ /** The MM wallet key is the storage token's signing key (write token). */
1600
+ export async function createUserAsMmOperator(
1601
+ kwilClient: KwilActionClient,
1602
+ params: CreateUserAsMmOperatorInput,
1603
+ ): Promise<void> {
1604
+ const inputs = CreateUserAsMmOperatorInputSchema.parse(params);
1605
+ await kwilClient.execute({
1606
+ name: "create_user_as_mm_operator",
1607
+ inputs,
1608
+ description: "Create an MM user with its MM wallet as an MM operator",
1609
+ });
1610
+ }
1611
+
1612
+ export const GetCredentialsAsMmOperatorInputSchema: z.ZodObject<{
1613
+ user_id: z.ZodUUID;
1614
+ storage_token: z.ZodString;
1615
+ }> = z.object({
1616
+ user_id: z.uuid(),
1617
+ storage_token: z.string(),
1618
+ });
1619
+
1620
+ export type GetCredentialsAsMmOperatorInput = z.infer<typeof GetCredentialsAsMmOperatorInputSchema>;
1621
+
1622
+ export const GetCredentialsAsMmOperatorOutputSchema: z.ZodObject<{
1623
+ id: z.ZodUUID;
1624
+ user_id: z.ZodUUID;
1625
+ public_notes: z.ZodString;
1626
+ content_uri: z.ZodString;
1627
+ content_size: z.ZodNumber;
1628
+ encryptor_public_key: z.ZodString;
1629
+ issuer_auth_public_key: z.ZodString;
1630
+ inserter_type: z.ZodString;
1631
+ inserter_id: z.ZodString;
1632
+ }> = z.object({
1633
+ id: z.uuid(),
1634
+ user_id: z.uuid(),
1635
+ public_notes: z.string(),
1636
+ content_uri: z.string(),
1637
+ content_size: z.number(),
1638
+ encryptor_public_key: z.string(),
1639
+ issuer_auth_public_key: z.string(),
1640
+ inserter_type: z.string(),
1641
+ inserter_id: z.string(),
1642
+ });
1643
+
1644
+ export type GetCredentialsAsMmOperatorOutput = z.infer<
1645
+ typeof GetCredentialsAsMmOperatorOutputSchema
1646
+ >;
1647
+
1648
+ /** Needs a read token of the user for the user's storage. */
1649
+ export async function getCredentialsAsMmOperator(
1650
+ kwilClient: KwilActionClient,
1651
+ params: GetCredentialsAsMmOperatorInput,
1652
+ ): Promise<GetCredentialsAsMmOperatorOutput[]> {
1653
+ const inputs = GetCredentialsAsMmOperatorInputSchema.parse(params);
1654
+ return await kwilClient.call<GetCredentialsAsMmOperatorOutput[]>({
1655
+ name: "get_credentials_as_mm_operator",
1656
+ inputs,
1657
+ });
1658
+ }
1659
+
1660
+ export const CreatePreliminaryCredentialAsMmOperatorInputSchema: z.ZodObject<{
1661
+ request_id: z.ZodUUID;
1662
+ user_id: z.ZodUUID;
1663
+ credential_id: z.ZodUUID;
1664
+ issuer_auth_public_key: z.ZodString;
1665
+ encryptor_public_key: z.ZodString;
1666
+ content_uri: z.ZodString;
1667
+ content_size: z.ZodNumber;
1668
+ content_hash: z.ZodString;
1669
+ public_notes: z.ZodString;
1670
+ public_notes_signature: z.ZodString;
1671
+ broader_signature: z.ZodString;
1672
+ storage_token: z.ZodString;
1673
+ }> = z.object({
1674
+ request_id: z.uuid(),
1675
+ user_id: z.uuid(),
1676
+ credential_id: z.uuid(),
1677
+ issuer_auth_public_key: z.string(),
1678
+ encryptor_public_key: z.string(),
1679
+ content_uri: blobContentUriSchema,
1680
+ content_size: contentSizeSchema,
1681
+ content_hash: z.string(),
1682
+ public_notes: z.string(),
1683
+ public_notes_signature: z.string(),
1684
+ broader_signature: z.string(),
1685
+ storage_token: z.string(),
1686
+ });
1687
+
1688
+ export type CreatePreliminaryCredentialAsMmOperatorInput = z.infer<
1689
+ typeof CreatePreliminaryCredentialAsMmOperatorInputSchema
1690
+ >;
1691
+
1692
+ /**
1693
+ * Like create_preliminary_credential, for an MM user. The issuer signs with the user's MM
1694
+ * wallet public key as the recipient wallet. Needs a write token of the user; the content URI's
1695
+ * storage must be the token's storage and the user's storage (the first credential defines it).
1696
+ */
1697
+ export async function createPreliminaryCredentialAsMmOperator(
1698
+ kwilClient: KwilActionClient,
1699
+ params: CreatePreliminaryCredentialAsMmOperatorInput,
1700
+ ): Promise<void> {
1701
+ const inputs = CreatePreliminaryCredentialAsMmOperatorInputSchema.parse(params);
1702
+ await kwilClient.execute({
1703
+ name: "create_preliminary_credential_as_mm_operator",
1704
+ inputs,
1705
+ description: "Create a preliminary ukyc:// credential for an MM user as its MM operator",
1706
+ });
1707
+ }
1708
+
1709
+ export const RequestUserWipeAsMmOperatorInputSchema: z.ZodObject<{
1710
+ user_id: z.ZodUUID;
1711
+ storage_token: z.ZodString;
1712
+ }> = z.object({
1713
+ user_id: z.uuid(),
1714
+ storage_token: z.string(),
1715
+ });
1716
+
1717
+ export type RequestUserWipeAsMmOperatorInput = z.infer<
1718
+ typeof RequestUserWipeAsMmOperatorInputSchema
1719
+ >;
1720
+
1721
+ /**
1722
+ * Freezes the MM user: no new credentials, no reads, no finalize. The gateway deletes the
1723
+ * user once the UKYC storage is deleted (finalize_mm_user_wipe_as_gateway). No cancel.
1724
+ * Needs a delete token of the user for the user's storage, also when already requested.
1725
+ */
1726
+ export async function requestUserWipeAsMmOperator(
1727
+ kwilClient: KwilActionClient,
1728
+ params: RequestUserWipeAsMmOperatorInput,
1729
+ ): Promise<void> {
1730
+ const inputs = RequestUserWipeAsMmOperatorInputSchema.parse(params);
1731
+ await kwilClient.execute({
1732
+ name: "request_user_wipe_as_mm_operator",
1733
+ inputs,
1734
+ description: "Request a data wipe of an MM user as its MM operator",
1735
+ });
1736
+ }
1737
+
1448
1738
  export const AddAttributeAsInserterInputSchema: z.ZodObject<{
1449
1739
  id: z.ZodUUID;
1450
1740
  user_id: z.ZodUUID;
@@ -1,7 +1,6 @@
1
1
  // @vitest-environment node
2
2
 
3
3
  import { type KwilSigner, NodeKwil } from "@idos-network/kwil-js";
4
- import { base64UrlEncode, utf8Encode } from "@idos-network/utils/codecs";
5
4
  import { describe, expect, it, vi } from "vitest";
6
5
 
7
6
  import {
@@ -9,19 +8,6 @@ import {
9
8
  createKgwAuthenticatedFetch,
10
9
  } from "./create-kgw-authenticated-fetch";
11
10
  import { KwilActionClient } from "./create-kwil-client";
12
- import { createMmTokenAuth } from "./mm-token/create-mm-token-kwil-signer";
13
-
14
- const mmEnvelopeToken = base64UrlEncode(
15
- utf8Encode(
16
- JSON.stringify({
17
- payload: {
18
- signing_public_key: base64UrlEncode(new Uint8Array(32).fill(7)),
19
- storage_id: "storage-abc",
20
- },
21
- signature: base64UrlEncode(new Uint8Array(64).fill(9)),
22
- }),
23
- ),
24
- );
25
11
 
26
12
  function createTestClient(
27
13
  initialCookie?: string,
@@ -279,6 +265,33 @@ describe("createKgwAuthenticatedFetch", () => {
279
265
  expect(cookieHeader(fetchFn.mock.calls[0])).toBe("kgw_session=valid; Path=/");
280
266
  });
281
267
 
268
+ it.each(["MM_TOKEN_INVALID", "MM_TOKEN_EXPIRED"])(
269
+ "does not reauthenticate when blob-gateway rejects the storage token (%s)",
270
+ async (code) => {
271
+ const { authFetch, fetchFn, refresh } = createTestClient("kgw_session=valid; Path=/");
272
+ fetchFn.mockResolvedValueOnce(Response.json({ error: "token", code }, { status: 401 }));
273
+
274
+ const response = await authFetch("https://blob.example/blob/v1/storage");
275
+
276
+ expect(response.status).toBe(401);
277
+ await expect(response.json()).resolves.toMatchObject({ code });
278
+ expect(refresh).not.toHaveBeenCalled();
279
+ expect(fetchFn).toHaveBeenCalledTimes(1);
280
+ },
281
+ );
282
+
283
+ it("logs in to KGW again when blob-gateway reports a missing session", async () => {
284
+ const { authFetch, fetchFn, refresh } = createTestClient("kgw_session=expired; Path=/");
285
+ fetchFn.mockResolvedValueOnce(
286
+ Response.json({ error: "session", code: "WIPE_UNAUTHORIZED" }, { status: 401 }),
287
+ );
288
+
289
+ await authFetch("https://blob.example/blob/v1/storage", { method: "DELETE" });
290
+
291
+ expect(refresh).toHaveBeenCalledTimes(1);
292
+ expect(fetchFn).toHaveBeenCalledTimes(2);
293
+ });
294
+
282
295
  it("shares one refresh across parallel auth failures", async () => {
283
296
  const { authFetch, fetchFn, refresh } = createTestClient("kgw_session=expired; Path=/");
284
297
  fetchFn
@@ -347,17 +360,7 @@ describe("createKgwAuthenticatedBlobGateway", () => {
347
360
  return new KwilActionClient(nodeKwil);
348
361
  }
349
362
 
350
- it("infers UKYC authorization from an MM signer", () => {
351
- const gateway = createKgwAuthenticatedBlobGateway({
352
- url: "https://blob.example",
353
- kwilClient: kgwSessionClient(),
354
- signer: createMmTokenAuth(mmEnvelopeToken),
355
- });
356
-
357
- expect(gateway.hasAccessToken).toBe(true);
358
- });
359
-
360
- it("accepts an explicit MM authority for a non-MM signer, and none otherwise", () => {
363
+ it("sends a storage token only when one is given", () => {
361
364
  const kwilClient = kgwSessionClient();
362
365
  const signer = {} as KwilSigner;
363
366
 
@@ -366,7 +369,7 @@ describe("createKgwAuthenticatedBlobGateway", () => {
366
369
  url: "https://blob.example",
367
370
  kwilClient,
368
371
  signer,
369
- mmAuth: createMmTokenAuth(mmEnvelopeToken),
372
+ accessToken: "storage-token",
370
373
  }).hasAccessToken,
371
374
  ).toBe(true);
372
375
  expect(
@@ -374,4 +377,21 @@ describe("createKgwAuthenticatedBlobGateway", () => {
374
377
  .hasAccessToken,
375
378
  ).toBe(false);
376
379
  });
380
+
381
+ it("sends the KGW cookie and the storage token on the same request", async () => {
382
+ const fetchFn = vi.fn<typeof fetch>(async () => new Response(null, { status: 204 }));
383
+ const gateway = createKgwAuthenticatedBlobGateway({
384
+ url: "https://blob.example",
385
+ kwilClient: kgwSessionClient(),
386
+ signer: {} as KwilSigner,
387
+ fetchFn,
388
+ }).withAccessToken("storage-token");
389
+
390
+ await gateway.deleteStorage();
391
+
392
+ const [, init] = fetchFn.mock.calls[0];
393
+ const headers = new Headers(init?.headers);
394
+ expect(headers.get("Cookie")).toBe("kgw_session=valid; Path=/");
395
+ expect(headers.get("Authorization")).toBe("AccessToken storage-token");
396
+ });
377
397
  });
@@ -2,9 +2,6 @@ import { type KwilSigner, NodeKwil } from "@idos-network/kwil-js";
2
2
  import { BlobGateway, type BlobGatewayParams } from "@idos-network/utils/blob-gateway";
3
3
 
4
4
  import type { KwilActionClient } from "./create-kwil-client";
5
- import type { MmTokenAuth } from "./mm-token/create-mm-token-kwil-signer";
6
-
7
- import { isMmTokenAuth } from "./mm-token/create-mm-token-kwil-signer";
8
5
 
9
6
  const KGW_AUTH_ERROR_CODE = -901;
10
7
 
@@ -23,10 +20,10 @@ export type CreateKgwAuthenticatedBlobGatewayParams = CreateKgwAuthenticatedFetc
23
20
  url: string;
24
21
  maxFetchBytes?: BlobGatewayParams["maxFetchBytes"];
25
22
  /**
26
- * UKYC storage authority for blob requests. Defaults to `signer` when that signer
27
- * is itself an MM authentication object, so the capability is never passed twice.
23
+ * UKYC storage token, sent as `Authorization: AccessToken <token>` next to the KGW session
24
+ * cookie. Only MM operators use it; `BlobGateway.withAccessToken()` swaps it per request.
28
25
  */
29
- mmAuth?: MmTokenAuth;
26
+ accessToken?: string;
30
27
  };
31
28
 
32
29
  type KgwSessionNodeKwil = NodeKwil & {
@@ -37,16 +34,13 @@ type KgwSessionNodeKwil = NodeKwil & {
37
34
  export function createKgwAuthenticatedBlobGateway({
38
35
  url,
39
36
  maxFetchBytes,
40
- mmAuth,
37
+ accessToken,
41
38
  ...fetchParams
42
39
  }: CreateKgwAuthenticatedBlobGatewayParams): BlobGateway {
43
- const storageAuth =
44
- mmAuth ?? (isMmTokenAuth(fetchParams.signer) ? fetchParams.signer : undefined);
45
-
46
40
  return new BlobGateway({
47
41
  url,
48
42
  maxFetchBytes,
49
- accessToken: storageAuth?.accessToken,
43
+ accessToken,
50
44
  fetchFn: createKgwAuthenticatedFetch(fetchParams),
51
45
  });
52
46
  }
@@ -302,20 +296,22 @@ function withCookie(
302
296
  return [input, { ...init, headers }];
303
297
  }
304
298
 
305
- async function defaultKgwAuthFailurePredicate(response: Response): Promise<boolean> {
306
- if (response.status === 401) {
307
- return true;
308
- }
299
+ // blob-gateway also answers 401 for a bad UKYC storage token. A new KGW session doesn't fix that.
300
+ const STORAGE_TOKEN_AUTH_ERROR_CODES = new Set(["MM_TOKEN_INVALID", "MM_TOKEN_EXPIRED"]);
309
301
 
302
+ async function defaultKgwAuthFailurePredicate(response: Response): Promise<boolean> {
310
303
  const contentType = response.headers.get("content-type");
311
304
  if (!contentType?.includes("application/json")) {
312
- return false;
305
+ return response.status === 401;
313
306
  }
314
307
 
315
308
  try {
316
- const body = (await response.json()) as { error?: { code?: number } };
309
+ const body = (await response.json()) as { code?: unknown; error?: { code?: number } };
310
+ if (response.status === 401) {
311
+ return !(typeof body.code === "string" && STORAGE_TOKEN_AUTH_ERROR_CODES.has(body.code));
312
+ }
317
313
  return body.error?.code === KGW_AUTH_ERROR_CODE;
318
314
  } catch {
319
- return false;
315
+ return response.status === 401;
320
316
  }
321
317
  }
@@ -0,0 +1,40 @@
1
+ import type { NodeKwil } from "@idos-network/kwil-js";
2
+
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ import { KwilActionClient, KwilCallError } from "./create-kwil-client";
6
+
7
+ function clientAnswering(data: { result?: unknown[]; error?: string }) {
8
+ const call = vi.fn(async () => ({ status: 200, data }));
9
+ return new KwilActionClient({ call } as unknown as NodeKwil);
10
+ }
11
+
12
+ const params = { name: "get_user_as_mm_operator", inputs: { storage_token: "t" } } as const;
13
+
14
+ describe("KwilActionClient.call", () => {
15
+ it("resolves to an empty result for a failed view by default", async () => {
16
+ const client = clientAnswering({ result: [], error: "storage token: expired" });
17
+
18
+ await expect(client.call(params)).resolves.toEqual([]);
19
+ });
20
+
21
+ it("throws KwilCallError with the action error when throwOnCallError is set", async () => {
22
+ const client = clientAnswering({ result: [], error: "storage token: expired" });
23
+ client.throwOnCallError = true;
24
+
25
+ const error = await client.call(params).catch((e: unknown) => e);
26
+
27
+ expect(error).toBeInstanceOf(KwilCallError);
28
+ expect(error).toMatchObject({
29
+ actionName: "get_user_as_mm_operator",
30
+ message: "storage token: expired",
31
+ });
32
+ });
33
+
34
+ it("returns the rows of a successful view when throwOnCallError is set", async () => {
35
+ const client = clientAnswering({ result: [{ id: "1" }] });
36
+ client.throwOnCallError = true;
37
+
38
+ await expect(client.call(params)).resolves.toEqual([{ id: "1" }]);
39
+ });
40
+ });
@@ -36,12 +36,33 @@ type AllKwilExecuteAction = {
36
36
 
37
37
  type KwilExecuteActionRequestParams = AllKwilExecuteAction[ActionName];
38
38
 
39
+ /**
40
+ * A view action ran but failed (its `error()` or a runtime error). kwild reports that in the
41
+ * call response's `error` field, not as an HTTP/RPC error, so it is only thrown when
42
+ * `KwilActionClient.throwOnCallError` is set.
43
+ */
44
+ export class KwilCallError extends Error {
45
+ readonly name = "KwilCallError";
46
+
47
+ constructor(
48
+ readonly actionName: string,
49
+ message: string,
50
+ ) {
51
+ super(message);
52
+ }
53
+ }
54
+
39
55
  /**
40
56
  * A client for interacting with kwil with type-safe abstractions for `call` and `execute`.
41
57
  * Has utility methods for creating actions and setting a signer.
42
58
  */
43
59
  export class KwilActionClient {
44
60
  signer?: KwilSigner;
61
+ /**
62
+ * When true, `call` throws `KwilCallError` if the view action reports an error. When false
63
+ * (default) such a call resolves to its (empty) result, as it always has.
64
+ */
65
+ throwOnCallError = false;
45
66
  readonly client: NodeKwil | WebKwil;
46
67
 
47
68
  constructor(client: NodeKwil | WebKwil) {
@@ -81,6 +102,10 @@ export class KwilActionClient {
81
102
  };
82
103
 
83
104
  const response = await this.client.call(action as CallBody, signer);
105
+ const actionError = response?.data?.error;
106
+ if (this.throwOnCallError && actionError) {
107
+ throw new KwilCallError(params.name, actionError);
108
+ }
84
109
  return response?.data?.result as T;
85
110
  }
86
111