@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/README.md +15 -9
- package/dist/index.d.ts +51 -26
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -464
- package/dist/index.js.map +1 -1
- package/package.json +13 -9
- package/src/enclave/helpers.ts +10 -6
- package/src/enclave/iframe-enclave.test.ts +26 -0
- package/src/enclave/iframe-enclave.ts +6 -3
- package/src/index.test.ts +521 -0
- package/src/index.ts +513 -98
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import type { BaseProvider } from "@idos-network/enclave";
|
|
4
|
+
|
|
5
|
+
import { createMmTokenAuth, KwilActionClient } from "@idos-network/kwil-infra";
|
|
6
|
+
import { NodeKwil } from "@idos-network/kwil-js";
|
|
7
|
+
import { BlobGateway, createBlobContentReference } from "@idos-network/utils/blob-gateway";
|
|
8
|
+
import { base64Encode, base64UrlEncode, utf8Encode } from "@idos-network/utils/codecs";
|
|
9
|
+
import { MemoryStore } from "@idos-network/utils/store";
|
|
10
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
idOSClientConfiguration,
|
|
14
|
+
idOSClientIdle,
|
|
15
|
+
idOSClientLoggedIn,
|
|
16
|
+
type idOSClientWithUserSigner,
|
|
17
|
+
toDelegatedWriteGrantBaseParams,
|
|
18
|
+
} from "./index.js";
|
|
19
|
+
|
|
20
|
+
const mmToken = base64UrlEncode(
|
|
21
|
+
utf8Encode(
|
|
22
|
+
JSON.stringify({
|
|
23
|
+
payload: {
|
|
24
|
+
signing_public_key: base64UrlEncode(new Uint8Array(32).fill(7)),
|
|
25
|
+
storage_id: "storage-abc",
|
|
26
|
+
},
|
|
27
|
+
signature: base64UrlEncode(new Uint8Array(64).fill(9)),
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
function idleClient(): idOSClientIdle {
|
|
33
|
+
const kwilClient = {
|
|
34
|
+
signer: undefined,
|
|
35
|
+
setSigner(signer: unknown) {
|
|
36
|
+
kwilClient.signer = signer as never;
|
|
37
|
+
},
|
|
38
|
+
call: vi.fn(async () => [{ has_profile: true }]),
|
|
39
|
+
execute: vi.fn(async () => "tx-hash"),
|
|
40
|
+
client: { auth: { logoutKGW: async () => {} } },
|
|
41
|
+
} as unknown as KwilActionClient;
|
|
42
|
+
const enclaveProvider = {
|
|
43
|
+
setSigner: () => {},
|
|
44
|
+
reset: async () => {},
|
|
45
|
+
} as unknown as BaseProvider;
|
|
46
|
+
|
|
47
|
+
return new idOSClientIdle(
|
|
48
|
+
new MemoryStore(),
|
|
49
|
+
kwilClient,
|
|
50
|
+
enclaveProvider,
|
|
51
|
+
new BlobGateway({ url: "https://blob.example" }),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function header(call: Parameters<typeof fetch> | undefined, name: string): string | null {
|
|
56
|
+
if (!call) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return new Headers(call[1]?.headers).get(name);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function nodeIdleClient(): idOSClientIdle {
|
|
64
|
+
const nodeKwil = new NodeKwil({
|
|
65
|
+
kwilProvider: "https://nodes.example",
|
|
66
|
+
chainId: "test-chain",
|
|
67
|
+
}) as NodeKwil & {
|
|
68
|
+
authenticateKGWAndSetCookie: ReturnType<typeof vi.fn<() => Promise<string>>>;
|
|
69
|
+
getKgwCookie: ReturnType<typeof vi.fn<() => string | undefined>>;
|
|
70
|
+
};
|
|
71
|
+
let cookie: string | undefined;
|
|
72
|
+
nodeKwil.getKgwCookie = vi.fn(() => cookie);
|
|
73
|
+
nodeKwil.authenticateKGWAndSetCookie = vi.fn(async () => {
|
|
74
|
+
cookie = "kgw_session=fresh; Path=/";
|
|
75
|
+
return cookie;
|
|
76
|
+
});
|
|
77
|
+
nodeKwil.auth.logoutKGW = async () => ({}) as never;
|
|
78
|
+
|
|
79
|
+
return new idOSClientIdle(
|
|
80
|
+
new MemoryStore(),
|
|
81
|
+
new KwilActionClient(nodeKwil),
|
|
82
|
+
{ setSigner: () => {}, reset: async () => {} } as unknown as BaseProvider,
|
|
83
|
+
new BlobGateway({ url: "https://blob.example" }),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe("UKYC blob authorization is scoped to the signed session", () => {
|
|
88
|
+
it("binds the MM capability when the signer is selected", async () => {
|
|
89
|
+
const idle = idleClient();
|
|
90
|
+
|
|
91
|
+
const withSigner = await idle.withUserSigner(createMmTokenAuth(mmToken));
|
|
92
|
+
|
|
93
|
+
expect(withSigner.blobGateway.hasAccessToken).toBe(true);
|
|
94
|
+
expect(idle.blobGateway.hasAccessToken).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("drops the MM capability on logout", async () => {
|
|
98
|
+
const withSigner = await idleClient().withUserSigner(createMmTokenAuth(mmToken));
|
|
99
|
+
|
|
100
|
+
expect((await withSigner.logOut()).blobGateway.hasAccessToken).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("resets the enclave on logout", async () => {
|
|
104
|
+
const idle = idleClient();
|
|
105
|
+
const reset = vi.fn(async () => {});
|
|
106
|
+
idle.enclaveProvider.reset = reset;
|
|
107
|
+
const withSigner = await idle.withUserSigner(createMmTokenAuth(mmToken));
|
|
108
|
+
|
|
109
|
+
await withSigner.logOut();
|
|
110
|
+
|
|
111
|
+
expect(reset).toHaveBeenCalledOnce();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("leaves a non-MM session without UKYC authorization", async () => {
|
|
115
|
+
const idle = idleClient();
|
|
116
|
+
const customSigner = {
|
|
117
|
+
publicAddress: "0x1234567890123456789012345678901234567890",
|
|
118
|
+
publicKey: base64UrlEncode(new Uint8Array(32).fill(3)),
|
|
119
|
+
signatureType: "ed25519",
|
|
120
|
+
walletType: "EVM",
|
|
121
|
+
signMessage: async () => new Uint8Array(64),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const withSigner = await idle.withUserSigner(customSigner as never);
|
|
125
|
+
|
|
126
|
+
expect(withSigner.blobGateway.hasAccessToken).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
function ed25519Signer(fill: number) {
|
|
131
|
+
return {
|
|
132
|
+
publicAddress: `0x${String(fill).repeat(40)}`,
|
|
133
|
+
publicKey: base64UrlEncode(new Uint8Array(32).fill(fill)),
|
|
134
|
+
signatureType: "ed25519",
|
|
135
|
+
walletType: "EVM",
|
|
136
|
+
signMessage: async () => new Uint8Array(64),
|
|
137
|
+
} as never;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
describe("signer switching on a shared idle client", () => {
|
|
141
|
+
const grantId = crypto.randomUUID();
|
|
142
|
+
const user = {
|
|
143
|
+
id: crypto.randomUUID(),
|
|
144
|
+
recipient_encryption_public_key: base64Encode(new Uint8Array(32).fill(7)),
|
|
145
|
+
encryption_password_store: "user" as const,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
it("stops an older wrapper from reading or writing as the newer caller", async () => {
|
|
149
|
+
const idle = idleClient();
|
|
150
|
+
const withA = await idle.withUserSigner(ed25519Signer(1));
|
|
151
|
+
const loggedInA = new idOSClientLoggedIn(withA, user);
|
|
152
|
+
const withB = await idle.withUserSigner(ed25519Signer(2));
|
|
153
|
+
const loggedInB = new idOSClientLoggedIn(withB, user);
|
|
154
|
+
|
|
155
|
+
await expect(withA.hasProfile()).rejects.toThrow(/stale/);
|
|
156
|
+
await expect(loggedInA.getAllCredentials()).rejects.toThrow(/stale/);
|
|
157
|
+
await expect(loggedInA.revokeAccessGrant(grantId)).rejects.toThrow(/stale/);
|
|
158
|
+
await expect(withA.createUserEncryptionProfile("user-a")).rejects.toThrow(/stale/);
|
|
159
|
+
expect(idle.kwilClient.execute).not.toHaveBeenCalled();
|
|
160
|
+
|
|
161
|
+
await expect(withB.hasProfile()).resolves.toBe(true);
|
|
162
|
+
await expect(loggedInB.revokeAccessGrant(grantId)).resolves.toEqual({ id: grantId });
|
|
163
|
+
expect(idle.kwilClient.execute).toHaveBeenCalledOnce();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("leaves the newer session alone when an older wrapper logs out", async () => {
|
|
167
|
+
const idle = idleClient();
|
|
168
|
+
const reset = vi.fn(async () => {});
|
|
169
|
+
idle.enclaveProvider.reset = reset;
|
|
170
|
+
const withA = await idle.withUserSigner(ed25519Signer(1));
|
|
171
|
+
const withB = await idle.withUserSigner(ed25519Signer(2));
|
|
172
|
+
|
|
173
|
+
await withA.logOut();
|
|
174
|
+
|
|
175
|
+
expect(reset).not.toHaveBeenCalled();
|
|
176
|
+
await expect(withB.hasProfile()).resolves.toBe(true);
|
|
177
|
+
|
|
178
|
+
await withB.logOut();
|
|
179
|
+
|
|
180
|
+
expect(reset).toHaveBeenCalledOnce();
|
|
181
|
+
await expect(withB.hasProfile()).rejects.toThrow(/stale/);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("stops a kwil client saved before the signer changed", async () => {
|
|
185
|
+
const idle = idleClient();
|
|
186
|
+
const signTypedData = vi.fn();
|
|
187
|
+
idle.enclaveProvider.signTypedData = signTypedData;
|
|
188
|
+
const withA = await idle.withUserSigner(ed25519Signer(1));
|
|
189
|
+
const loggedInA = new idOSClientLoggedIn(withA, user);
|
|
190
|
+
const savedWith = withA.kwilClient;
|
|
191
|
+
const savedLoggedIn = loggedInA.kwilClient;
|
|
192
|
+
const savedEnclave = withA.enclaveProvider;
|
|
193
|
+
|
|
194
|
+
await idle.withUserSigner(ed25519Signer(2));
|
|
195
|
+
|
|
196
|
+
await expect(savedWith.execute({} as never)).rejects.toThrow(/stale/);
|
|
197
|
+
await expect(savedLoggedIn.call({} as never)).rejects.toThrow(/stale/);
|
|
198
|
+
expect(() => savedEnclave.signTypedData({}, {}, {})).toThrow(/stale/);
|
|
199
|
+
expect(idle.kwilClient.execute).not.toHaveBeenCalled();
|
|
200
|
+
expect(signTypedData).not.toHaveBeenCalled();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("does not return a client that can act as the newer signer", async () => {
|
|
204
|
+
const idle = idleClient();
|
|
205
|
+
const withA = await idle.withUserSigner(ed25519Signer(1));
|
|
206
|
+
const loggedInA = new idOSClientLoggedIn(withA, user);
|
|
207
|
+
const withB = await idle.withUserSigner(ed25519Signer(2));
|
|
208
|
+
|
|
209
|
+
const fromWith = await withA.logOut();
|
|
210
|
+
const fromLoggedIn = await loggedInA.logOut();
|
|
211
|
+
|
|
212
|
+
await expect(fromWith.kwilClient.execute({} as never)).rejects.toThrow(/stale/);
|
|
213
|
+
await expect(fromLoggedIn.kwilClient.execute({} as never)).rejects.toThrow(/stale/);
|
|
214
|
+
expect(() => fromWith.kwilClient.setSigner(undefined)).toThrow(/stale/);
|
|
215
|
+
expect(() => fromWith.enclaveProvider.setSigner({} as never)).toThrow(/stale/);
|
|
216
|
+
await expect(fromWith.withUserSigner(ed25519Signer(3))).rejects.toThrow(/stale/);
|
|
217
|
+
await expect(withB.hasProfile()).resolves.toBe(true);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("does not install a new signer while logout is still resetting the enclave", async () => {
|
|
221
|
+
const idle = idleClient();
|
|
222
|
+
let releaseReset = (): void => {};
|
|
223
|
+
const resetGate = new Promise<void>((resolve) => {
|
|
224
|
+
releaseReset = resolve;
|
|
225
|
+
});
|
|
226
|
+
let markResetStarted = (): void => {};
|
|
227
|
+
const resetStarted = new Promise<void>((resolve) => {
|
|
228
|
+
markResetStarted = resolve;
|
|
229
|
+
});
|
|
230
|
+
idle.enclaveProvider.reset = vi.fn(async () => {
|
|
231
|
+
markResetStarted();
|
|
232
|
+
await resetGate;
|
|
233
|
+
});
|
|
234
|
+
const withA = await idle.withUserSigner(ed25519Signer(1));
|
|
235
|
+
|
|
236
|
+
const loggingOut = withA.logOut();
|
|
237
|
+
await resetStarted;
|
|
238
|
+
const switching = idle.withUserSigner(ed25519Signer(2));
|
|
239
|
+
for (let i = 0; i < 10; i++) await Promise.resolve();
|
|
240
|
+
expect(idle.kwilClient.signer).toBeUndefined();
|
|
241
|
+
|
|
242
|
+
releaseReset();
|
|
243
|
+
await loggingOut;
|
|
244
|
+
await switching;
|
|
245
|
+
expect(idle.kwilClient.signer).not.toBeUndefined();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("retries enclave reset until it finishes, unless a newer signer owns the session", async () => {
|
|
249
|
+
const idle = idleClient();
|
|
250
|
+
let failReset = true;
|
|
251
|
+
const reset = vi.fn(async () => {
|
|
252
|
+
if (failReset) throw new Error("reset failed");
|
|
253
|
+
});
|
|
254
|
+
idle.enclaveProvider.reset = reset;
|
|
255
|
+
const withA = await idle.withUserSigner(ed25519Signer(1));
|
|
256
|
+
|
|
257
|
+
await expect(withA.logOut()).rejects.toThrow(/reset failed/);
|
|
258
|
+
expect(idle.kwilClient.signer).toBeUndefined();
|
|
259
|
+
|
|
260
|
+
failReset = false;
|
|
261
|
+
await withA.logOut();
|
|
262
|
+
expect(reset).toHaveBeenCalledTimes(2);
|
|
263
|
+
|
|
264
|
+
const withC = await idle.withUserSigner(ed25519Signer(3));
|
|
265
|
+
failReset = true;
|
|
266
|
+
await expect(withC.logOut()).rejects.toThrow(/reset failed/);
|
|
267
|
+
const withD = await idle.withUserSigner(ed25519Signer(4));
|
|
268
|
+
await withC.logOut();
|
|
269
|
+
|
|
270
|
+
expect(reset).toHaveBeenCalledTimes(3);
|
|
271
|
+
await expect(withD.hasProfile()).resolves.toBe(true);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("does not reconfigure the enclave after a newer signer takes over during login", async () => {
|
|
275
|
+
const idle = idleClient();
|
|
276
|
+
let releaseGetUser = (): void => {};
|
|
277
|
+
const getUserGate = new Promise<void>((resolve) => {
|
|
278
|
+
releaseGetUser = resolve;
|
|
279
|
+
});
|
|
280
|
+
let markGetUserStarted = (): void => {};
|
|
281
|
+
const getUserStarted = new Promise<void>((resolve) => {
|
|
282
|
+
markGetUserStarted = resolve;
|
|
283
|
+
});
|
|
284
|
+
vi.spyOn(idle.kwilClient, "call").mockImplementation(async (params) => {
|
|
285
|
+
if (params?.name === "get_user") {
|
|
286
|
+
markGetUserStarted();
|
|
287
|
+
await getUserGate;
|
|
288
|
+
return [user];
|
|
289
|
+
}
|
|
290
|
+
return [{ has_profile: true }];
|
|
291
|
+
});
|
|
292
|
+
const reconfigure = vi.fn(async () => {});
|
|
293
|
+
idle.enclaveProvider.reconfigure = reconfigure;
|
|
294
|
+
const withA = await idle.withUserSigner(ed25519Signer(1));
|
|
295
|
+
|
|
296
|
+
const loggingIn = withA.logIn();
|
|
297
|
+
await getUserStarted;
|
|
298
|
+
await idle.withUserSigner(ed25519Signer(2));
|
|
299
|
+
releaseGetUser();
|
|
300
|
+
|
|
301
|
+
await expect(loggingIn).rejects.toThrow(/stale/);
|
|
302
|
+
expect(reconfigure).not.toHaveBeenCalled();
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe("Node client defaults", () => {
|
|
307
|
+
it("defaults to MemoryStore outside the browser", () => {
|
|
308
|
+
class FakeEnclave {
|
|
309
|
+
constructor(_options: unknown) {}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const config = new idOSClientConfiguration({
|
|
313
|
+
nodeUrl: "https://nodes.example",
|
|
314
|
+
enclaveOptions: {} as never,
|
|
315
|
+
enclaveProvider: FakeEnclave as never,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
expect(config.store).toBeInstanceOf(MemoryStore);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe("Node KGW-authenticated blob sessions", () => {
|
|
323
|
+
afterEach(() => {
|
|
324
|
+
vi.unstubAllGlobals();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("sends the KGW cookie and MM AccessToken, then drops both on logout", async () => {
|
|
328
|
+
const ciphertext = utf8Encode("ciphertext");
|
|
329
|
+
const fetchFn = vi.fn<typeof fetch>(async () => {
|
|
330
|
+
return new Response("ciphertext", {
|
|
331
|
+
headers: { "content-length": String(ciphertext.byteLength) },
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
vi.stubGlobal("fetch", fetchFn);
|
|
335
|
+
|
|
336
|
+
const withSigner = await nodeIdleClient().withUserSigner(createMmTokenAuth(mmToken));
|
|
337
|
+
expect(withSigner.blobGateway.hasAccessToken).toBe(true);
|
|
338
|
+
|
|
339
|
+
await expect(
|
|
340
|
+
withSigner.blobGateway.fetchBlob({
|
|
341
|
+
credentialId: "credential-1",
|
|
342
|
+
expectedSize: ciphertext.byteLength,
|
|
343
|
+
}),
|
|
344
|
+
).resolves.toEqual(ciphertext);
|
|
345
|
+
|
|
346
|
+
expect(header(fetchFn.mock.calls[0], "cookie")).toBe("kgw_session=fresh; Path=/");
|
|
347
|
+
expect(header(fetchFn.mock.calls[0], "authorization")).toBe(`AccessToken ${mmToken}`);
|
|
348
|
+
|
|
349
|
+
const idle = await withSigner.logOut();
|
|
350
|
+
expect(idle.blobGateway.hasAccessToken).toBe(false);
|
|
351
|
+
|
|
352
|
+
fetchFn.mockClear();
|
|
353
|
+
await idle.blobGateway.fetchBlob({
|
|
354
|
+
credentialId: "credential-1",
|
|
355
|
+
expectedSize: ciphertext.byteLength,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
expect(header(fetchFn.mock.calls[0], "cookie")).toBeNull();
|
|
359
|
+
expect(header(fetchFn.mock.calls[0], "authorization")).toBeNull();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("returns blob-backed ciphertext without decrypting", async () => {
|
|
363
|
+
const ciphertext = utf8Encode("encrypted-bytes");
|
|
364
|
+
const { uri, size } = await createBlobContentReference(ciphertext);
|
|
365
|
+
const credentialId = crypto.randomUUID();
|
|
366
|
+
const fetchFn = vi.fn<typeof fetch>(async () => {
|
|
367
|
+
return new Response("encrypted-bytes", {
|
|
368
|
+
headers: { "content-length": String(size) },
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
vi.stubGlobal("fetch", fetchFn);
|
|
372
|
+
|
|
373
|
+
const withSigner = await nodeIdleClient().withUserSigner(createMmTokenAuth(mmToken));
|
|
374
|
+
vi.spyOn(withSigner.kwilClient, "call").mockResolvedValue([
|
|
375
|
+
{
|
|
376
|
+
id: credentialId,
|
|
377
|
+
user_id: crypto.randomUUID(),
|
|
378
|
+
public_notes: "{}",
|
|
379
|
+
content: null,
|
|
380
|
+
content_uri: uri,
|
|
381
|
+
content_size: size,
|
|
382
|
+
encryptor_public_key: base64Encode(new Uint8Array(32).fill(1)),
|
|
383
|
+
issuer_auth_public_key: "issuer-key",
|
|
384
|
+
inserter_type: "user",
|
|
385
|
+
inserter_id: "wallet",
|
|
386
|
+
},
|
|
387
|
+
]);
|
|
388
|
+
|
|
389
|
+
const client = new idOSClientLoggedIn(withSigner, {
|
|
390
|
+
id: crypto.randomUUID(),
|
|
391
|
+
recipient_encryption_public_key: base64Encode(new Uint8Array(32).fill(7)),
|
|
392
|
+
encryption_password_store: "user",
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const credential = await client.getCredentialWithEncryptedContent(credentialId);
|
|
396
|
+
|
|
397
|
+
expect(credential.content).toBe(base64Encode(ciphertext));
|
|
398
|
+
expect(credential.content_uri).toBeNull();
|
|
399
|
+
expect(header(fetchFn.mock.calls[0], "cookie")).toBe("kgw_session=fresh; Path=/");
|
|
400
|
+
expect(header(fetchFn.mock.calls[0], "authorization")).toBe(`AccessToken ${mmToken}`);
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const NACL_BOX_ENCRYPTION_OVERHEAD_BYTES = 24 + 16; // nonce + authentication tag
|
|
405
|
+
const credentialPlaintext = utf8Encode("private credential content");
|
|
406
|
+
|
|
407
|
+
async function createCredentialForWallet(walletType: "MM" | "EVM"): Promise<{
|
|
408
|
+
credential: Awaited<ReturnType<idOSClientLoggedIn["createCredential"]>>;
|
|
409
|
+
preliminaryInput: Record<string, unknown> | undefined;
|
|
410
|
+
uploadedBytes: Uint8Array | undefined;
|
|
411
|
+
}> {
|
|
412
|
+
let preliminaryInput: Record<string, unknown> | undefined;
|
|
413
|
+
let uploadedBytes: Uint8Array | undefined;
|
|
414
|
+
const kwilSigner = walletType === "MM" ? createMmTokenAuth(mmToken) : {};
|
|
415
|
+
const kwilClient = {
|
|
416
|
+
signer: kwilSigner,
|
|
417
|
+
execute: async ({ inputs }: { inputs: Record<string, unknown> }) => {
|
|
418
|
+
preliminaryInput = inputs;
|
|
419
|
+
},
|
|
420
|
+
} as unknown as KwilActionClient;
|
|
421
|
+
const blobGateway = new BlobGateway({
|
|
422
|
+
url: "https://blob.example",
|
|
423
|
+
accessToken: walletType === "MM" ? mmToken : undefined,
|
|
424
|
+
fetchFn: async (_input, init) => {
|
|
425
|
+
const body = init?.body;
|
|
426
|
+
if (!(body instanceof FormData)) throw new Error("Expected multipart upload");
|
|
427
|
+
const original = body.get("original");
|
|
428
|
+
if (!(original instanceof Blob)) throw new Error("Expected original blob");
|
|
429
|
+
|
|
430
|
+
uploadedBytes = new Uint8Array(await original.arrayBuffer());
|
|
431
|
+
const { cid } = await createBlobContentReference(uploadedBytes);
|
|
432
|
+
return Response.json({
|
|
433
|
+
request_id: preliminaryInput?.request_id,
|
|
434
|
+
original_cid: cid,
|
|
435
|
+
});
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
const withSigner = {
|
|
439
|
+
store: new MemoryStore(),
|
|
440
|
+
kwilClient,
|
|
441
|
+
enclaveProvider: {},
|
|
442
|
+
signer: {},
|
|
443
|
+
kwilSigner,
|
|
444
|
+
walletIdentifier: "wallet",
|
|
445
|
+
walletPublicKey: undefined,
|
|
446
|
+
walletType,
|
|
447
|
+
blobGateway,
|
|
448
|
+
} as unknown as idOSClientWithUserSigner;
|
|
449
|
+
const client = new idOSClientLoggedIn(withSigner, {
|
|
450
|
+
id: crypto.randomUUID(),
|
|
451
|
+
recipient_encryption_public_key: base64Encode(new Uint8Array(32).fill(7)),
|
|
452
|
+
encryption_password_store: "user",
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
credential: await client.createCredential("public notes", credentialPlaintext),
|
|
457
|
+
preliminaryInput,
|
|
458
|
+
uploadedBytes,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
describe("credential blob storage", () => {
|
|
463
|
+
it("creates MM credentials with ukyc:// and the encrypted byte size", async () => {
|
|
464
|
+
const { credential, preliminaryInput, uploadedBytes } = await createCredentialForWallet("MM");
|
|
465
|
+
|
|
466
|
+
expect(credential.content_uri).toMatch(/^ukyc:\/\/storage-abc\/blobs\//);
|
|
467
|
+
expect(preliminaryInput?.content_uri).toBe(credential.content_uri);
|
|
468
|
+
expect(preliminaryInput?.content_size).toBe(
|
|
469
|
+
credentialPlaintext.byteLength + NACL_BOX_ENCRYPTION_OVERHEAD_BYTES,
|
|
470
|
+
);
|
|
471
|
+
expect(uploadedBytes?.byteLength).toBe(preliminaryInput?.content_size);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("creates non-MM credentials through the IPFS CID-validation flow", async () => {
|
|
475
|
+
const { credential, preliminaryInput, uploadedBytes } = await createCredentialForWallet("EVM");
|
|
476
|
+
|
|
477
|
+
expect(credential.content_uri).toMatch(/^ipfs:\/\//);
|
|
478
|
+
expect(preliminaryInput?.content_uri).toBe(credential.content_uri);
|
|
479
|
+
expect(preliminaryInput?.content_size).toBe(
|
|
480
|
+
credentialPlaintext.byteLength + NACL_BOX_ENCRYPTION_OVERHEAD_BYTES,
|
|
481
|
+
);
|
|
482
|
+
expect(uploadedBytes?.byteLength).toBe(preliminaryInput?.content_size);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("returns the dwg message string and maps params via toDelegatedWriteGrantBaseParams", async () => {
|
|
486
|
+
const callAction = vi.fn(async () => [{ message: "mock-dwg-message" }]);
|
|
487
|
+
const kwilClient = {
|
|
488
|
+
call: callAction,
|
|
489
|
+
} as unknown as KwilActionClient;
|
|
490
|
+
const client = new idOSClientLoggedIn({ kwilClient } as unknown as idOSClientWithUserSigner, {
|
|
491
|
+
id: crypto.randomUUID(),
|
|
492
|
+
recipient_encryption_public_key: base64Encode(new Uint8Array(32).fill(7)),
|
|
493
|
+
encryption_password_store: "user",
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const dwgInput = {
|
|
497
|
+
id: crypto.randomUUID(),
|
|
498
|
+
owner_wallet_identifier: "0x311CEe6648df431EbbeA38dfB680C28661c893Ea",
|
|
499
|
+
grantee_wallet_identifier: "0x1111111111111111111111111111111111111111",
|
|
500
|
+
issuer_public_key: "issuer-key",
|
|
501
|
+
access_grant_timelock: "2026-01-01T00:00:00Z",
|
|
502
|
+
not_usable_before: "2026-01-01T00:00:00Z",
|
|
503
|
+
not_usable_after: "2026-01-02T00:00:00Z",
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const message = await client.requestDWGMessage(dwgInput);
|
|
507
|
+
const params = toDelegatedWriteGrantBaseParams(dwgInput);
|
|
508
|
+
|
|
509
|
+
expect(message).toBe("mock-dwg-message");
|
|
510
|
+
expect(params).toEqual({
|
|
511
|
+
id: dwgInput.id,
|
|
512
|
+
ownerWalletIdentifier: dwgInput.owner_wallet_identifier,
|
|
513
|
+
consumerWalletIdentifier: dwgInput.grantee_wallet_identifier,
|
|
514
|
+
issuerPublicKey: dwgInput.issuer_public_key,
|
|
515
|
+
accessGrantTimelock: dwgInput.access_grant_timelock,
|
|
516
|
+
notUsableBefore: dwgInput.not_usable_before,
|
|
517
|
+
notUsableAfter: dwgInput.not_usable_after,
|
|
518
|
+
});
|
|
519
|
+
expect(callAction).toHaveBeenCalledWith({ name: "dwg_message", inputs: dwgInput });
|
|
520
|
+
});
|
|
521
|
+
});
|