@idos-network/kwil-infra 3.0.0 → 4.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/actions/index.d.mts +2 -2
  3. package/dist/actions/index.mjs +1 -1
  4. package/dist/actions/index.mjs.map +1 -1
  5. package/dist/facesign/index.d.mts +2 -2
  6. package/dist/facesign/index.mjs +1 -1
  7. package/dist/facesign-signer-CTPPUFxT.mjs +2 -0
  8. package/dist/facesign-signer-CTPPUFxT.mjs.map +1 -0
  9. package/dist/{facesign-signer-5I8juA7p.d.mts → facesign-signer-Dqh899om.d.mts} +3 -2
  10. package/dist/facesign-signer-Dqh899om.d.mts.map +1 -0
  11. package/dist/{index-Bqz6trCk.d.mts → index-Dwonqvwu.d.mts} +33 -20
  12. package/dist/index-Dwonqvwu.d.mts.map +1 -0
  13. package/dist/index.d.mts +2 -2
  14. package/dist/index.mjs +1 -1
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/nep413-D_D-6Yf6.mjs +2 -0
  17. package/dist/nep413-D_D-6Yf6.mjs.map +1 -0
  18. package/dist/signature-verification/index.d.mts +1 -1
  19. package/dist/signature-verification/index.mjs +1 -1
  20. package/dist/signature-verification/index.mjs.map +1 -1
  21. package/dist/xrp/utils.d.mts +41 -41
  22. package/dist/xrp/utils.d.mts.map +1 -1
  23. package/package.json +3 -3
  24. package/src/actions/actions.test.ts +18 -2
  25. package/src/actions/actions.ts +76 -19
  26. package/src/facesign/facesign-signer.test.ts +58 -1
  27. package/src/facesign/facesign-signer.ts +8 -0
  28. package/src/facesign/index.ts +1 -1
  29. package/src/mm-token/create-mm-token-kwil-signer.test.ts +0 -16
  30. package/src/mm-token/create-mm-token-kwil-signer.ts +0 -5
  31. package/src/near/create-near-wallet-kwil-signer.test.ts +218 -0
  32. package/src/near/create-near-wallet-kwil-signer.ts +121 -28
  33. package/src/near/nep413.ts +47 -37
  34. package/src/near/signature-verification.test.ts +35 -0
  35. package/src/near/signature-verification.ts +15 -36
  36. package/dist/facesign-signer-5I8juA7p.d.mts.map +0 -1
  37. package/dist/facesign-signer-z5T312uQ.mjs +0 -2
  38. package/dist/facesign-signer-z5T312uQ.mjs.map +0 -1
  39. package/dist/index-Bqz6trCk.d.mts.map +0 -1
@@ -12,6 +12,11 @@ export const WALLET_TYPES = ["EVM", "NEAR", "XRPL", "Stellar", "FaceSign", "MM"]
12
12
  export type WalletType = (typeof WALLET_TYPES)[number];
13
13
 
14
14
  export const walletTypeSchema: z.ZodType<WalletType> = z.enum(WALLET_TYPES);
15
+ export const ADD_WALLET_TYPES = ["EVM", "NEAR", "XRPL", "Stellar", "FaceSign"] as const;
16
+
17
+ export type AddWalletType = (typeof ADD_WALLET_TYPES)[number];
18
+
19
+ export const addWalletTypeSchema: z.ZodType<AddWalletType> = z.enum(ADD_WALLET_TYPES);
15
20
  export const ENCRYPTION_PASSWORD_STORES = ["user", "mpc", "mm"] as const;
16
21
 
17
22
  export type EncryptionPasswordStore = (typeof ENCRYPTION_PASSWORD_STORES)[number];
@@ -165,6 +170,20 @@ export const actionSchema: Record<string, ActionSchemaElement[]> = {
165
170
  type: DataType.Uuid,
166
171
  },
167
172
  ],
173
+ credential_broader_message: [
174
+ {
175
+ name: "public_notes_signature",
176
+ type: DataType.Text,
177
+ },
178
+ {
179
+ name: "content_uri",
180
+ type: DataType.Text,
181
+ },
182
+ {
183
+ name: "recipient_wallet",
184
+ type: DataType.Text,
185
+ },
186
+ ],
168
187
  create_preliminary_credential: [
169
188
  {
170
189
  name: "request_id",
@@ -736,7 +755,7 @@ export type GetUserAsInserterOutput = z.infer<typeof GetUserAsInserterOutputSche
736
755
  export async function getUserAsInserter(
737
756
  kwilClient: KwilActionClient,
738
757
  params: GetUserAsInserterInput,
739
- ): Promise<GetUserAsInserterOutput> {
758
+ ): Promise<GetUserAsInserterOutput | undefined> {
740
759
  const inputs = GetUserAsInserterInputSchema.parse(params);
741
760
  return await kwilClient
742
761
  .call<GetUserAsInserterOutput[]>({
@@ -792,13 +811,13 @@ export async function upsertWalletAsInserter(
792
811
 
793
812
  export const AddWalletMessageInputSchema: z.ZodObject<{
794
813
  address: z.ZodString;
795
- wallet_type: z.ZodType<WalletType>;
814
+ wallet_type: z.ZodType<AddWalletType>;
796
815
  user_id: z.ZodUUID;
797
816
  not_before: z.ZodISODateTime;
798
817
  not_after: z.ZodISODateTime;
799
818
  }> = z.object({
800
819
  address: z.string(),
801
- wallet_type: walletTypeSchema,
820
+ wallet_type: addWalletTypeSchema,
802
821
  user_id: z.uuid(),
803
822
  not_before: rfc3339DateTimeSchema,
804
823
  not_after: rfc3339DateTimeSchema,
@@ -834,7 +853,7 @@ export const AddWalletInputSchema: z.ZodObject<{
834
853
  id: z.ZodUUID;
835
854
  address: z.ZodString;
836
855
  public_key: z.ZodOptional<z.ZodNullable<z.ZodString>>;
837
- wallet_type: z.ZodType<WalletType>;
856
+ wallet_type: z.ZodType<AddWalletType>;
838
857
  not_before: z.ZodISODateTime;
839
858
  not_after: z.ZodISODateTime;
840
859
  signature: z.ZodString;
@@ -842,7 +861,7 @@ export const AddWalletInputSchema: z.ZodObject<{
842
861
  id: z.uuid(),
843
862
  address: z.string(),
844
863
  public_key: z.string().nullish(),
845
- wallet_type: walletTypeSchema,
864
+ wallet_type: addWalletTypeSchema,
846
865
  not_before: rfc3339DateTimeSchema,
847
866
  not_after: rfc3339DateTimeSchema,
848
867
  signature: z.string(),
@@ -868,7 +887,7 @@ export const GetWalletsOutputSchema: z.ZodObject<{
868
887
  address: z.ZodString;
869
888
  public_key: z.ZodNullable<z.ZodString>;
870
889
  wallet_type: z.ZodType<WalletType>;
871
- message: z.ZodString;
890
+ message: z.ZodNullable<z.ZodString>;
872
891
  signature: z.ZodString;
873
892
  inserter: z.ZodNullable<z.ZodString>;
874
893
  }> = z.object({
@@ -877,7 +896,7 @@ export const GetWalletsOutputSchema: z.ZodObject<{
877
896
  address: z.string(),
878
897
  public_key: z.string().nullable(),
879
898
  wallet_type: walletTypeSchema,
880
- message: z.string(),
899
+ message: z.string().nullable(),
881
900
  signature: z.string(),
882
901
  inserter: z.string().nullable(),
883
902
  });
@@ -911,6 +930,43 @@ export async function removeWallet(
911
930
  });
912
931
  }
913
932
 
933
+ export const CredentialBroaderMessageInputSchema: z.ZodObject<{
934
+ public_notes_signature: z.ZodString;
935
+ content_uri: z.ZodString;
936
+ recipient_wallet: z.ZodString;
937
+ }> = z.object({
938
+ public_notes_signature: z.string(),
939
+ content_uri: blobContentUriSchema,
940
+ recipient_wallet: z.string(),
941
+ });
942
+
943
+ export type CredentialBroaderMessageInput = z.infer<typeof CredentialBroaderMessageInputSchema>;
944
+
945
+ export const CredentialBroaderMessageOutputSchema: z.ZodObject<{
946
+ message: z.ZodString;
947
+ }> = z.object({
948
+ message: z.string(),
949
+ });
950
+
951
+ export type CredentialBroaderMessageOutput = z.infer<typeof CredentialBroaderMessageOutputSchema>;
952
+
953
+ /** CREDENTIAL ACTIONS */
954
+ export async function credentialBroaderMessage(
955
+ kwilClient: KwilActionClient,
956
+ params: CredentialBroaderMessageInput,
957
+ ): Promise<CredentialBroaderMessageOutput> {
958
+ const inputs = CredentialBroaderMessageInputSchema.parse(params);
959
+ return await kwilClient
960
+ .call<CredentialBroaderMessageOutput[]>({
961
+ name: "credential_broader_message",
962
+ inputs,
963
+ })
964
+ .then((result) => {
965
+ if (!result[0]) throw new Error("credential_broader_message returned no rows");
966
+ return result[0];
967
+ });
968
+ }
969
+
914
970
  export const CreatePreliminaryCredentialInputSchema: z.ZodObject<{
915
971
  request_id: z.ZodUUID;
916
972
  credential_id: z.ZodUUID;
@@ -939,7 +995,6 @@ export type CreatePreliminaryCredentialInput = z.infer<
939
995
  typeof CreatePreliminaryCredentialInputSchema
940
996
  >;
941
997
 
942
- /** CREDENTIAL ACTIONS */
943
998
  export async function createPreliminaryCredential(
944
999
  kwilClient: KwilActionClient,
945
1000
  params: CreatePreliminaryCredentialInput,
@@ -1260,7 +1315,7 @@ export type CredentialExistAsInserterOutput = z.infer<typeof CredentialExistAsIn
1260
1315
  export async function credentialExistAsInserter(
1261
1316
  kwilClient: KwilActionClient,
1262
1317
  params: CredentialExistAsInserterInput,
1263
- ): Promise<CredentialExistAsInserterOutput> {
1318
+ ): Promise<CredentialExistAsInserterOutput | undefined> {
1264
1319
  const inputs = CredentialExistAsInserterInputSchema.parse(params);
1265
1320
  return await kwilClient
1266
1321
  .call<CredentialExistAsInserterOutput[]>({
@@ -1380,7 +1435,7 @@ export type GetSiblingCredentialIdOutput = z.infer<typeof GetSiblingCredentialId
1380
1435
  export async function getSiblingCredentialId(
1381
1436
  kwilClient: KwilActionClient,
1382
1437
  params: GetSiblingCredentialIdInput,
1383
- ): Promise<GetSiblingCredentialIdOutput> {
1438
+ ): Promise<GetSiblingCredentialIdOutput | undefined> {
1384
1439
  const inputs = GetSiblingCredentialIdInputSchema.parse(params);
1385
1440
  return await kwilClient
1386
1441
  .call<GetSiblingCredentialIdOutput[]>({
@@ -1573,7 +1628,7 @@ export type DwgMessageOutput = z.infer<typeof DwgMessageOutputSchema>;
1573
1628
  export async function dwgMessage(
1574
1629
  kwilClient: KwilActionClient,
1575
1630
  params: DwgMessageInput,
1576
- ): Promise<DwgMessageOutput> {
1631
+ ): Promise<DwgMessageOutput | undefined> {
1577
1632
  const inputs = DwgMessageInputSchema.parse(params);
1578
1633
  return await kwilClient
1579
1634
  .call<DwgMessageOutput[]>({
@@ -1708,7 +1763,7 @@ export type GetAccessGrantsGrantedCountOutput = z.infer<
1708
1763
  export async function getAccessGrantsGrantedCount(
1709
1764
  kwilClient: KwilActionClient,
1710
1765
  params: GetAccessGrantsGrantedCountInput,
1711
- ): Promise<GetAccessGrantsGrantedCountOutput> {
1766
+ ): Promise<GetAccessGrantsGrantedCountOutput | undefined> {
1712
1767
  const inputs = GetAccessGrantsGrantedCountInputSchema.parse(params);
1713
1768
  return await kwilClient
1714
1769
  .call<GetAccessGrantsGrantedCountOutput[]>({
@@ -1737,7 +1792,7 @@ export type HasLockedAccessGrantsOutput = z.infer<typeof HasLockedAccessGrantsOu
1737
1792
  export async function hasLockedAccessGrants(
1738
1793
  kwilClient: KwilActionClient,
1739
1794
  params: HasLockedAccessGrantsInput,
1740
- ): Promise<HasLockedAccessGrantsOutput> {
1795
+ ): Promise<HasLockedAccessGrantsOutput | undefined> {
1741
1796
  const inputs = HasLockedAccessGrantsInputSchema.parse(params);
1742
1797
  return await kwilClient
1743
1798
  .call<HasLockedAccessGrantsOutput[]>({
@@ -1815,7 +1870,7 @@ export type HasProfileOutput = z.infer<typeof HasProfileOutputSchema>;
1815
1870
  export async function hasProfile(
1816
1871
  kwilClient: KwilActionClient,
1817
1872
  params: HasProfileInput,
1818
- ): Promise<HasProfileOutput> {
1873
+ ): Promise<HasProfileOutput | undefined> {
1819
1874
  const inputs = HasProfileInputSchema.parse(params);
1820
1875
  return await kwilClient
1821
1876
  .call<HasProfileOutput[]>(
@@ -1867,7 +1922,7 @@ export type CheckCallerPayerOutput = z.infer<typeof CheckCallerPayerOutputSchema
1867
1922
  export async function checkCallerPayer(
1868
1923
  kwilClient: KwilActionClient,
1869
1924
  params: CheckCallerPayerInput,
1870
- ): Promise<CheckCallerPayerOutput> {
1925
+ ): Promise<CheckCallerPayerOutput | undefined> {
1871
1926
  const inputs = CheckCallerPayerInputSchema.parse(params);
1872
1927
  return await kwilClient
1873
1928
  .call<CheckCallerPayerOutput[]>({
@@ -1918,7 +1973,7 @@ export type CheckBalanceOutput = z.infer<typeof CheckBalanceOutputSchema>;
1918
1973
  export async function checkBalance(
1919
1974
  kwilClient: KwilActionClient,
1920
1975
  params: CheckBalanceInput,
1921
- ): Promise<CheckBalanceOutput> {
1976
+ ): Promise<CheckBalanceOutput | undefined> {
1922
1977
  const inputs = CheckBalanceInputSchema.parse(params);
1923
1978
  return await kwilClient
1924
1979
  .call<CheckBalanceOutput[]>({
@@ -1949,7 +2004,7 @@ export type GetWalletWithBalanceOutput = z.infer<typeof GetWalletWithBalanceOutp
1949
2004
  export async function getWalletWithBalance(
1950
2005
  kwilClient: KwilActionClient,
1951
2006
  params: GetWalletWithBalanceInput,
1952
- ): Promise<GetWalletWithBalanceOutput> {
2007
+ ): Promise<GetWalletWithBalanceOutput | undefined> {
1953
2008
  const inputs = GetWalletWithBalanceInputSchema.parse(params);
1954
2009
  return await kwilClient
1955
2010
  .call<GetWalletWithBalanceOutput[]>({
@@ -1989,7 +2044,9 @@ export const GetAllowanceOutputSchema: z.ZodObject<{
1989
2044
  export type GetAllowanceOutput = z.infer<typeof GetAllowanceOutputSchema>;
1990
2045
 
1991
2046
  /** FaceSign should not be able to call this */
1992
- export async function getAllowance(kwilClient: KwilActionClient): Promise<GetAllowanceOutput> {
2047
+ export async function getAllowance(
2048
+ kwilClient: KwilActionClient,
2049
+ ): Promise<GetAllowanceOutput | undefined> {
1993
2050
  return await kwilClient
1994
2051
  .call<GetAllowanceOutput[]>({
1995
2052
  name: "get_allowance",
@@ -2045,7 +2102,7 @@ export type GetIssuerFeeOutput = z.infer<typeof GetIssuerFeeOutputSchema>;
2045
2102
  export async function getIssuerFee(
2046
2103
  kwilClient: KwilActionClient,
2047
2104
  params: GetIssuerFeeInput,
2048
- ): Promise<GetIssuerFeeOutput> {
2105
+ ): Promise<GetIssuerFeeOutput | undefined> {
2049
2106
  const inputs = GetIssuerFeeInputSchema.parse(params);
2050
2107
  return await kwilClient
2051
2108
  .call<GetIssuerFeeOutput[]>({
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { afterEach, describe, expect, it, vi } from "vitest";
4
4
 
5
- import { FaceSignSignerProvider } from "./facesign-signer";
5
+ import { FaceSignSignerProvider, resetStoredFaceSignKey } from "./facesign-signer";
6
6
 
7
7
  const ENCLAVE_ORIGIN = "https://facesign.example";
8
8
 
@@ -21,6 +21,63 @@ function iframeWindow(): Window {
21
21
  return iframe.contentWindow;
22
22
  }
23
23
 
24
+ describe("resetStoredFaceSignKey", () => {
25
+ afterEach(() => {
26
+ vi.useRealTimers();
27
+ document.body.replaceChildren();
28
+ });
29
+
30
+ async function readyProvider(hasKey: boolean) {
31
+ const signer = provider();
32
+ const posted: unknown[] = [];
33
+ const pending = resetStoredFaceSignKey(signer);
34
+
35
+ await vi.waitFor(() => {
36
+ expect(document.querySelector("iframe")).not.toBeNull();
37
+ });
38
+
39
+ vi.spyOn(iframeWindow(), "postMessage").mockImplementation((message) => {
40
+ posted.push(message);
41
+ });
42
+
43
+ window.dispatchEvent(
44
+ new MessageEvent("message", {
45
+ origin: ENCLAVE_ORIGIN,
46
+ data: { type: "facesign_ready", hasKey },
47
+ }),
48
+ );
49
+
50
+ return { signer, posted, pending };
51
+ }
52
+
53
+ it("deletes a stored key and tears the provider down", async () => {
54
+ const { posted, pending } = await readyProvider(true);
55
+
56
+ await vi.waitFor(() => {
57
+ expect(posted).toContainEqual({ type: "reset", data: { id: 1 } });
58
+ });
59
+
60
+ window.dispatchEvent(
61
+ new MessageEvent("message", {
62
+ origin: ENCLAVE_ORIGIN,
63
+ data: { type: "reset_complete", data: { id: 1, ok: true } },
64
+ }),
65
+ );
66
+
67
+ await expect(pending).resolves.toBe(true);
68
+ expect(document.querySelector("iframe")).toBeNull();
69
+ });
70
+
71
+ it("leaves a provider with no stored key ready to scan", async () => {
72
+ const { signer, posted, pending } = await readyProvider(false);
73
+
74
+ await expect(pending).resolves.toBe(false);
75
+ expect(posted).toEqual([]);
76
+ expect(document.querySelector("iframe")).not.toBeNull();
77
+ signer.destroy();
78
+ });
79
+ });
80
+
24
81
  describe("FaceSignSignerProvider.reset", () => {
25
82
  afterEach(() => {
26
83
  vi.useRealTimers();
@@ -18,6 +18,14 @@ interface SessionResponse {
18
18
  address: string;
19
19
  }
20
20
 
21
+ // Browser-stored key is not the active idOS profile. Delete it before add-wallet can sign. reset() destroys this provider.
22
+ export async function resetStoredFaceSignKey(provider: FaceSignSignerProvider): Promise<boolean> {
23
+ const { hasKey } = await provider.preload();
24
+ if (!hasKey) return false;
25
+ await provider.reset();
26
+ return true;
27
+ }
28
+
21
29
  export class FaceSignSignerProvider {
22
30
  #iframe: HTMLIFrameElement | null = null;
23
31
  #container: HTMLDialogElement | null = null;
@@ -1,2 +1,2 @@
1
1
  export type { FaceSignMetadata, FaceSignSignerProviderOptions } from "./facesign-signer";
2
- export { FaceSignSignerProvider } from "./facesign-signer";
2
+ export { FaceSignSignerProvider, resetStoredFaceSignKey } from "./facesign-signer";
@@ -4,7 +4,6 @@ import { describe, expect, it } from "vitest";
4
4
  import {
5
5
  createMmTokenAuth,
6
6
  isMmTokenAuth,
7
- mmTokenCredentialContentUri,
8
7
  mmTokenStorageId,
9
8
  type MmTokenEnvelope,
10
9
  } from "./create-mm-token-kwil-signer.js";
@@ -116,21 +115,6 @@ describe("createMmTokenAuth", () => {
116
115
  expect(mmTokenStorageId(signer)).toBe("storage-abc");
117
116
  });
118
117
 
119
- it("builds the credential ukyc:// URI from a base64url-encoded token", () => {
120
- const credentialId = "0198c21d-79cb-7000-8000-000000000001";
121
- const encodedToken = encodeEnvelope(createEnvelope());
122
- const signer = createMmTokenAuth(encodedToken);
123
- const credential = {
124
- id: credentialId,
125
- content_uri: mmTokenCredentialContentUri(signer, credentialId),
126
- };
127
-
128
- expect(credential).toEqual({
129
- id: credentialId,
130
- content_uri: `ukyc://storage-abc/blobs/${credentialId}`,
131
- });
132
- });
133
-
134
118
  it("fails when the token payload has no storage_id", () => {
135
119
  const signer = createMmTokenAuth(
136
120
  encodeEnvelope(
@@ -1,5 +1,4 @@
1
1
  import { KwilSigner } from "@idos-network/kwil-js";
2
- import { createUkycContentUri } from "@idos-network/utils/blob-gateway";
3
2
  import {
4
3
  base64UrlDecode,
5
4
  base64UrlEncode,
@@ -58,10 +57,6 @@ export function mmTokenStorageId(signer: KwilSigner): string {
58
57
  return storageId.trim();
59
58
  }
60
59
 
61
- export function mmTokenCredentialContentUri(signer: KwilSigner, credentialId: string): string {
62
- return createUkycContentUri(mmTokenStorageId(signer), credentialId);
63
- }
64
-
65
60
  function isMmTokenEnvelope(value: unknown): value is MmTokenEnvelope {
66
61
  return (
67
62
  value !== null &&
@@ -0,0 +1,218 @@
1
+ import type { Wallet as NearWallet } from "@near-wallet-selector/core";
2
+
3
+ import {
4
+ base64Encode,
5
+ borshSerialize,
6
+ bs58Encode,
7
+ bytesConcat,
8
+ sha256Hash,
9
+ } from "@idos-network/utils/codecs";
10
+ import { MemoryStore } from "@idos-network/utils/store";
11
+ import nacl from "tweetnacl";
12
+ import { afterEach, describe, expect, it, vi } from "vitest";
13
+
14
+ import type { KwilActionClient } from "../create-kwil-client";
15
+
16
+ import { createNearWalletKwilSigner } from "./create-near-wallet-kwil-signer";
17
+
18
+ const fullAccess = vi.hoisted(() => ({
19
+ keys: [] as string[],
20
+ limitedKeys: [] as string[],
21
+ queries: 0,
22
+ lastRequest: undefined as { request_type?: string; account_id?: string } | undefined,
23
+ }));
24
+
25
+ vi.mock("near-api-js", () => ({
26
+ connect: async () => ({
27
+ connection: {
28
+ provider: {
29
+ query: async (request: { request_type?: string; account_id?: string }) => {
30
+ fullAccess.queries += 1;
31
+ fullAccess.lastRequest = request;
32
+ return {
33
+ keys: [
34
+ ...fullAccess.keys.map((public_key) => ({
35
+ public_key,
36
+ access_key: { permission: "FullAccess" },
37
+ })),
38
+ ...fullAccess.limitedKeys.map((public_key) => ({
39
+ public_key,
40
+ access_key: {
41
+ permission: {
42
+ FunctionCall: {
43
+ allowance: "0",
44
+ receiver_id: "example.near",
45
+ method_names: [],
46
+ },
47
+ },
48
+ },
49
+ })),
50
+ ],
51
+ };
52
+ },
53
+ },
54
+ },
55
+ }),
56
+ }));
57
+
58
+ const ACCOUNT = "victim.testnet";
59
+ const RECIPIENT = "idos.network";
60
+ const CALLBACK = "https://app.example/callback";
61
+ const MESSAGE = "idOS authentication";
62
+ const NEP413_TAG = 2147484061;
63
+
64
+ const nep413PayloadSchema = {
65
+ struct: {
66
+ message: "string",
67
+ nonce: { array: { type: "u8", len: 32 } },
68
+ recipient: "string",
69
+ callbackUrl: { option: "string" },
70
+ },
71
+ } as const;
72
+
73
+ const keyPair = nacl.sign.keyPair.fromSeed(new Uint8Array(32).fill(4));
74
+ const fragmentKey = `ed25519:${bs58Encode(keyPair.publicKey)}`;
75
+ const realKey = `ed25519:${bs58Encode(new Uint8Array(32).fill(1))}`;
76
+
77
+ const kwilClient = {
78
+ client: { auth: { logoutKGW: async () => {} } },
79
+ } as unknown as KwilActionClient;
80
+
81
+ function signNep413(nonce: Uint8Array, message = MESSAGE): string {
82
+ const hash = sha256Hash(
83
+ bytesConcat(
84
+ borshSerialize("u32", NEP413_TAG),
85
+ borshSerialize(nep413PayloadSchema, {
86
+ message,
87
+ nonce,
88
+ recipient: RECIPIENT,
89
+ callbackUrl: CALLBACK,
90
+ }),
91
+ ),
92
+ );
93
+ return base64Encode(nacl.sign.detached(hash, keyPair.secretKey));
94
+ }
95
+
96
+ function signedRequest(): { nonce: Uint8Array; signature: string } {
97
+ for (let i = 0; i < 64; i++) {
98
+ const nonce = new Uint8Array(32).fill(i);
99
+ const signature = signNep413(nonce);
100
+ if (signature.includes("+")) return { nonce, signature };
101
+ }
102
+ throw new Error("expected a base64 signature containing '+'");
103
+ }
104
+
105
+ async function signerPublicKey(fragment: {
106
+ accountId: string;
107
+ signature: string;
108
+ publicKey: string;
109
+ nonce: Uint8Array;
110
+ message?: string;
111
+ }): Promise<string> {
112
+ // Literal "+", not percent-encoded, so URLSearchParams would turn it into a space.
113
+ window.location.hash = `accountId=${fragment.accountId}&signature=${fragment.signature}&publicKey=${fragment.publicKey}`;
114
+ const store = new MemoryStore();
115
+ await store.set("signer-address", ACCOUNT);
116
+ await store.set("signer-public-key", realKey);
117
+ await store.set("sign-last-message", fragment.message ?? MESSAGE);
118
+ await store.set("sign-last-nonce", Array.from(fragment.nonce));
119
+ await store.set("sign-last-url", CALLBACK);
120
+ const wallet = { id: "my-near-wallet", signMessage: vi.fn() } as unknown as NearWallet;
121
+
122
+ const { publicKey } = await createNearWalletKwilSigner(wallet, ACCOUNT, store, kwilClient);
123
+ return publicKey;
124
+ }
125
+
126
+ describe("createNearWalletKwilSigner my-near-wallet redirect", () => {
127
+ afterEach(() => {
128
+ window.location.hash = "";
129
+ fullAccess.keys = [];
130
+ fullAccess.limitedKeys = [];
131
+ fullAccess.queries = 0;
132
+ fullAccess.lastRequest = undefined;
133
+ });
134
+
135
+ it("accepts a fragment whose signature matches the saved request and a full-access key", async () => {
136
+ const { nonce, signature } = signedRequest();
137
+ fullAccess.keys = [realKey, fragmentKey];
138
+
139
+ expect(
140
+ await signerPublicKey({ accountId: ACCOUNT, signature, publicKey: fragmentKey, nonce }),
141
+ ).toBe(fragmentKey);
142
+ expect(window.location.hash).toBe("");
143
+ expect(fullAccess.queries).toBe(1);
144
+ expect(fullAccess.lastRequest).toMatchObject({
145
+ request_type: "view_access_key_list",
146
+ account_id: ACCOUNT,
147
+ });
148
+ });
149
+
150
+ it("ignores a signature that does not match the saved request", async () => {
151
+ const { nonce, signature } = signedRequest();
152
+ fullAccess.keys = [realKey, fragmentKey];
153
+
154
+ expect(
155
+ await signerPublicKey({
156
+ accountId: ACCOUNT,
157
+ signature,
158
+ publicKey: fragmentKey,
159
+ nonce,
160
+ message: "different message",
161
+ }),
162
+ ).toBe(realKey);
163
+ expect(fullAccess.queries).toBe(0);
164
+ });
165
+
166
+ it("ignores a fragment for a different account", async () => {
167
+ const { nonce, signature } = signedRequest();
168
+ fullAccess.keys = [realKey, fragmentKey];
169
+
170
+ expect(
171
+ await signerPublicKey({
172
+ accountId: "attacker.testnet",
173
+ signature,
174
+ publicKey: fragmentKey,
175
+ nonce,
176
+ }),
177
+ ).toBe(realKey);
178
+ expect(fullAccess.queries).toBe(0);
179
+ });
180
+
181
+ it("ignores a public key that is not a full-access key of the account", async () => {
182
+ const { nonce, signature } = signedRequest();
183
+ fullAccess.keys = [realKey];
184
+ fullAccess.limitedKeys = [fragmentKey];
185
+
186
+ expect(
187
+ await signerPublicKey({ accountId: ACCOUNT, signature, publicKey: fragmentKey, nonce }),
188
+ ).toBe(realKey);
189
+ expect(fullAccess.queries).toBe(1);
190
+ expect(fullAccess.lastRequest).toMatchObject({
191
+ request_type: "view_access_key_list",
192
+ account_id: ACCOUNT,
193
+ });
194
+ });
195
+
196
+ it("does not replay an untrusted fragment signature", async () => {
197
+ const { nonce } = signedRequest();
198
+ window.location.hash = `accountId=${ACCOUNT}&signature=sig&publicKey=${fragmentKey}`;
199
+ const store = new MemoryStore();
200
+ await store.set("signer-address", ACCOUNT);
201
+ await store.set("signer-public-key", realKey);
202
+ await store.set("sign-last-message", MESSAGE);
203
+ await store.set("sign-last-nonce", Array.from(nonce));
204
+ await store.set("sign-last-url", CALLBACK);
205
+ const original = vi.fn();
206
+ const wallet = { id: "my-near-wallet", signMessage: original } as unknown as NearWallet;
207
+
208
+ await createNearWalletKwilSigner(wallet, ACCOUNT, store, kwilClient);
209
+ void wallet.signMessage?.({
210
+ message: MESSAGE,
211
+ recipient: RECIPIENT,
212
+ nonce: Buffer.alloc(32),
213
+ });
214
+
215
+ await vi.waitFor(() => expect(original).toHaveBeenCalledOnce());
216
+ expect(original.mock.calls[0]?.[0].message).toBe(MESSAGE);
217
+ });
218
+ });