@idos-network/enclave 1.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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/dist/base-0xSNl4rW.d.mts +140 -0
  4. package/dist/base-0xSNl4rW.d.mts.map +1 -0
  5. package/dist/client-BSkUuHYm.mjs +1196 -0
  6. package/dist/client-BSkUuHYm.mjs.map +1 -0
  7. package/dist/client-Bu0aO5sT.d.mts +38 -0
  8. package/dist/client-Bu0aO5sT.d.mts.map +1 -0
  9. package/dist/index.d.mts +16 -0
  10. package/dist/index.d.mts.map +1 -0
  11. package/dist/index.mjs +2 -0
  12. package/dist/keys-RuC1M-_K.mjs +187 -0
  13. package/dist/keys-RuC1M-_K.mjs.map +1 -0
  14. package/dist/local.d.mts +96 -0
  15. package/dist/local.d.mts.map +1 -0
  16. package/dist/local.mjs +253 -0
  17. package/dist/local.mjs.map +1 -0
  18. package/dist/mpc/index.d.mts +3 -0
  19. package/dist/mpc/index.mjs +2 -0
  20. package/dist/types-DKsOwtWL.d.mts +73 -0
  21. package/dist/types-DKsOwtWL.d.mts.map +1 -0
  22. package/package.json +76 -0
  23. package/src/base.test.ts +137 -0
  24. package/src/base.ts +306 -0
  25. package/src/index.ts +3 -0
  26. package/src/keys.ts +10 -0
  27. package/src/local.test.ts +113 -0
  28. package/src/local.ts +495 -0
  29. package/src/mpc/api.ts +122 -0
  30. package/src/mpc/client.ts +411 -0
  31. package/src/mpc/engine-client.ts +144 -0
  32. package/src/mpc/generated/IdosContract.ts +92 -0
  33. package/src/mpc/index.ts +2 -0
  34. package/src/mpc/secretsharing/f256.ts +122 -0
  35. package/src/mpc/secretsharing/finite-field-element.ts +50 -0
  36. package/src/mpc/secretsharing/lagrange.ts +163 -0
  37. package/src/mpc/secretsharing/polynomial.ts +84 -0
  38. package/src/mpc/secretsharing/secret-share-factory.ts +30 -0
  39. package/src/mpc/secretsharing/secret-shares.ts +20 -0
  40. package/src/mpc/secretsharing/shamir-secret-shares.ts +164 -0
  41. package/src/mpc/secretsharing/xor-secret-shares.ts +68 -0
  42. package/src/mpc/types.ts +117 -0
  43. package/src/types.ts +37 -0
package/src/local.ts ADDED
@@ -0,0 +1,495 @@
1
+ import type { EncryptionPasswordStore } from "@idos-network/kwil-infra/actions";
2
+
3
+ import {
4
+ base64Codec,
5
+ base64Encode,
6
+ hexDecode,
7
+ hexEncode,
8
+ utf8Decode,
9
+ utf8Encode,
10
+ } from "@idos-network/utils/codecs";
11
+ import { generatePassword } from "@idos-network/utils/cryptography";
12
+ import { decrypt, encrypt, keyDerivation } from "@idos-network/utils/encryption";
13
+ import { LocalStorageStore, type PipeCodecArgs, type Store } from "@idos-network/utils/store";
14
+ import { syncScrypt } from "scrypt-js";
15
+ import nacl from "tweetnacl";
16
+
17
+ import type {
18
+ AddAddressMessageToSign,
19
+ AddAddressSignatureMessage,
20
+ DownloadMessageToSign,
21
+ RemoveAddressMessageToSign,
22
+ RemoveAddressSignatureMessage,
23
+ UploadMessageToSign,
24
+ } from "./mpc/types";
25
+ import type {
26
+ EnclaveOptions,
27
+ MPCPasswordContext,
28
+ PasswordContext,
29
+ PrivateEncryptionProfile,
30
+ PublicEncryptionProfile,
31
+ } from "./types";
32
+
33
+ import { BaseProvider } from "./base";
34
+ import { STORAGE_KEYS } from "./keys";
35
+ import { Client as MPCClient } from "./mpc/client";
36
+
37
+ export interface LocalEnclaveOptions extends EnclaveOptions {
38
+ store?: Store;
39
+ allowedEncryptionStores?: EncryptionPasswordStore[];
40
+ mpcConfiguration?: {
41
+ nodeUrl: string;
42
+ contractAddress: string;
43
+ numMalicious: number;
44
+ numNodes: number;
45
+ numToReconstruct: number;
46
+ };
47
+ }
48
+
49
+ export class LocalEnclave<
50
+ K extends LocalEnclaveOptions = LocalEnclaveOptions,
51
+ > extends BaseProvider<K> {
52
+ protected allowedEncryptionStores: EncryptionPasswordStore[];
53
+
54
+ // Store for data
55
+ protected store: Store;
56
+ protected storeBase64: Store;
57
+ protected storeObfuscated: Store;
58
+ protected storeObfuscatedBase64: Store;
59
+
60
+ // In case of MPC usage
61
+ protected mpcClientInstance?: MPCClient;
62
+
63
+ // Stored key pair and user id for that key pair
64
+ // those are most likely loaded from the store
65
+ // during the load() method.
66
+ protected storedEncryptionProfile?: PrivateEncryptionProfile;
67
+
68
+ constructor(options: K) {
69
+ super(options);
70
+
71
+ // By default, we only allow password auth method
72
+ this.allowedEncryptionStores = options.allowedEncryptionStores ?? ["user"];
73
+ this.store = options.store ?? new LocalStorageStore();
74
+ this.storeObfuscated = this.store.pipeCodec(this.userIdObfuscationCodec());
75
+ this.storeBase64 = this.store.pipeCodec(base64Codec);
76
+ this.storeObfuscatedBase64 = this.store
77
+ .pipeCodec(this.userIdObfuscationCodec())
78
+ .pipeCodec(base64Codec);
79
+
80
+ if (options.mpcConfiguration) {
81
+ this.mpcClientInstance = new MPCClient(
82
+ options.mpcConfiguration.nodeUrl,
83
+ options.mpcConfiguration.contractAddress,
84
+ options.mpcConfiguration.numMalicious,
85
+ options.mpcConfiguration.numNodes,
86
+ options.mpcConfiguration.numToReconstruct,
87
+ // TODO: This is weird, we should not be forcing this
88
+ options.walletType ?? "EVM",
89
+ options.walletAddress ?? "",
90
+ options.walletPublicKey,
91
+ );
92
+ }
93
+ }
94
+
95
+ /** @override parent method to reset the enclave */
96
+ async reset(): Promise<void> {
97
+ await super.reset();
98
+ this.storedEncryptionProfile = undefined;
99
+ this.store.reset();
100
+ }
101
+
102
+ /** @override parent method to reconfigure the enclave */
103
+ async reconfigure(options: Partial<K> = {}): Promise<void> {
104
+ await super.reconfigure(options);
105
+ // Reconfigure MPC client if any signer information changed
106
+ if (this.mpcClientInstance && options.walletType && options.walletAddress) {
107
+ this.mpcClientInstance.reconfigure(
108
+ options.walletType,
109
+ options.walletAddress ?? "",
110
+ options.walletPublicKey,
111
+ );
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Return a codec that encrypts/decrypts data using a key derived from the provider's user ID.
117
+ *
118
+ * This ensures that data is minimally obfuscated to avoid low-sophistication attacks.
119
+ */
120
+ private userIdObfuscationCodec(): PipeCodecArgs<string> {
121
+ // oxlint-disable-next-line typescript/no-this-alias -- Needed for `this` in nested encode/decode functions
122
+ const self = this;
123
+
124
+ const toKey = (userId: string, salt: Uint8Array) =>
125
+ syncScrypt(utf8Encode(userId), salt, 16384, 8, 1, nacl.secretbox.keyLength);
126
+
127
+ return {
128
+ encode(data: string): string {
129
+ if (!self.options.userId) {
130
+ throw new Error("User ID required for encryption");
131
+ }
132
+
133
+ const dataBytes = utf8Encode(data); // Convert string to Uint8Array
134
+ const nonce = nacl.randomBytes(nacl.secretbox.nonceLength);
135
+ const salt = nacl.randomBytes(16);
136
+ const key = toKey(self.options.userId, salt);
137
+ const payload = nacl.secretbox(dataBytes, nonce, key);
138
+
139
+ // Store salt + nonce + ciphertext
140
+ const combined = new Uint8Array(salt.length + nonce.length + payload.length);
141
+ combined.set(salt, 0);
142
+ combined.set(nonce, salt.length);
143
+ combined.set(payload, salt.length + nonce.length);
144
+
145
+ return `0x${hexEncode(combined)}`;
146
+ },
147
+
148
+ decode(payload: string): string {
149
+ if (!self.options.userId) {
150
+ throw new Error("User ID required for decryption");
151
+ }
152
+
153
+ if (payload.slice(0, 2) !== "0x") {
154
+ throw new Error(`missing 0x prefix: ${payload}`);
155
+ }
156
+
157
+ const combined = hexDecode(payload.slice(2));
158
+ const salt = combined.slice(0, 16);
159
+ const nonce = combined.slice(16, 16 + nacl.secretbox.nonceLength);
160
+ const ciphertext = combined.slice(16 + nacl.secretbox.nonceLength);
161
+
162
+ const key = toKey(self.options.userId, salt);
163
+ const result = nacl.secretbox.open(ciphertext, nonce, key);
164
+
165
+ if (!result) throw new Error("Failed to decrypt data");
166
+
167
+ return utf8Decode(result); // Convert Uint8Array back to string
168
+ },
169
+ };
170
+ }
171
+ /** @see parent method extended with loading the profile from the store */
172
+ async load(): Promise<void> {
173
+ await super.load();
174
+
175
+ const userId = await this.store.get<string>(STORAGE_KEYS.USER_ID);
176
+ if (userId) this.options.userId = userId;
177
+
178
+ let password = await this.storeObfuscated.get<string>(STORAGE_KEYS.OBFUSCATED_PASSWORD);
179
+ if (!password) {
180
+ password = await this.store.get<string>(STORAGE_KEYS.DEPRECATED___PASSWORD);
181
+ if (!password) return;
182
+
183
+ await this.storeObfuscated.set(STORAGE_KEYS.OBFUSCATED_PASSWORD, password);
184
+ }
185
+ await this.store.delete(STORAGE_KEYS.DEPRECATED___PASSWORD);
186
+
187
+ let encryptionSecretKey = await this.storeObfuscatedBase64.get<Uint8Array>(
188
+ STORAGE_KEYS.OBFUSCATED_BASE64_ENCRYPTION_SECRET_KEY,
189
+ );
190
+ if (!encryptionSecretKey) {
191
+ encryptionSecretKey = await this.storeBase64.get<Uint8Array>(
192
+ STORAGE_KEYS.DEPRECATED___ENCRYPTION_SECRET_KEY,
193
+ );
194
+ if (!encryptionSecretKey) return;
195
+
196
+ await this.storeObfuscatedBase64.set(
197
+ STORAGE_KEYS.OBFUSCATED_BASE64_ENCRYPTION_SECRET_KEY,
198
+ encryptionSecretKey,
199
+ );
200
+ }
201
+ await this.store.delete(STORAGE_KEYS.DEPRECATED___ENCRYPTION_SECRET_KEY);
202
+ // Even older version.
203
+ await this.store.delete(STORAGE_KEYS.DEPRECATED___ENCRYPTION_PRIVATE_KEY);
204
+
205
+ if (!password || !userId || !encryptionSecretKey) {
206
+ return;
207
+ }
208
+
209
+ let encryptionPasswordStore = await this.store.get<EncryptionPasswordStore>(
210
+ STORAGE_KEYS.ENCRYPTION_PASSWORD_STORE,
211
+ );
212
+
213
+ // Migration from "password" to "user"
214
+ // TODO: Remove this after a while
215
+ if (!encryptionPasswordStore || (encryptionPasswordStore as string) === "password") {
216
+ encryptionPasswordStore = "user";
217
+ await this.store.set<EncryptionPasswordStore>(
218
+ STORAGE_KEYS.ENCRYPTION_PASSWORD_STORE,
219
+ encryptionPasswordStore,
220
+ );
221
+ }
222
+
223
+ this.storedEncryptionProfile = {
224
+ userId,
225
+ password,
226
+ keyPair: nacl.box.keyPair.fromSecretKey(encryptionSecretKey),
227
+ encryptionPasswordStore,
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Encrypts a message to a receiver.
233
+ * This method also checks if the user is authorized to use the keys.
234
+ *
235
+ * @param message - The message to encrypt.
236
+ * @param receiverPublicKey - The public key of the receiver.
237
+ *
238
+ * @returns The encrypted message.
239
+ */
240
+ async encrypt(
241
+ message: Uint8Array,
242
+ receiverPublicKey: Uint8Array,
243
+ ): Promise<{ content: Uint8Array; encryptorPublicKey: Uint8Array }> {
244
+ const { keyPair } = await this.getPrivateEncryptionProfile();
245
+ return encrypt(message, keyPair.publicKey, receiverPublicKey);
246
+ }
247
+
248
+ /**
249
+ * Decrypts a message from a sender.
250
+ * This method also checks if the user is authorized to use the keys.
251
+ *
252
+ * @param message - The message to decrypt.
253
+ * @param senderPublicKey - The public key of the sender.
254
+ *
255
+ * @returns The decrypted message.
256
+ */
257
+ async decrypt(
258
+ message: Uint8Array,
259
+ senderPublicKey: Uint8Array,
260
+ ): Promise<Uint8Array<ArrayBufferLike>> {
261
+ const { keyPair } = await this.getPrivateEncryptionProfile();
262
+ return decrypt(message, keyPair, senderPublicKey);
263
+ }
264
+
265
+ /**
266
+ * @see BaseProvider#getPrivateEncryptionProfile
267
+ */
268
+ async getPrivateEncryptionProfile(skipGuard = false): Promise<PrivateEncryptionProfile> {
269
+ // In case there is a no key pair, which matches we don't need a new one.
270
+ if (this.storedEncryptionProfile) {
271
+ let canBeUsed = this.storedEncryptionProfile.userId === this.userId;
272
+
273
+ if (canBeUsed && !skipGuard) {
274
+ // When users matches, we also need to check
275
+ // if origin is authorized to use the keys.
276
+ canBeUsed = await this.guardKeys();
277
+ }
278
+
279
+ if (canBeUsed) {
280
+ return this.storedEncryptionProfile;
281
+ }
282
+
283
+ // Something did not match, we need to create a new key pair
284
+ // and reset the enclave.
285
+ await this.reset();
286
+ }
287
+
288
+ // The stored profile can't be used, or we have to create a new one.
289
+ let password: string | undefined;
290
+ let encryptionPasswordStore: EncryptionPasswordStore | undefined;
291
+
292
+ const context = await this.getPasswordContext();
293
+
294
+ if (context.encryptionPasswordStore === "user") {
295
+ await this.store.setRememberDuration(context.duration);
296
+
297
+ password = context.password;
298
+ encryptionPasswordStore = context.encryptionPasswordStore;
299
+ }
300
+
301
+ if (context.encryptionPasswordStore === "mpc") {
302
+ // TODO: This is resetting duration, but should?
303
+ await this.store.setRememberDuration(undefined);
304
+
305
+ password = await this.ensureMPCPassword();
306
+ encryptionPasswordStore = "mpc";
307
+ }
308
+
309
+ if (!password || !encryptionPasswordStore) {
310
+ throw new Error("Password or encryption password store is not found");
311
+ }
312
+
313
+ return this.createEncryptionProfileFromPassword(password, this.userId, encryptionPasswordStore);
314
+ }
315
+
316
+ /** @see BaseProvider#ensureUserEncryptionProfile */
317
+ async ensureUserEncryptionProfile(): Promise<PublicEncryptionProfile> {
318
+ const { keyPair, encryptionPasswordStore, userId } = await this.getPrivateEncryptionProfile();
319
+
320
+ return {
321
+ userId,
322
+ userEncryptionPublicKey: base64Encode(keyPair.publicKey),
323
+ encryptionPasswordStore: encryptionPasswordStore,
324
+ };
325
+ }
326
+
327
+ /**
328
+ * This method needs to check `options` and should derive the password context from it.
329
+ *
330
+ * @returns The password context.
331
+ */
332
+ async getPasswordContext(): Promise<PasswordContext | MPCPasswordContext> {
333
+ throw new Error("Method 'getPasswordContext' has to be implemented in the subclass.");
334
+ }
335
+
336
+ /**
337
+ * Creates and store encryption profile from a password.
338
+ *
339
+ * @param password - The password to use.
340
+ * @param userId - The user id to use.
341
+ * @param encryptionPasswordStore - The encryption password store to use.
342
+ *
343
+ * @returns The encryption profile.
344
+ */
345
+ async createEncryptionProfileFromPassword(
346
+ password: string,
347
+ userId: string,
348
+ encryptionPasswordStore: EncryptionPasswordStore,
349
+ ): Promise<PrivateEncryptionProfile> {
350
+ const secretKey = await keyDerivation(password, userId);
351
+
352
+ const keyPair = nacl.box.keyPair.fromSecretKey(secretKey);
353
+
354
+ await this.store.set(STORAGE_KEYS.USER_ID, userId);
355
+ await this.storeObfuscated.set(STORAGE_KEYS.OBFUSCATED_PASSWORD, password);
356
+ await this.store.delete(STORAGE_KEYS.DEPRECATED___PASSWORD);
357
+ await this.store.set(STORAGE_KEYS.ENCRYPTION_PASSWORD_STORE, encryptionPasswordStore);
358
+ await this.storeObfuscatedBase64.set(
359
+ STORAGE_KEYS.OBFUSCATED_BASE64_ENCRYPTION_SECRET_KEY,
360
+ keyPair.secretKey,
361
+ );
362
+ await this.store.delete(STORAGE_KEYS.DEPRECATED___ENCRYPTION_SECRET_KEY);
363
+ await this.storeBase64.set(STORAGE_KEYS.ENCRYPTION_PUBLIC_KEY, keyPair.publicKey);
364
+
365
+ this.storedEncryptionProfile = {
366
+ userId,
367
+ password,
368
+ keyPair,
369
+ encryptionPasswordStore,
370
+ };
371
+
372
+ return this.storedEncryptionProfile;
373
+ }
374
+
375
+ protected async ensureMPCPassword(): Promise<string> {
376
+ if (this.options?.mode !== "new") {
377
+ const { status: downloadStatus, secret: downloadedPassword } = await this.downloadSecret();
378
+
379
+ if (downloadStatus === "ok" && downloadedPassword) {
380
+ return utf8Decode(downloadedPassword);
381
+ }
382
+
383
+ throw Error("A secret might be stored at MPC ZK nodes, but can't be obtained");
384
+ }
385
+
386
+ const password = generatePassword();
387
+ const { status: uploadStatus } = await this.uploadSecret(password);
388
+
389
+ if (uploadStatus !== "success") {
390
+ throw Error(`A secret upload failed with status: ${uploadStatus}`);
391
+ }
392
+
393
+ return password;
394
+ }
395
+
396
+ private get mpcClient(): MPCClient {
397
+ if (!this.mpcClientInstance) {
398
+ throw new Error("MPC client is not initialized");
399
+ }
400
+ return this.mpcClientInstance;
401
+ }
402
+
403
+ private async downloadSecret(): Promise<{
404
+ status: string;
405
+ secret: Buffer<ArrayBufferLike> | undefined;
406
+ }> {
407
+ // I really don't like this, I guess the walletAddress should be stored in the store.
408
+ if (!this.options.walletAddress) {
409
+ throw new Error("walletAddress is not found");
410
+ }
411
+
412
+ const ephemeralKeyPair = nacl.box.keyPair();
413
+
414
+ const downloadRequest = this.mpcClient.downloadRequest(ephemeralKeyPair.publicKey);
415
+
416
+ const messageToSign = this.mpcClient.downloadMessageToSign(
417
+ downloadRequest,
418
+ ) as DownloadMessageToSign;
419
+
420
+ const signedMessage = await this.signTypedData(
421
+ messageToSign.domain,
422
+ messageToSign.types,
423
+ messageToSign.value,
424
+ );
425
+
426
+ return this.mpcClient.downloadSecret(
427
+ this.userId,
428
+ downloadRequest,
429
+ signedMessage,
430
+ ephemeralKeyPair.secretKey,
431
+ );
432
+ }
433
+
434
+ private async uploadSecret(secret: string): Promise<{ status: string }> {
435
+ if (!this.options.walletAddress) {
436
+ console.error("signerAddress is not found");
437
+ return { status: "no-signer-address" };
438
+ }
439
+
440
+ const blindedShares = this.mpcClient.getBlindedShares(Buffer.from(secret, "utf8"));
441
+ const uploadRequest = this.mpcClient.uploadRequest(blindedShares);
442
+ const messageToSign = this.mpcClient.uploadMessageToSign(uploadRequest) as UploadMessageToSign;
443
+
444
+ const signedMessage = await this.signTypedData(
445
+ // oxlint-disable-next-line typescript/no-explicit-any -- TODO: Change this when we know how to MPC & other chains
446
+ messageToSign.domain as any,
447
+ // oxlint-disable-next-line typescript/no-explicit-any -- TODO: Change this when we know how to MPC & other chains
448
+ messageToSign.types as any,
449
+ // oxlint-disable-next-line typescript/no-explicit-any -- TODO: Change this when we know how to MPC & other chains
450
+ messageToSign.value as any,
451
+ );
452
+
453
+ return this.mpcClient.uploadSecret(this.userId, uploadRequest, signedMessage, blindedShares);
454
+ }
455
+
456
+ async addAddressMessageToSign(
457
+ address: string,
458
+ publicKey: string | undefined,
459
+ addressToAddType: string,
460
+ ): Promise<AddAddressMessageToSign> {
461
+ return this.mpcClient.addAddressMessageToSign(
462
+ address,
463
+ publicKey,
464
+ addressToAddType,
465
+ ) as AddAddressMessageToSign;
466
+ }
467
+
468
+ async removeAddressMessageToSign(
469
+ address: string,
470
+ publicKey: string | undefined,
471
+ addressToRemoveType: string,
472
+ ): Promise<RemoveAddressMessageToSign> {
473
+ return this.mpcClient.removeAddressMessageToSign(
474
+ address,
475
+ publicKey,
476
+ addressToRemoveType,
477
+ ) as RemoveAddressMessageToSign;
478
+ }
479
+
480
+ async addAddressToMpcSecret(
481
+ userId: string,
482
+ message: AddAddressSignatureMessage,
483
+ signature: string,
484
+ ): Promise<string> {
485
+ return this.mpcClient.addAddress(userId, message, signature);
486
+ }
487
+
488
+ async removeAddressFromMpcSecret(
489
+ userId: string,
490
+ message: RemoveAddressSignatureMessage,
491
+ signature: string,
492
+ ): Promise<string> {
493
+ return this.mpcClient.removeAddress(userId, message, signature);
494
+ }
495
+ }
package/src/mpc/api.ts ADDED
@@ -0,0 +1,122 @@
1
+ const postHeaders = {
2
+ Accept: "application/json, text/plain, */*",
3
+ "Content-Type": "application/json",
4
+ };
5
+
6
+ const getHeaders = {
7
+ Accept: "application/json, text/plain, */*",
8
+ };
9
+
10
+ export type RequestType = "GET" | "PUT" | "POST" | "PATCH";
11
+
12
+ function buildOptions<T>(method: RequestType, headers: RequestInit["headers"], entityBytes: T) {
13
+ const result: RequestInit = { method, headers, body: null };
14
+
15
+ if (entityBytes != null) {
16
+ result.body = JSON.stringify(entityBytes);
17
+ }
18
+ return result;
19
+ }
20
+
21
+ /**
22
+ * Make a http get-request.
23
+ *
24
+ * @param url the url to request.
25
+ * @return a promise containing the result of the get request.
26
+ */
27
+ export function getRequest<R>(url: string): Promise<{ status: string; body: R | undefined }> {
28
+ const options = buildOptions("GET", getHeaders, null);
29
+ return handleFetch<R>(fetch(url, options));
30
+ }
31
+
32
+ /**
33
+ * Make a http put-request.
34
+ *
35
+ * @param url the url to request.
36
+ * @param object the object to put.
37
+ * @param headers
38
+ * @return a promise containing whether the put succeeded or not.
39
+ */
40
+ export function putRequest<T>(
41
+ url: string,
42
+ object: T,
43
+ headers?: Record<string, string>,
44
+ ): Promise<string> {
45
+ const options = buildOptions("PUT", { ...postHeaders, ...headers }, object);
46
+ return fetch(url, options)
47
+ .then(async (response) => {
48
+ const responseClone = response.clone();
49
+ const responseBody = await responseClone.text();
50
+ console.log({ responseCode: response.status, responseBody });
51
+ return response.status.toString();
52
+ })
53
+ .catch((error) => {
54
+ console.error(error);
55
+ return error;
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Make a http patch-request.
61
+ *
62
+ * @param url the url to request.
63
+ * @param object the object to put.
64
+ * @param headers
65
+ * @return a promise containing whether the put succeeded or not.
66
+ */
67
+ export function patchRequest<T>(
68
+ url: string,
69
+ object: T,
70
+ headers?: RequestInit["headers"],
71
+ ): Promise<string> {
72
+ const options = buildOptions("PATCH", { ...postHeaders, ...headers }, object);
73
+ return fetch(url, options)
74
+ .then(async (response) => {
75
+ const responseClone = response.clone();
76
+ const responseBody = await responseClone.text();
77
+ console.log({ responseCode: response.status, responseBody });
78
+ return response.status.toString();
79
+ })
80
+ .catch((error) => {
81
+ console.error(error);
82
+ return error;
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Make a http post-request.
88
+ *
89
+ * @param url the url to request.
90
+ * @param object the object to post.
91
+ * @param headers
92
+ * @return a promise containing the result of the post request.
93
+ */
94
+ export function postRequest<T, R>(
95
+ url: string,
96
+ object: T,
97
+ headers?: Record<string, string>,
98
+ ): Promise<{ status: string; body: R | undefined }> {
99
+ const options = buildOptions("POST", { ...postHeaders, ...headers }, object);
100
+ return handleFetch<R>(fetch(url, options));
101
+ }
102
+
103
+ function handleFetch<T>(
104
+ promise: Promise<Response>,
105
+ ): Promise<{ status: string; body: T | undefined }> {
106
+ return promise
107
+ .then(async (response) => {
108
+ const responseClone = response.clone();
109
+ const responseBody = await responseClone.text();
110
+ console.log({ responseCode: response.status, responseBody });
111
+
112
+ if (response.status === 200) {
113
+ const data = (await response.json()) as T;
114
+ return { status: response.status.toString(), body: data };
115
+ }
116
+ return { status: response.status.toString(), body: undefined };
117
+ })
118
+ .catch((error) => {
119
+ console.error(error);
120
+ return { status: "fetch-error", body: undefined };
121
+ });
122
+ }