@idos-network/client 1.0.0-rc.1.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/LICENSE +21 -0
- package/README.md +11 -0
- package/dist/index.d.ts +322 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5633 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
- package/src/enclave/iframe-enclave.ts +237 -0
- package/src/enclave/index.ts +3 -0
- package/src/enclave/metamask-snap-enclave.ts +91 -0
- package/src/enclave/types.ts +44 -0
- package/src/index.ts +472 -0
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@idos-network/client",
|
|
3
|
+
"description": "idOS Client JavaScript SDK for browser environments",
|
|
4
|
+
"version": "1.0.0-rc.1.0",
|
|
5
|
+
"clean-package": {
|
|
6
|
+
"remove": [
|
|
7
|
+
"devDependencies.@idos-network/core"
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@kwilteam/kwil-js": "^0.9.4",
|
|
12
|
+
"es-toolkit": "^1.38.0",
|
|
13
|
+
"tiny-invariant": "^1.3.3"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"clean-package": "^2.2.0",
|
|
17
|
+
"release-it": "^19.0.2",
|
|
18
|
+
"release-it-pnpm": "^4.6.5",
|
|
19
|
+
"tsdown": "^0.11.12",
|
|
20
|
+
"typescript": "^5.8.3"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20.0.0"
|
|
24
|
+
},
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist/**/*",
|
|
33
|
+
"src/**/*"
|
|
34
|
+
],
|
|
35
|
+
"homepage": "https://idos.network",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"module": "dist/index.js",
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"repository": "https://github.com/idos-network/idos-sdk-js",
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsdown",
|
|
44
|
+
"dev": "tsdown --watch",
|
|
45
|
+
"postpack": "clean-package restore",
|
|
46
|
+
"prepack": "clean-package",
|
|
47
|
+
"publint": "pnpx publint --strict",
|
|
48
|
+
"release-it": "release-it"
|
|
49
|
+
},
|
|
50
|
+
"sideEffects": false,
|
|
51
|
+
"type": "module",
|
|
52
|
+
"types": "dist/index.d.ts"
|
|
53
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { base64Encode, type idOSCredential } from "@idos-network/core";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
DiscoverUserEncryptionPublicKeyResponse,
|
|
5
|
+
EnclaveOptions,
|
|
6
|
+
EnclaveProvider,
|
|
7
|
+
StoredData,
|
|
8
|
+
} from "./types";
|
|
9
|
+
|
|
10
|
+
export class IframeEnclave implements EnclaveProvider {
|
|
11
|
+
options: Omit<EnclaveOptions, "container" | "url">;
|
|
12
|
+
container: string;
|
|
13
|
+
iframe: HTMLIFrameElement;
|
|
14
|
+
hostUrl: URL;
|
|
15
|
+
|
|
16
|
+
constructor(options: EnclaveOptions) {
|
|
17
|
+
const { container, ...other } = options;
|
|
18
|
+
|
|
19
|
+
this.container = container;
|
|
20
|
+
this.options = other;
|
|
21
|
+
this.hostUrl = new URL(other.url ?? "https://enclave.idos.network");
|
|
22
|
+
this.iframe = document.createElement("iframe");
|
|
23
|
+
this.iframe.id = "idos-enclave-iframe";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async load(): Promise<void> {
|
|
27
|
+
await this.loadEnclave();
|
|
28
|
+
await this.requestToEnclave({ configure: this.options });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async reconfigure(options: Omit<EnclaveOptions, "container" | "url">): Promise<void> {
|
|
32
|
+
Object.assign(this.options, options);
|
|
33
|
+
await this.requestToEnclave({ configure: this.options });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async ready(userId: string, expectedUserEncryptionPublicKey?: string): Promise<Uint8Array> {
|
|
37
|
+
let { encryptionPublicKey: userEncryptionPublicKey } = (await this.requestToEnclave({
|
|
38
|
+
storage: {
|
|
39
|
+
userId,
|
|
40
|
+
expectedUserEncryptionPublicKey,
|
|
41
|
+
},
|
|
42
|
+
})) as StoredData;
|
|
43
|
+
|
|
44
|
+
while (!userEncryptionPublicKey) {
|
|
45
|
+
this.showEnclave();
|
|
46
|
+
try {
|
|
47
|
+
userEncryptionPublicKey = (await this.requestToEnclave({
|
|
48
|
+
keys: {},
|
|
49
|
+
})) as Uint8Array;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
if (this.options.throwOnUserCancelUnlock) throw e;
|
|
52
|
+
} finally {
|
|
53
|
+
this.hideEnclave();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return userEncryptionPublicKey;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async reset(): Promise<void> {
|
|
61
|
+
this.requestToEnclave({ reset: {} });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async confirm(message: string): Promise<boolean> {
|
|
65
|
+
this.showEnclave();
|
|
66
|
+
|
|
67
|
+
return this.requestToEnclave({ confirm: { message } }).then((response) => {
|
|
68
|
+
this.hideEnclave();
|
|
69
|
+
return response as boolean;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async encrypt(
|
|
74
|
+
message: Uint8Array,
|
|
75
|
+
receiverPublicKey: Uint8Array,
|
|
76
|
+
): Promise<{ content: Uint8Array; encryptorPublicKey: Uint8Array }> {
|
|
77
|
+
return this.requestToEnclave({
|
|
78
|
+
encrypt: { message, receiverPublicKey },
|
|
79
|
+
}) as Promise<{ content: Uint8Array; encryptorPublicKey: Uint8Array }>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async decrypt(message: Uint8Array, senderPublicKey: Uint8Array): Promise<Uint8Array> {
|
|
83
|
+
return this.requestToEnclave({
|
|
84
|
+
decrypt: { fullMessage: message, senderPublicKey },
|
|
85
|
+
}) as Promise<Uint8Array>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
filterCredentials(
|
|
89
|
+
credentials: idOSCredential[],
|
|
90
|
+
privateFieldFilters: { pick: Record<string, unknown[]>; omit: Record<string, unknown[]> },
|
|
91
|
+
): Promise<idOSCredential[]> {
|
|
92
|
+
return this.requestToEnclave({
|
|
93
|
+
filterCredentials: { credentials, privateFieldFilters },
|
|
94
|
+
}) as Promise<idOSCredential[]>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async loadEnclave(): Promise<void> {
|
|
98
|
+
const container =
|
|
99
|
+
document.querySelector(this.container) ||
|
|
100
|
+
throwNewError(Error, `Can't find container with selector ${this.container}`);
|
|
101
|
+
|
|
102
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy#directives
|
|
103
|
+
const permissionsPolicies = ["publickey-credentials-get", "storage-access"];
|
|
104
|
+
|
|
105
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox
|
|
106
|
+
const liftedSandboxRestrictions = [
|
|
107
|
+
"forms",
|
|
108
|
+
"modals",
|
|
109
|
+
"popups",
|
|
110
|
+
"popups-to-escape-sandbox",
|
|
111
|
+
"same-origin",
|
|
112
|
+
"scripts",
|
|
113
|
+
].map((toLift) => `allow-${toLift}`);
|
|
114
|
+
|
|
115
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#referrerpolicy
|
|
116
|
+
const referrerPolicy = "origin";
|
|
117
|
+
|
|
118
|
+
const styles = {
|
|
119
|
+
"aspect-ratio": "4/1",
|
|
120
|
+
"background-color": "transparent",
|
|
121
|
+
border: "none",
|
|
122
|
+
display: "block",
|
|
123
|
+
width: "100%",
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
this.iframe.allow = permissionsPolicies.join("; ");
|
|
127
|
+
this.iframe.referrerPolicy = referrerPolicy;
|
|
128
|
+
this.iframe.sandbox.add(...liftedSandboxRestrictions);
|
|
129
|
+
this.iframe.src = this.hostUrl.toString();
|
|
130
|
+
for (const [k, v] of Object.entries(styles)) {
|
|
131
|
+
this.iframe.style.setProperty(k, v);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let el: HTMLElement | null;
|
|
135
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: it's on purpose
|
|
136
|
+
while ((el = document.getElementById(this.iframe.id))) {
|
|
137
|
+
console.log("reinstalling idOS iframe...");
|
|
138
|
+
container.removeChild(el);
|
|
139
|
+
}
|
|
140
|
+
container.appendChild(this.iframe);
|
|
141
|
+
|
|
142
|
+
return new Promise((resolve) =>
|
|
143
|
+
this.iframe.addEventListener(
|
|
144
|
+
"load",
|
|
145
|
+
() => {
|
|
146
|
+
resolve();
|
|
147
|
+
},
|
|
148
|
+
{ once: true },
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
showEnclave(): void {
|
|
154
|
+
// biome-ignore lint/style/noNonNullAssertion: Make the explosion visible.
|
|
155
|
+
this.iframe.parentElement!.classList.add("visible");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
hideEnclave(): void {
|
|
159
|
+
// biome-ignore lint/style/noNonNullAssertion: Make the explosion visible.
|
|
160
|
+
this.iframe.parentElement!.classList.remove("visible");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// biome-ignore lint/suspicious/noExplicitAny: `any` is fine here. We will type it properly later.
|
|
164
|
+
async requestToEnclave(request: any): Promise<any> {
|
|
165
|
+
return new Promise((resolve, reject) => {
|
|
166
|
+
const { port1, port2 } = new MessageChannel();
|
|
167
|
+
|
|
168
|
+
port1.onmessage = ({ data }) => {
|
|
169
|
+
port1.close();
|
|
170
|
+
data.error ? reject(data.error) : resolve(data.result);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// biome-ignore lint/style/noNonNullAssertion: Make the explosion visible.
|
|
174
|
+
this.iframe.contentWindow!.postMessage(request, this.hostUrl.origin, [port2]);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async backupPasswordOrSecret(): Promise<void> {
|
|
179
|
+
const abortController = new AbortController();
|
|
180
|
+
this.showEnclave();
|
|
181
|
+
|
|
182
|
+
window.addEventListener(
|
|
183
|
+
"message",
|
|
184
|
+
async (event) => {
|
|
185
|
+
if (event.data.type !== "idOS:store" || event.origin !== this.hostUrl.origin) return;
|
|
186
|
+
|
|
187
|
+
let status = "";
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
status = "success";
|
|
191
|
+
this.hideEnclave();
|
|
192
|
+
} catch (_) {
|
|
193
|
+
status = "failure";
|
|
194
|
+
this.hideEnclave();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
event.ports[0].postMessage({
|
|
198
|
+
result: {
|
|
199
|
+
type: "idOS:store",
|
|
200
|
+
status,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
event.ports[0].close();
|
|
204
|
+
abortController.abort();
|
|
205
|
+
},
|
|
206
|
+
{ signal: abortController.signal },
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
await this.requestToEnclave({
|
|
211
|
+
backupPasswordOrSecret: {},
|
|
212
|
+
});
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error(error);
|
|
215
|
+
} finally {
|
|
216
|
+
this.hideEnclave();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async discoverUserEncryptionPublicKey(
|
|
221
|
+
userId: string,
|
|
222
|
+
): Promise<DiscoverUserEncryptionPublicKeyResponse> {
|
|
223
|
+
if (this.options.mode !== "new")
|
|
224
|
+
throw new Error("You can only call `discoverUserEncryptionPublicKey` when mode is `new`.");
|
|
225
|
+
|
|
226
|
+
const userEncryptionPublicKey = await this.ready(userId);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
userId,
|
|
230
|
+
userEncryptionPublicKey: base64Encode(userEncryptionPublicKey),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function throwNewError(ErrorClass: ErrorConstructor, ...args: Parameters<ErrorConstructor>): never {
|
|
236
|
+
throw new ErrorClass(...args);
|
|
237
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { idOSCredential } from "@idos-network/core";
|
|
2
|
+
import type {
|
|
3
|
+
DiscoverUserEncryptionPublicKeyResponse,
|
|
4
|
+
EnclaveOptions,
|
|
5
|
+
EnclaveProvider,
|
|
6
|
+
} from "./types";
|
|
7
|
+
|
|
8
|
+
export class MetaMaskSnapEnclave implements EnclaveProvider {
|
|
9
|
+
// biome-ignore lint/suspicious/noExplicitAny: Types will be added later
|
|
10
|
+
enclaveHost: any;
|
|
11
|
+
snapId: string;
|
|
12
|
+
|
|
13
|
+
constructor(_?: Record<string, unknown>) {
|
|
14
|
+
// biome-ignore lint/suspicious/noExplicitAny: Types will be added later.
|
|
15
|
+
this.enclaveHost = (window as any).ethereum;
|
|
16
|
+
this.snapId = "npm:@idos-network/metamask-snap-enclave";
|
|
17
|
+
}
|
|
18
|
+
reconfigure(_options: Omit<EnclaveOptions, "container" | "url">): Promise<void> {
|
|
19
|
+
throw new Error("Method not implemented.");
|
|
20
|
+
}
|
|
21
|
+
filterCredentials(
|
|
22
|
+
credentials: idOSCredential[],
|
|
23
|
+
privateFieldFilters: { pick: Record<string, unknown[]>; omit: Record<string, unknown[]> },
|
|
24
|
+
): Promise<idOSCredential[]> {
|
|
25
|
+
console.log(credentials, privateFieldFilters);
|
|
26
|
+
throw new Error("Method not implemented.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async discoverUserEncryptionPublicKey(): Promise<DiscoverUserEncryptionPublicKeyResponse> {
|
|
30
|
+
throw new Error("Method not implemented.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async load(): Promise<void> {
|
|
34
|
+
const snaps = await this.enclaveHost.request({ method: "wallet_getSnaps" });
|
|
35
|
+
// biome-ignore lint/suspicious/noExplicitAny: Types will be added later
|
|
36
|
+
const connected = Object.values(snaps).find((snap: any) => snap.id === this.snapId);
|
|
37
|
+
|
|
38
|
+
if (!connected)
|
|
39
|
+
await this.enclaveHost.request({
|
|
40
|
+
method: "wallet_requestSnaps",
|
|
41
|
+
params: { [this.snapId]: {} },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async ready(userId: string): Promise<Uint8Array> {
|
|
46
|
+
let { encryptionPublicKey } = JSON.parse(await this.invokeSnap("storage", { userId }));
|
|
47
|
+
|
|
48
|
+
encryptionPublicKey ||= await this.invokeSnap("init");
|
|
49
|
+
encryptionPublicKey &&= Uint8Array.from(Object.values(encryptionPublicKey));
|
|
50
|
+
|
|
51
|
+
return encryptionPublicKey;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// biome-ignore lint/suspicious/noExplicitAny: Using `any` to avoid type errors.
|
|
55
|
+
invokeSnap(method: string, params: unknown = {}): Promise<any> {
|
|
56
|
+
return this.enclaveHost.request({
|
|
57
|
+
method: "wallet_invokeSnap",
|
|
58
|
+
params: {
|
|
59
|
+
snapId: this.snapId,
|
|
60
|
+
request: { method, params },
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async reset(): Promise<void> {
|
|
66
|
+
return this.invokeSnap("reset");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async confirm(message: string): Promise<boolean> {
|
|
70
|
+
return this.invokeSnap("confirm", { message });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async encrypt(
|
|
74
|
+
message: Uint8Array,
|
|
75
|
+
receiverPublicKey: Uint8Array,
|
|
76
|
+
): Promise<{ content: Uint8Array; encryptorPublicKey: Uint8Array }> {
|
|
77
|
+
await this.invokeSnap("encrypt", { message, receiverPublicKey });
|
|
78
|
+
|
|
79
|
+
throw new Error("The Metamask Enclave needs to be updated");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async decrypt(message: Uint8Array, senderPublicKey: Uint8Array): Promise<Uint8Array> {
|
|
83
|
+
const decrypted = await this.invokeSnap("decrypt", { message, senderPublicKey });
|
|
84
|
+
|
|
85
|
+
return Uint8Array.from(Object.values(decrypted));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async backupPasswordOrSecret(): Promise<void> {
|
|
89
|
+
throw new Error("Method not implemented.");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { idOSCredential } from "@idos-network/core";
|
|
2
|
+
|
|
3
|
+
export type StoredData = {
|
|
4
|
+
encryptionPublicKey?: Uint8Array;
|
|
5
|
+
userId?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type DiscoverUserEncryptionPublicKeyResponse = {
|
|
9
|
+
userId: string;
|
|
10
|
+
userEncryptionPublicKey: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type EnclaveOptions = {
|
|
14
|
+
container: string;
|
|
15
|
+
theme?: "light" | "dark";
|
|
16
|
+
mode?: "new" | "existing";
|
|
17
|
+
url?: string;
|
|
18
|
+
throwOnUserCancelUnlock?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export interface EnclaveProvider {
|
|
22
|
+
load(): Promise<void>;
|
|
23
|
+
reconfigure(options: Omit<EnclaveOptions, "container" | "url">): Promise<void>;
|
|
24
|
+
|
|
25
|
+
ready(userId: string, currentUserEncryptionPublicKey?: string): Promise<Uint8Array>;
|
|
26
|
+
reset(): Promise<void>;
|
|
27
|
+
confirm(message: string): Promise<boolean>;
|
|
28
|
+
encrypt(
|
|
29
|
+
message: Uint8Array,
|
|
30
|
+
receiverPublicKey?: Uint8Array,
|
|
31
|
+
): Promise<{ content: Uint8Array; encryptorPublicKey: Uint8Array }>;
|
|
32
|
+
decrypt(message: Uint8Array, senderPublicKey?: Uint8Array): Promise<Uint8Array>;
|
|
33
|
+
discoverUserEncryptionPublicKey(userId: string): Promise<DiscoverUserEncryptionPublicKeyResponse>;
|
|
34
|
+
|
|
35
|
+
filterCredentials(
|
|
36
|
+
credentials: idOSCredential[],
|
|
37
|
+
privateFieldFilters: {
|
|
38
|
+
pick: Record<string, unknown[]>;
|
|
39
|
+
omit: Record<string, unknown[]>;
|
|
40
|
+
},
|
|
41
|
+
): Promise<idOSCredential[]>;
|
|
42
|
+
|
|
43
|
+
backupPasswordOrSecret(): Promise<void>;
|
|
44
|
+
}
|