@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.
- package/CHANGELOG.md +25 -0
- package/README.md +1 -46
- package/dist/actions/index.d.mts +2 -2
- package/dist/actions/index.mjs +1 -1
- package/dist/actions/index.mjs.map +1 -1
- package/dist/{index-Dwonqvwu.d.mts → index-Dc_rffi1.d.mts} +119 -52
- package/dist/index-Dc_rffi1.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/signature-verification/index.d.mts +1 -1
- package/package.json +2 -2
- package/src/actions/actions.test.ts +5 -3
- package/src/actions/actions.ts +313 -23
- package/src/create-kgw-authenticated-fetch.test.ts +46 -26
- package/src/create-kgw-authenticated-fetch.ts +14 -18
- package/src/create-kwil-client.test.ts +40 -0
- package/src/create-kwil-client.ts +25 -0
- package/src/create-kwil-signer.test.ts +2 -67
- package/src/create-kwil-signer.ts +2 -27
- package/src/index.ts +0 -1
- package/dist/index-Dwonqvwu.d.mts.map +0 -1
- package/src/mm-token/create-mm-token-kwil-signer.test.ts +0 -132
- package/src/mm-token/create-mm-token-kwil-signer.ts +0 -161
package/src/actions/actions.ts
CHANGED
|
@@ -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<
|
|
774
|
-
not_before: z.
|
|
775
|
-
not_after: z.
|
|
867
|
+
wallet_type: z.ZodType<AddWalletType>;
|
|
868
|
+
not_before: z.ZodISODateTime;
|
|
869
|
+
not_after: z.ZodISODateTime;
|
|
776
870
|
signature: z.ZodString;
|
|
777
|
-
}> = z
|
|
778
|
-
.
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
/**
|
|
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("
|
|
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
|
-
|
|
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
|
|
27
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
306
|
-
|
|
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
|
|
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
|
|
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
|
|