@arbidocs/sdk 0.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 +351 -0
- package/dist/index.cjs +1154 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +6994 -0
- package/dist/index.d.ts +6994 -0
- package/dist/index.js +1107 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1154 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var createFetchClient = require('openapi-fetch');
|
|
4
|
+
var sodium = require('libsodium-wrappers-sumo');
|
|
5
|
+
|
|
6
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var createFetchClient__default = /*#__PURE__*/_interopDefault(createFetchClient);
|
|
9
|
+
var sodium__default = /*#__PURE__*/_interopDefault(sodium);
|
|
10
|
+
|
|
11
|
+
// src/client.ts
|
|
12
|
+
|
|
13
|
+
// src/session.ts
|
|
14
|
+
function createInitialState() {
|
|
15
|
+
return {
|
|
16
|
+
accessToken: null,
|
|
17
|
+
userEmail: null,
|
|
18
|
+
userExtId: null,
|
|
19
|
+
selectedWorkspaceId: null,
|
|
20
|
+
cachedWorkspaceHeaders: {},
|
|
21
|
+
isSsoMode: false,
|
|
22
|
+
isAuth0Authenticated: false,
|
|
23
|
+
auth0AccessToken: null
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function createSessionManager() {
|
|
27
|
+
let state = createInitialState();
|
|
28
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
29
|
+
function notify() {
|
|
30
|
+
const snapshot = { ...state };
|
|
31
|
+
for (const listener of listeners) {
|
|
32
|
+
listener(snapshot);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
getState() {
|
|
37
|
+
return state;
|
|
38
|
+
},
|
|
39
|
+
setAccessToken(token) {
|
|
40
|
+
state = { ...state, accessToken: token };
|
|
41
|
+
notify();
|
|
42
|
+
},
|
|
43
|
+
setUser(email, extId) {
|
|
44
|
+
state = { ...state, userEmail: email, userExtId: extId ?? state.userExtId };
|
|
45
|
+
notify();
|
|
46
|
+
},
|
|
47
|
+
setSelectedWorkspace(id) {
|
|
48
|
+
state = { ...state, selectedWorkspaceId: id };
|
|
49
|
+
notify();
|
|
50
|
+
},
|
|
51
|
+
setCachedWorkspaceHeader(workspaceId, header) {
|
|
52
|
+
state = {
|
|
53
|
+
...state,
|
|
54
|
+
cachedWorkspaceHeaders: {
|
|
55
|
+
...state.cachedWorkspaceHeaders,
|
|
56
|
+
[workspaceId]: header
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
notify();
|
|
60
|
+
},
|
|
61
|
+
clearWorkspaceHeaders() {
|
|
62
|
+
state = { ...state, cachedWorkspaceHeaders: {} };
|
|
63
|
+
notify();
|
|
64
|
+
},
|
|
65
|
+
getWorkspaceKeyHeader() {
|
|
66
|
+
const { selectedWorkspaceId, cachedWorkspaceHeaders } = state;
|
|
67
|
+
if (!selectedWorkspaceId) return null;
|
|
68
|
+
return cachedWorkspaceHeaders[selectedWorkspaceId] ?? null;
|
|
69
|
+
},
|
|
70
|
+
setSsoState(opts) {
|
|
71
|
+
state = {
|
|
72
|
+
...state,
|
|
73
|
+
isSsoMode: opts.isSsoMode ?? state.isSsoMode,
|
|
74
|
+
isAuth0Authenticated: opts.isAuth0Authenticated ?? state.isAuth0Authenticated,
|
|
75
|
+
auth0AccessToken: opts.auth0AccessToken !== void 0 ? opts.auth0AccessToken : state.auth0AccessToken
|
|
76
|
+
};
|
|
77
|
+
notify();
|
|
78
|
+
},
|
|
79
|
+
clear() {
|
|
80
|
+
state = createInitialState();
|
|
81
|
+
notify();
|
|
82
|
+
},
|
|
83
|
+
subscribe(listener) {
|
|
84
|
+
listeners.add(listener);
|
|
85
|
+
return () => {
|
|
86
|
+
listeners.delete(listener);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function createTokenProvider(session) {
|
|
92
|
+
return {
|
|
93
|
+
getAccessToken: () => session.getState().accessToken
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function createWorkspaceKeyProvider(session) {
|
|
97
|
+
return {
|
|
98
|
+
getWorkspaceKeyHeader: () => session.getWorkspaceKeyHeader()
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function createAuthStateProvider(session) {
|
|
102
|
+
return {
|
|
103
|
+
getUserEmail: () => session.getState().userEmail,
|
|
104
|
+
getSsoState: () => {
|
|
105
|
+
const s = session.getState();
|
|
106
|
+
return {
|
|
107
|
+
isSsoMode: s.isSsoMode,
|
|
108
|
+
isAuth0Authenticated: s.isAuth0Authenticated,
|
|
109
|
+
auth0AccessToken: s.auth0AccessToken
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
getSelectedWorkspaceId: () => session.getState().selectedWorkspaceId,
|
|
113
|
+
setAccessToken: (token) => session.setAccessToken(token),
|
|
114
|
+
clearWorkspaceHeaders: () => session.clearWorkspaceHeaders(),
|
|
115
|
+
setCachedWorkspaceHeader: (workspaceId, header) => session.setCachedWorkspaceHeader(workspaceId, header)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
var sodiumReady = null;
|
|
119
|
+
async function initSodium() {
|
|
120
|
+
if (sodiumReady) {
|
|
121
|
+
await sodiumReady;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
sodiumReady = sodium__default.default.ready;
|
|
125
|
+
await sodiumReady;
|
|
126
|
+
}
|
|
127
|
+
function base64ToBytes(base64String) {
|
|
128
|
+
return sodium__default.default.from_base64(base64String, sodium__default.default.base64_variants.ORIGINAL);
|
|
129
|
+
}
|
|
130
|
+
function bytesToBase64(bytes) {
|
|
131
|
+
return sodium__default.default.to_base64(bytes, sodium__default.default.base64_variants.ORIGINAL);
|
|
132
|
+
}
|
|
133
|
+
function base64Encode(bytes) {
|
|
134
|
+
const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join("");
|
|
135
|
+
return btoa(binString);
|
|
136
|
+
}
|
|
137
|
+
function base64Decode(base64) {
|
|
138
|
+
const binString = atob(base64);
|
|
139
|
+
return Uint8Array.from(binString, (m) => m.codePointAt(0) || 0);
|
|
140
|
+
}
|
|
141
|
+
function generateSalt(username, deploymentDomain) {
|
|
142
|
+
if (!deploymentDomain) {
|
|
143
|
+
throw new Error("Deployment domain must be provided for salt generation");
|
|
144
|
+
}
|
|
145
|
+
const normalized = username.toLowerCase();
|
|
146
|
+
const saltString = `${normalized}|${deploymentDomain}`;
|
|
147
|
+
const hash = sodium__default.default.crypto_hash_sha256(sodium__default.default.from_string(saltString));
|
|
148
|
+
return hash.slice(0, 16);
|
|
149
|
+
}
|
|
150
|
+
async function generateKeyPairs(username, password, deploymentDomain) {
|
|
151
|
+
await initSodium();
|
|
152
|
+
if (!deploymentDomain) {
|
|
153
|
+
throw new Error("Deployment domain not available. Cannot generate keys.");
|
|
154
|
+
}
|
|
155
|
+
const salt = generateSalt(username, deploymentDomain);
|
|
156
|
+
const seed = sodium__default.default.crypto_pwhash(
|
|
157
|
+
32,
|
|
158
|
+
// 32 bytes for Ed25519 seed
|
|
159
|
+
password,
|
|
160
|
+
salt,
|
|
161
|
+
sodium__default.default.crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
|
162
|
+
sodium__default.default.crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
|
163
|
+
sodium__default.default.crypto_pwhash_ALG_ARGON2ID13
|
|
164
|
+
);
|
|
165
|
+
const signingKeyPair = sodium__default.default.crypto_sign_seed_keypair(seed);
|
|
166
|
+
const encryptionPublicKey = sodium__default.default.crypto_sign_ed25519_pk_to_curve25519(signingKeyPair.publicKey);
|
|
167
|
+
const encryptionPrivateKey = sodium__default.default.crypto_sign_ed25519_sk_to_curve25519(
|
|
168
|
+
signingKeyPair.privateKey
|
|
169
|
+
);
|
|
170
|
+
return {
|
|
171
|
+
signing: {
|
|
172
|
+
publicKey: signingKeyPair.publicKey,
|
|
173
|
+
privateKey: signingKeyPair.privateKey,
|
|
174
|
+
publicKeyBase64: sodium__default.default.to_base64(signingKeyPair.publicKey, sodium__default.default.base64_variants.ORIGINAL)
|
|
175
|
+
},
|
|
176
|
+
encryption: {
|
|
177
|
+
publicKey: encryptionPublicKey,
|
|
178
|
+
privateKey: encryptionPrivateKey,
|
|
179
|
+
publicKeyBase64: sodium__default.default.to_base64(encryptionPublicKey, sodium__default.default.base64_variants.ORIGINAL)
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function signMessage(message, privateKey) {
|
|
184
|
+
const messageBytes = sodium__default.default.from_string(message);
|
|
185
|
+
const signature = sodium__default.default.crypto_sign_detached(messageBytes, privateKey);
|
|
186
|
+
return base64Encode(signature);
|
|
187
|
+
}
|
|
188
|
+
async function createWorkspaceKeyHeader(workspaceKey, serverSessionPublicKey) {
|
|
189
|
+
await initSodium();
|
|
190
|
+
const encryptedKey = sealedBoxEncrypt(workspaceKey, serverSessionPublicKey);
|
|
191
|
+
return encryptedKey;
|
|
192
|
+
}
|
|
193
|
+
function sealedBoxDecrypt(encryptedBase64, userEncryptionPrivateKey) {
|
|
194
|
+
const encrypted = base64ToBytes(encryptedBase64);
|
|
195
|
+
const publicKey = sodium__default.default.crypto_scalarmult_base(userEncryptionPrivateKey);
|
|
196
|
+
const decrypted = sodium__default.default.crypto_box_seal_open(encrypted, publicKey, userEncryptionPrivateKey);
|
|
197
|
+
return decrypted;
|
|
198
|
+
}
|
|
199
|
+
function sealedBoxEncrypt(message, publicKey) {
|
|
200
|
+
const encrypted = sodium__default.default.crypto_box_seal(message, publicKey);
|
|
201
|
+
return bytesToBase64(encrypted);
|
|
202
|
+
}
|
|
203
|
+
function derivePublicKey(privateKey) {
|
|
204
|
+
return sodium__default.default.crypto_scalarmult_base(privateKey);
|
|
205
|
+
}
|
|
206
|
+
function deriveEncryptionKeypairFromSigning(signingKeyPair) {
|
|
207
|
+
const encryptionPublicKey = sodium__default.default.crypto_sign_ed25519_pk_to_curve25519(signingKeyPair.publicKey);
|
|
208
|
+
const encryptionPrivateKey = sodium__default.default.crypto_sign_ed25519_sk_to_curve25519(signingKeyPair.secretKey);
|
|
209
|
+
return {
|
|
210
|
+
publicKey: encryptionPublicKey,
|
|
211
|
+
secretKey: encryptionPrivateKey
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
async function computeSharedSecret(theirPublicKeyBase64, myPrivateKey) {
|
|
215
|
+
await initSodium();
|
|
216
|
+
const theirPublicKey = base64ToBytes(theirPublicKeyBase64);
|
|
217
|
+
return sodium__default.default.crypto_box_beforenm(theirPublicKey, myPrivateKey);
|
|
218
|
+
}
|
|
219
|
+
async function encryptMessageWithSharedSecret(message, sharedSecret) {
|
|
220
|
+
await initSodium();
|
|
221
|
+
const messageBytes = sodium__default.default.from_string(message);
|
|
222
|
+
const nonce = sodium__default.default.randombytes_buf(sodium__default.default.crypto_box_NONCEBYTES);
|
|
223
|
+
const ciphertext = sodium__default.default.crypto_box_easy_afternm(messageBytes, nonce, sharedSecret);
|
|
224
|
+
const combined = new Uint8Array(nonce.length + ciphertext.length);
|
|
225
|
+
combined.set(nonce, 0);
|
|
226
|
+
combined.set(ciphertext, nonce.length);
|
|
227
|
+
return bytesToBase64(combined);
|
|
228
|
+
}
|
|
229
|
+
async function decryptMessageWithSharedSecret(encryptedBase64, sharedSecret) {
|
|
230
|
+
await initSodium();
|
|
231
|
+
const combined = base64ToBytes(encryptedBase64);
|
|
232
|
+
const nonce = combined.slice(0, sodium__default.default.crypto_box_NONCEBYTES);
|
|
233
|
+
const ciphertext = combined.slice(sodium__default.default.crypto_box_NONCEBYTES);
|
|
234
|
+
const decrypted = sodium__default.default.crypto_box_open_easy_afternm(ciphertext, nonce, sharedSecret);
|
|
235
|
+
return sodium__default.default.to_string(decrypted);
|
|
236
|
+
}
|
|
237
|
+
async function encryptMessage(message, recipientPublicKeyBase64, senderPrivateKey) {
|
|
238
|
+
await initSodium();
|
|
239
|
+
const recipientPublicKey = base64ToBytes(recipientPublicKeyBase64);
|
|
240
|
+
const messageBytes = sodium__default.default.from_string(message);
|
|
241
|
+
const nonce = sodium__default.default.randombytes_buf(sodium__default.default.crypto_box_NONCEBYTES);
|
|
242
|
+
const ciphertext = sodium__default.default.crypto_box_easy(
|
|
243
|
+
messageBytes,
|
|
244
|
+
nonce,
|
|
245
|
+
recipientPublicKey,
|
|
246
|
+
senderPrivateKey
|
|
247
|
+
);
|
|
248
|
+
const combined = new Uint8Array(nonce.length + ciphertext.length);
|
|
249
|
+
combined.set(nonce, 0);
|
|
250
|
+
combined.set(ciphertext, nonce.length);
|
|
251
|
+
return bytesToBase64(combined);
|
|
252
|
+
}
|
|
253
|
+
async function decryptMessage(encryptedBase64, senderPublicKeyBase64, recipientPrivateKey) {
|
|
254
|
+
await initSodium();
|
|
255
|
+
const combined = base64ToBytes(encryptedBase64);
|
|
256
|
+
const senderPublicKey = base64ToBytes(senderPublicKeyBase64);
|
|
257
|
+
const nonce = combined.slice(0, sodium__default.default.crypto_box_NONCEBYTES);
|
|
258
|
+
const ciphertext = combined.slice(sodium__default.default.crypto_box_NONCEBYTES);
|
|
259
|
+
const decrypted = sodium__default.default.crypto_box_open_easy(
|
|
260
|
+
ciphertext,
|
|
261
|
+
nonce,
|
|
262
|
+
senderPublicKey,
|
|
263
|
+
recipientPrivateKey
|
|
264
|
+
);
|
|
265
|
+
return sodium__default.default.to_string(decrypted);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/crypto/keypairs.ts
|
|
269
|
+
async function generateUserKeypairs(username, password, deploymentDomain) {
|
|
270
|
+
if (!deploymentDomain) {
|
|
271
|
+
throw new Error("Deployment domain must be provided. Cannot generate keypairs.");
|
|
272
|
+
}
|
|
273
|
+
const keypairs = await generateKeyPairs(username, password, deploymentDomain);
|
|
274
|
+
const signingKeyPair = {
|
|
275
|
+
publicKey: keypairs.signing.publicKey,
|
|
276
|
+
secretKey: keypairs.signing.privateKey
|
|
277
|
+
};
|
|
278
|
+
const encryptionKeyPair = {
|
|
279
|
+
publicKey: keypairs.encryption.publicKey,
|
|
280
|
+
secretKey: keypairs.encryption.privateKey
|
|
281
|
+
};
|
|
282
|
+
return {
|
|
283
|
+
signingKeyPair,
|
|
284
|
+
encryptionKeyPair
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/crypto/credentials.ts
|
|
289
|
+
async function generateRegistrationCredentials(registrationData, password, deploymentDomain) {
|
|
290
|
+
const { signingKeyPair } = await generateUserKeypairs(
|
|
291
|
+
registrationData.email,
|
|
292
|
+
password,
|
|
293
|
+
deploymentDomain
|
|
294
|
+
);
|
|
295
|
+
const request = {
|
|
296
|
+
...registrationData,
|
|
297
|
+
signing_key: base64Encode(signingKeyPair.publicKey)
|
|
298
|
+
};
|
|
299
|
+
return { request, signingPrivateKey: signingKeyPair.secretKey };
|
|
300
|
+
}
|
|
301
|
+
async function generateLoginCredentials(loginData, password, deploymentDomain) {
|
|
302
|
+
const { signingKeyPair } = await generateUserKeypairs(loginData.email, password, deploymentDomain);
|
|
303
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
304
|
+
const message = `${loginData.email}|${timestamp}`;
|
|
305
|
+
const signature = signMessage(message, signingKeyPair.secretKey);
|
|
306
|
+
const request = {
|
|
307
|
+
...loginData,
|
|
308
|
+
signature,
|
|
309
|
+
timestamp
|
|
310
|
+
};
|
|
311
|
+
return { request, signingPrivateKey: signingKeyPair.secretKey };
|
|
312
|
+
}
|
|
313
|
+
function generateLoginCredentialsFromKey(loginData, signingPrivateKey) {
|
|
314
|
+
if (signingPrivateKey.length !== 64) {
|
|
315
|
+
throw new Error("Invalid signing key: must be exactly 64 bytes");
|
|
316
|
+
}
|
|
317
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
318
|
+
const message = `${loginData.email}|${timestamp}`;
|
|
319
|
+
const signature = signMessage(message, signingPrivateKey);
|
|
320
|
+
const request = {
|
|
321
|
+
...loginData,
|
|
322
|
+
signature,
|
|
323
|
+
timestamp
|
|
324
|
+
};
|
|
325
|
+
return { request, signingPrivateKey };
|
|
326
|
+
}
|
|
327
|
+
async function generatePasswordChangeCredentials(email, currentPassword, newPassword, deploymentDomain) {
|
|
328
|
+
const currentKeypairs = await generateUserKeypairs(email, currentPassword, deploymentDomain);
|
|
329
|
+
const newKeypairs = await generateUserKeypairs(email, newPassword, deploymentDomain);
|
|
330
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
331
|
+
const message = `${email}|${timestamp}`;
|
|
332
|
+
const signature = signMessage(message, currentKeypairs.signingKeyPair.secretKey);
|
|
333
|
+
const request = {
|
|
334
|
+
signature,
|
|
335
|
+
timestamp,
|
|
336
|
+
new_signing_key: base64Encode(newKeypairs.signingKeyPair.publicKey)
|
|
337
|
+
};
|
|
338
|
+
return { request, newSigningPrivateKey: newKeypairs.signingKeyPair.secretKey };
|
|
339
|
+
}
|
|
340
|
+
async function generateRecoveryPasswordChangeCredentials(email, currentSigningKey, newPassword, deploymentDomain) {
|
|
341
|
+
const newKeypairs = await generateUserKeypairs(email, newPassword, deploymentDomain);
|
|
342
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
343
|
+
const message = `${email}|${timestamp}`;
|
|
344
|
+
const signature = signMessage(message, currentSigningKey);
|
|
345
|
+
const request = {
|
|
346
|
+
signature,
|
|
347
|
+
timestamp,
|
|
348
|
+
new_signing_key: base64Encode(newKeypairs.signingKeyPair.publicKey)
|
|
349
|
+
};
|
|
350
|
+
return { request, newSigningPrivateKey: newKeypairs.signingKeyPair.secretKey };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/middleware/bearer-auth.ts
|
|
354
|
+
function createBearerAuthMiddleware(config) {
|
|
355
|
+
return {
|
|
356
|
+
async onRequest({ request }) {
|
|
357
|
+
const accessToken = config.tokenProvider.getAccessToken();
|
|
358
|
+
if (accessToken) {
|
|
359
|
+
request.headers.set("Authorization", `Bearer ${accessToken}`);
|
|
360
|
+
}
|
|
361
|
+
return request;
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/middleware/workspace-key.ts
|
|
367
|
+
function needsWorkspaceKey(url, urlConfig) {
|
|
368
|
+
if (urlConfig.excludePatterns.some((pattern) => url.includes(pattern))) {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
return urlConfig.includePatterns.some((pattern) => url.includes(pattern));
|
|
372
|
+
}
|
|
373
|
+
function createWorkspaceKeyMiddleware(config) {
|
|
374
|
+
return {
|
|
375
|
+
async onRequest({ request }) {
|
|
376
|
+
if (needsWorkspaceKey(request.url, config.urlConfig)) {
|
|
377
|
+
const workspaceHeader = config.workspaceKeyProvider.getWorkspaceKeyHeader();
|
|
378
|
+
if (workspaceHeader) {
|
|
379
|
+
request.headers.set("Workspace-Key", workspaceHeader);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return request;
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/middleware/auto-relogin.ts
|
|
388
|
+
function createAutoReloginMiddleware(config) {
|
|
389
|
+
return {
|
|
390
|
+
async onResponse({ response, request }) {
|
|
391
|
+
if (needsWorkspaceKey(request.url, config.urlConfig)) {
|
|
392
|
+
const hasWorkspaceKey = request.headers.has("Workspace-Key");
|
|
393
|
+
console.debug(
|
|
394
|
+
`[${request.method}] ${response.status}`,
|
|
395
|
+
request.url,
|
|
396
|
+
`Workspace-Key: ${hasWorkspaceKey ? "\u2713" : "\u2717"}`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
if (response.status === 400) {
|
|
400
|
+
const isSafeMethod = ["GET", "HEAD", "DELETE"].includes(request.method);
|
|
401
|
+
if (isSafeMethod) {
|
|
402
|
+
console.info("[API] 400 error on protected endpoint - triggering instant re-login");
|
|
403
|
+
const newToken = await config.reloginHandler();
|
|
404
|
+
if (newToken) {
|
|
405
|
+
const newRequest = request.clone();
|
|
406
|
+
newRequest.headers.set("Authorization", `Bearer ${newToken}`);
|
|
407
|
+
if (needsWorkspaceKey(request.url, config.urlConfig)) {
|
|
408
|
+
const workspaceHeader = config.workspaceKeyProvider.getWorkspaceKeyHeader();
|
|
409
|
+
if (workspaceHeader) {
|
|
410
|
+
newRequest.headers.set("Workspace-Key", workspaceHeader);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
console.info("[API] Retrying request after re-login");
|
|
414
|
+
return fetch(newRequest);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (response.status === 401) {
|
|
419
|
+
const excludePatterns = config.reloginExcludePatterns ?? [];
|
|
420
|
+
const isExcluded = excludePatterns.some((pattern) => request.url.includes(pattern));
|
|
421
|
+
if (!isExcluded) {
|
|
422
|
+
const newToken = await config.reloginHandler();
|
|
423
|
+
if (newToken) {
|
|
424
|
+
try {
|
|
425
|
+
const newRequest = request.clone();
|
|
426
|
+
newRequest.headers.set("Authorization", `Bearer ${newToken}`);
|
|
427
|
+
if (needsWorkspaceKey(request.url, config.urlConfig)) {
|
|
428
|
+
const workspaceHeader = config.workspaceKeyProvider.getWorkspaceKeyHeader();
|
|
429
|
+
if (workspaceHeader) {
|
|
430
|
+
newRequest.headers.set("Workspace-Key", workspaceHeader);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
console.info("[API] Retrying request after re-login:", request.method, request.url);
|
|
434
|
+
return fetch(newRequest);
|
|
435
|
+
} catch (cloneError) {
|
|
436
|
+
console.warn("[API] Could not clone request for retry:", cloneError);
|
|
437
|
+
config.onRetryCloneFailed?.();
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return response;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/relogin/handler.ts
|
|
448
|
+
function createReloginHandler(deps) {
|
|
449
|
+
let reloginPromise = null;
|
|
450
|
+
return function instantReLogin() {
|
|
451
|
+
if (reloginPromise) {
|
|
452
|
+
console.info("[API] Re-login already in progress, waiting...");
|
|
453
|
+
return reloginPromise;
|
|
454
|
+
}
|
|
455
|
+
reloginPromise = (async () => {
|
|
456
|
+
try {
|
|
457
|
+
await deps.crypto.ensureReady();
|
|
458
|
+
const userEmail = deps.authState.getUserEmail();
|
|
459
|
+
if (!userEmail) {
|
|
460
|
+
console.warn("[API] No user email found for instant re-login");
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
const session = await deps.sessionStorage.getSession();
|
|
464
|
+
if (!session) {
|
|
465
|
+
console.warn("[API] No session found for instant re-login");
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
const ed25519PublicKey = session.signingPrivateKey.slice(32, 64);
|
|
469
|
+
const encryptionKeyPair = deps.crypto.deriveEncryptionKeypair({
|
|
470
|
+
publicKey: ed25519PublicKey,
|
|
471
|
+
secretKey: session.signingPrivateKey
|
|
472
|
+
});
|
|
473
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
474
|
+
const message = `${userEmail}|${timestamp}`;
|
|
475
|
+
const signature = deps.crypto.signMessage(message, session.signingPrivateKey);
|
|
476
|
+
console.info("[API] Attempting instant re-login");
|
|
477
|
+
const ssoState = deps.authState.getSsoState();
|
|
478
|
+
console.info("[API] Re-login SSO mode:", ssoState.isSsoMode);
|
|
479
|
+
console.info("[API] Auth0 authenticated:", ssoState.isAuth0Authenticated);
|
|
480
|
+
console.info("[API] Persisted Auth0 token available:", !!ssoState.auth0AccessToken);
|
|
481
|
+
const isSsoAccount = ssoState.isSsoMode || ssoState.isAuth0Authenticated || !!ssoState.auth0AccessToken;
|
|
482
|
+
let ssoToken = null;
|
|
483
|
+
if (deps.ssoTokenProvider) {
|
|
484
|
+
console.info("[API] SSO token provider available: true");
|
|
485
|
+
try {
|
|
486
|
+
ssoToken = await deps.ssoTokenProvider.getToken();
|
|
487
|
+
console.info(
|
|
488
|
+
"[API] Got SSO token from provider:",
|
|
489
|
+
ssoToken ? "yes (length: " + ssoToken.length + ")" : "no"
|
|
490
|
+
);
|
|
491
|
+
} catch (error) {
|
|
492
|
+
console.error("[API] Failed to get SSO token from provider:", error);
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
console.info("[API] SSO token provider available: false");
|
|
496
|
+
}
|
|
497
|
+
if (!ssoToken && ssoState.auth0AccessToken) {
|
|
498
|
+
ssoToken = ssoState.auth0AccessToken;
|
|
499
|
+
console.info(
|
|
500
|
+
"[API] Using persisted SSO token (length:",
|
|
501
|
+
ssoToken.length,
|
|
502
|
+
") - may be expired"
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
if (!ssoToken && isSsoAccount) {
|
|
506
|
+
console.warn("[API] SSO account requires token but none available - aborting re-login");
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
const loginResult = await deps.loginProvider.login({
|
|
510
|
+
email: userEmail,
|
|
511
|
+
signature,
|
|
512
|
+
timestamp,
|
|
513
|
+
ssoToken: ssoToken ?? void 0
|
|
514
|
+
});
|
|
515
|
+
if (!loginResult) {
|
|
516
|
+
console.warn("[API] Instant re-login failed");
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
await deps.sessionStorage.saveSession({
|
|
520
|
+
signingPrivateKey: session.signingPrivateKey,
|
|
521
|
+
serverSessionKey: loginResult.sessionKey,
|
|
522
|
+
userExtId: loginResult.userExtId
|
|
523
|
+
});
|
|
524
|
+
deps.onReloginSuccess?.({
|
|
525
|
+
email: userEmail,
|
|
526
|
+
accessToken: loginResult.accessToken,
|
|
527
|
+
userExtId: loginResult.userExtId,
|
|
528
|
+
serverSessionKey: loginResult.sessionKey
|
|
529
|
+
});
|
|
530
|
+
deps.authState.setAccessToken(loginResult.accessToken);
|
|
531
|
+
deps.authState.clearWorkspaceHeaders();
|
|
532
|
+
const selectedWorkspaceId = deps.authState.getSelectedWorkspaceId();
|
|
533
|
+
if (selectedWorkspaceId) {
|
|
534
|
+
try {
|
|
535
|
+
const wrappedKey = await deps.workspaceKeyRefreshProvider.getWrappedKey(
|
|
536
|
+
loginResult.accessToken,
|
|
537
|
+
selectedWorkspaceId
|
|
538
|
+
);
|
|
539
|
+
if (wrappedKey) {
|
|
540
|
+
const workspaceKey = deps.crypto.sealedBoxDecrypt(
|
|
541
|
+
wrappedKey,
|
|
542
|
+
encryptionKeyPair.secretKey
|
|
543
|
+
);
|
|
544
|
+
const workspaceKeyHeader = await deps.crypto.createWorkspaceKeyHeader(
|
|
545
|
+
workspaceKey,
|
|
546
|
+
loginResult.sessionKey
|
|
547
|
+
);
|
|
548
|
+
deps.authState.setCachedWorkspaceHeader(selectedWorkspaceId, workspaceKeyHeader);
|
|
549
|
+
}
|
|
550
|
+
} catch (error) {
|
|
551
|
+
console.error("[API] Failed to regenerate workspace header:", error);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
console.info("[API] Re-login successful");
|
|
555
|
+
return loginResult.accessToken;
|
|
556
|
+
} catch (error) {
|
|
557
|
+
console.error("[API] Instant re-login error:", error);
|
|
558
|
+
return null;
|
|
559
|
+
} finally {
|
|
560
|
+
reloginPromise = null;
|
|
561
|
+
}
|
|
562
|
+
})();
|
|
563
|
+
return reloginPromise;
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/storage/indexed-db.ts
|
|
568
|
+
var DB_NAME = "arbi-crypto";
|
|
569
|
+
var DB_VERSION = 2;
|
|
570
|
+
var db = null;
|
|
571
|
+
async function initializeDatabase() {
|
|
572
|
+
return new Promise((resolve, reject) => {
|
|
573
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
574
|
+
request.onerror = () => {
|
|
575
|
+
console.error("[STORAGE] Failed to open IndexedDB:", request.error);
|
|
576
|
+
reject(request.error);
|
|
577
|
+
};
|
|
578
|
+
request.onsuccess = () => {
|
|
579
|
+
db = request.result;
|
|
580
|
+
resolve(db);
|
|
581
|
+
};
|
|
582
|
+
request.onupgradeneeded = (event) => {
|
|
583
|
+
const database = event.target.result;
|
|
584
|
+
if (!database.objectStoreNames.contains("session")) {
|
|
585
|
+
database.createObjectStore("session", { keyPath: "id" });
|
|
586
|
+
}
|
|
587
|
+
if (!database.objectStoreNames.contains("wrapping-key")) {
|
|
588
|
+
database.createObjectStore("wrapping-key", { keyPath: "id" });
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
async function ensureDatabase() {
|
|
594
|
+
if (!db) {
|
|
595
|
+
await initializeDatabase();
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
async function getOrCreateWrappingKey() {
|
|
599
|
+
await ensureDatabase();
|
|
600
|
+
if (!db) {
|
|
601
|
+
throw new Error("Database not initialized");
|
|
602
|
+
}
|
|
603
|
+
return new Promise((resolve, reject) => {
|
|
604
|
+
if (!db) {
|
|
605
|
+
reject(new Error("Database not initialized"));
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const transaction = db.transaction(["wrapping-key"], "readwrite");
|
|
609
|
+
const store = transaction.objectStore("wrapping-key");
|
|
610
|
+
const request = store.get("master");
|
|
611
|
+
request.onsuccess = () => {
|
|
612
|
+
const result = request.result;
|
|
613
|
+
if (result?.key) {
|
|
614
|
+
resolve(result.key);
|
|
615
|
+
} else {
|
|
616
|
+
crypto.subtle.generateKey(
|
|
617
|
+
{
|
|
618
|
+
name: "AES-GCM",
|
|
619
|
+
length: 256
|
|
620
|
+
},
|
|
621
|
+
false,
|
|
622
|
+
// ❌ NON-EXTRACTABLE - Can't be exported!
|
|
623
|
+
["encrypt", "decrypt"]
|
|
624
|
+
).then((key) => {
|
|
625
|
+
if (!db) {
|
|
626
|
+
reject(new Error("Database not initialized"));
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const putTransaction = db.transaction(["wrapping-key"], "readwrite");
|
|
630
|
+
const putStore = putTransaction.objectStore("wrapping-key");
|
|
631
|
+
const putRequest = putStore.put({
|
|
632
|
+
id: "master",
|
|
633
|
+
key,
|
|
634
|
+
created: Date.now()
|
|
635
|
+
});
|
|
636
|
+
putRequest.onsuccess = () => {
|
|
637
|
+
resolve(key);
|
|
638
|
+
};
|
|
639
|
+
putRequest.onerror = () => {
|
|
640
|
+
reject(new Error("Failed to store wrapping key"));
|
|
641
|
+
};
|
|
642
|
+
}).catch((error) => {
|
|
643
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
644
|
+
reject(new Error(`Failed to generate wrapping key: ${errorMsg}`));
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
request.onerror = () => {
|
|
649
|
+
reject(new Error("Failed to retrieve wrapping key from IndexedDB"));
|
|
650
|
+
};
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
async function encryptWithWrappingKey(data) {
|
|
654
|
+
const wrappingKey = await getOrCreateWrappingKey();
|
|
655
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
656
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
657
|
+
{
|
|
658
|
+
name: "AES-GCM",
|
|
659
|
+
iv
|
|
660
|
+
},
|
|
661
|
+
wrappingKey,
|
|
662
|
+
data
|
|
663
|
+
);
|
|
664
|
+
return {
|
|
665
|
+
ciphertext: new Uint8Array(ciphertext),
|
|
666
|
+
iv
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
async function decryptWithWrappingKey(ciphertext, iv) {
|
|
670
|
+
const wrappingKey = await getOrCreateWrappingKey();
|
|
671
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
672
|
+
{
|
|
673
|
+
name: "AES-GCM",
|
|
674
|
+
iv
|
|
675
|
+
},
|
|
676
|
+
wrappingKey,
|
|
677
|
+
ciphertext
|
|
678
|
+
);
|
|
679
|
+
return new Uint8Array(decrypted);
|
|
680
|
+
}
|
|
681
|
+
async function saveSession(sessionData) {
|
|
682
|
+
await ensureDatabase();
|
|
683
|
+
if (!db) {
|
|
684
|
+
throw new Error("Database not initialized");
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
const encryptedSigningKey = await encryptWithWrappingKey(sessionData.signingPrivateKey);
|
|
688
|
+
let encryptedServerSessionKey;
|
|
689
|
+
if (sessionData.serverSessionKey) {
|
|
690
|
+
encryptedServerSessionKey = await encryptWithWrappingKey(sessionData.serverSessionKey);
|
|
691
|
+
}
|
|
692
|
+
return new Promise((resolve, reject) => {
|
|
693
|
+
if (!db) {
|
|
694
|
+
reject(new Error("Database not initialized"));
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const transaction = db.transaction(["session"], "readwrite");
|
|
698
|
+
const store = transaction.objectStore("session");
|
|
699
|
+
const data = {
|
|
700
|
+
id: "current",
|
|
701
|
+
encryptedSigningPrivateKey: encryptedSigningKey.ciphertext,
|
|
702
|
+
signingPrivateKeyIV: encryptedSigningKey.iv,
|
|
703
|
+
encryptedServerSessionKey: encryptedServerSessionKey?.ciphertext,
|
|
704
|
+
serverSessionKeyIV: encryptedServerSessionKey?.iv,
|
|
705
|
+
userExtId: sessionData.userExtId,
|
|
706
|
+
timestamp: Date.now()
|
|
707
|
+
};
|
|
708
|
+
const request = store.put(data);
|
|
709
|
+
request.onsuccess = () => {
|
|
710
|
+
resolve();
|
|
711
|
+
};
|
|
712
|
+
request.onerror = () => {
|
|
713
|
+
console.error("[STORAGE] Failed to save session:", request.error);
|
|
714
|
+
reject(request.error);
|
|
715
|
+
};
|
|
716
|
+
});
|
|
717
|
+
} catch (error) {
|
|
718
|
+
console.error("[STORAGE] Failed to encrypt session data:", error);
|
|
719
|
+
throw error;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
async function getSession() {
|
|
723
|
+
await ensureDatabase();
|
|
724
|
+
if (!db) {
|
|
725
|
+
throw new Error("Database not initialized");
|
|
726
|
+
}
|
|
727
|
+
return new Promise((resolve, reject) => {
|
|
728
|
+
if (!db) {
|
|
729
|
+
reject(new Error("Database not initialized"));
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const transaction = db.transaction(["session"], "readonly");
|
|
733
|
+
const store = transaction.objectStore("session");
|
|
734
|
+
const request = store.get("current");
|
|
735
|
+
request.onsuccess = async () => {
|
|
736
|
+
const result = request.result;
|
|
737
|
+
if (result) {
|
|
738
|
+
try {
|
|
739
|
+
const signingPrivateKey = await decryptWithWrappingKey(
|
|
740
|
+
result.encryptedSigningPrivateKey,
|
|
741
|
+
result.signingPrivateKeyIV
|
|
742
|
+
);
|
|
743
|
+
let serverSessionKey;
|
|
744
|
+
if (result.encryptedServerSessionKey && result.serverSessionKeyIV) {
|
|
745
|
+
serverSessionKey = await decryptWithWrappingKey(
|
|
746
|
+
result.encryptedServerSessionKey,
|
|
747
|
+
result.serverSessionKeyIV
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
resolve({
|
|
751
|
+
signingPrivateKey,
|
|
752
|
+
serverSessionKey,
|
|
753
|
+
userExtId: result.userExtId
|
|
754
|
+
});
|
|
755
|
+
} catch (error) {
|
|
756
|
+
console.error("[STORAGE] Failed to decrypt session data:", error);
|
|
757
|
+
reject(error);
|
|
758
|
+
}
|
|
759
|
+
} else {
|
|
760
|
+
resolve(null);
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
request.onerror = () => {
|
|
764
|
+
console.error("[STORAGE] Failed to get session:", request.error);
|
|
765
|
+
reject(request.error);
|
|
766
|
+
};
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
async function updateSigningPrivateKey(newSigningPrivateKey) {
|
|
770
|
+
await ensureDatabase();
|
|
771
|
+
try {
|
|
772
|
+
const existingSession = await getSession();
|
|
773
|
+
if (!existingSession) {
|
|
774
|
+
throw new Error("No existing session to update");
|
|
775
|
+
}
|
|
776
|
+
await saveSession({
|
|
777
|
+
signingPrivateKey: newSigningPrivateKey,
|
|
778
|
+
serverSessionKey: existingSession.serverSessionKey,
|
|
779
|
+
userExtId: existingSession.userExtId
|
|
780
|
+
});
|
|
781
|
+
} catch (error) {
|
|
782
|
+
console.error("[STORAGE] Failed to update signing private key:", error);
|
|
783
|
+
throw error;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
async function clearSession() {
|
|
787
|
+
await ensureDatabase();
|
|
788
|
+
if (!db) {
|
|
789
|
+
throw new Error("Database not initialized");
|
|
790
|
+
}
|
|
791
|
+
return new Promise((resolve, reject) => {
|
|
792
|
+
if (!db) {
|
|
793
|
+
reject(new Error("Database not initialized"));
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const transaction = db.transaction(["session"], "readwrite");
|
|
797
|
+
const store = transaction.objectStore("session");
|
|
798
|
+
const request = store.delete("current");
|
|
799
|
+
request.onsuccess = () => {
|
|
800
|
+
resolve();
|
|
801
|
+
};
|
|
802
|
+
request.onerror = () => {
|
|
803
|
+
console.error("[STORAGE] Failed to clear session:", request.error);
|
|
804
|
+
reject(request.error);
|
|
805
|
+
};
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
async function clearWrappingKey() {
|
|
809
|
+
await ensureDatabase();
|
|
810
|
+
if (!db) {
|
|
811
|
+
throw new Error("Database not initialized");
|
|
812
|
+
}
|
|
813
|
+
return new Promise((resolve, reject) => {
|
|
814
|
+
if (!db) {
|
|
815
|
+
reject(new Error("Database not initialized"));
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const transaction = db.transaction(["wrapping-key"], "readwrite");
|
|
819
|
+
const store = transaction.objectStore("wrapping-key");
|
|
820
|
+
const request = store.delete("master");
|
|
821
|
+
request.onsuccess = () => {
|
|
822
|
+
resolve();
|
|
823
|
+
};
|
|
824
|
+
request.onerror = () => {
|
|
825
|
+
console.error("[STORAGE] Failed to clear wrapping key:", request.error);
|
|
826
|
+
reject(request.error);
|
|
827
|
+
};
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
async function clearAllData() {
|
|
831
|
+
await clearSession();
|
|
832
|
+
await clearWrappingKey();
|
|
833
|
+
}
|
|
834
|
+
async function hasSession() {
|
|
835
|
+
const session = await getSession();
|
|
836
|
+
return session !== null;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// src/auth/register.ts
|
|
840
|
+
async function register(params, deps) {
|
|
841
|
+
const credentials = await generateRegistrationCredentials(
|
|
842
|
+
{
|
|
843
|
+
email: params.email,
|
|
844
|
+
verification_credential: params.verificationCode,
|
|
845
|
+
given_name: params.firstName,
|
|
846
|
+
family_name: params.lastName,
|
|
847
|
+
picture: params.picture
|
|
848
|
+
},
|
|
849
|
+
params.password,
|
|
850
|
+
deps.deploymentDomain
|
|
851
|
+
);
|
|
852
|
+
const response = await deps.fetchClient.POST("/api/user/register", {
|
|
853
|
+
body: credentials.request
|
|
854
|
+
});
|
|
855
|
+
if (response.error) {
|
|
856
|
+
throw new Error(
|
|
857
|
+
`Registration failed: ${typeof response.error === "object" && response.error !== null && "detail" in response.error ? response.error.detail : "Unknown error"}`
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
return { signingPrivateKey: credentials.signingPrivateKey };
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// src/auth/login.ts
|
|
864
|
+
async function login(params, deps) {
|
|
865
|
+
const credentials = await generateLoginCredentials(
|
|
866
|
+
{
|
|
867
|
+
email: params.email,
|
|
868
|
+
sso_token: params.ssoToken
|
|
869
|
+
},
|
|
870
|
+
params.password,
|
|
871
|
+
deps.deploymentDomain
|
|
872
|
+
);
|
|
873
|
+
return performLogin(credentials.request, credentials.signingPrivateKey, deps);
|
|
874
|
+
}
|
|
875
|
+
async function loginWithKey(params, deps) {
|
|
876
|
+
const credentials = generateLoginCredentialsFromKey(
|
|
877
|
+
{
|
|
878
|
+
email: params.email,
|
|
879
|
+
sso_token: params.ssoToken
|
|
880
|
+
},
|
|
881
|
+
params.signingPrivateKey
|
|
882
|
+
);
|
|
883
|
+
return performLogin(credentials.request, credentials.signingPrivateKey, deps);
|
|
884
|
+
}
|
|
885
|
+
async function performLogin(request, signingPrivateKey, deps) {
|
|
886
|
+
const response = await deps.fetchClient.POST("/api/user/login", {
|
|
887
|
+
body: request
|
|
888
|
+
});
|
|
889
|
+
if (response.error || !response.data) {
|
|
890
|
+
throw new Error(
|
|
891
|
+
`Login failed: ${typeof response.error === "object" && response.error !== null && "detail" in response.error ? response.error.detail : "Unknown error"}`
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
const data = response.data;
|
|
895
|
+
const serverSessionKey = base64ToBytes(data.session_key);
|
|
896
|
+
await saveSession({
|
|
897
|
+
signingPrivateKey,
|
|
898
|
+
serverSessionKey,
|
|
899
|
+
userExtId: data.user.external_id ?? void 0
|
|
900
|
+
});
|
|
901
|
+
deps.session.setAccessToken(data.access_token);
|
|
902
|
+
deps.session.setUser(request.email, data.user.external_id ?? void 0);
|
|
903
|
+
return {
|
|
904
|
+
accessToken: data.access_token,
|
|
905
|
+
userExtId: data.user.external_id ?? void 0,
|
|
906
|
+
signingPrivateKey,
|
|
907
|
+
serverSessionKey
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// src/auth/logout.ts
|
|
912
|
+
async function logout(session) {
|
|
913
|
+
session.clear();
|
|
914
|
+
await clearAllData();
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// src/auth/change-password.ts
|
|
918
|
+
async function changePassword(params, deps) {
|
|
919
|
+
const credentials = await generatePasswordChangeCredentials(
|
|
920
|
+
params.email,
|
|
921
|
+
params.currentPassword,
|
|
922
|
+
params.newPassword,
|
|
923
|
+
deps.deploymentDomain
|
|
924
|
+
);
|
|
925
|
+
const response = await deps.fetchClient.POST("/api/user/change_password", {
|
|
926
|
+
body: {
|
|
927
|
+
...credentials.request,
|
|
928
|
+
rewrapped_workspace_keys: params.rewrappedWorkspaceKeys ?? {}
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
if (response.error) {
|
|
932
|
+
throw new Error(
|
|
933
|
+
`Password change failed: ${typeof response.error === "object" && response.error !== null && "detail" in response.error ? response.error.detail : "Unknown error"}`
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
await updateSigningPrivateKey(credentials.newSigningPrivateKey);
|
|
937
|
+
return { newSigningPrivateKey: credentials.newSigningPrivateKey };
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// src/client.ts
|
|
941
|
+
var DEFAULT_WORKSPACE_KEY_URL_CONFIG = {
|
|
942
|
+
excludePatterns: ["/api/user/", "/api/health/", "/api/configs/", "/api/workspace/create"],
|
|
943
|
+
includePatterns: [
|
|
944
|
+
"/api/workspace/wrk-",
|
|
945
|
+
"/api/document/",
|
|
946
|
+
"/api/conversation/",
|
|
947
|
+
"/api/assistant/",
|
|
948
|
+
"/api/tag/"
|
|
949
|
+
]
|
|
950
|
+
};
|
|
951
|
+
function createArbiClient(options) {
|
|
952
|
+
const {
|
|
953
|
+
baseUrl,
|
|
954
|
+
deploymentDomain,
|
|
955
|
+
workspaceKeyUrlConfig = DEFAULT_WORKSPACE_KEY_URL_CONFIG,
|
|
956
|
+
reloginExcludePatterns = ["/api/user/login"],
|
|
957
|
+
credentials = "include",
|
|
958
|
+
ssoTokenProvider = null,
|
|
959
|
+
onReloginSuccess
|
|
960
|
+
} = options;
|
|
961
|
+
const session = createSessionManager();
|
|
962
|
+
const tokenProvider = createTokenProvider(session);
|
|
963
|
+
const workspaceKeyProvider = createWorkspaceKeyProvider(session);
|
|
964
|
+
const authState = createAuthStateProvider(session);
|
|
965
|
+
const cryptoProvider = {
|
|
966
|
+
ensureReady: initSodium,
|
|
967
|
+
signMessage,
|
|
968
|
+
deriveEncryptionKeypair: deriveEncryptionKeypairFromSigning,
|
|
969
|
+
sealedBoxDecrypt,
|
|
970
|
+
createWorkspaceKeyHeader,
|
|
971
|
+
fromBase64: base64ToBytes
|
|
972
|
+
};
|
|
973
|
+
const loginProvider = {
|
|
974
|
+
async login(payload) {
|
|
975
|
+
const rawFetch = createFetchClient__default.default({
|
|
976
|
+
baseUrl,
|
|
977
|
+
credentials
|
|
978
|
+
});
|
|
979
|
+
const response = await rawFetch.POST("/api/user/login", {
|
|
980
|
+
body: {
|
|
981
|
+
email: payload.email,
|
|
982
|
+
signature: payload.signature,
|
|
983
|
+
timestamp: payload.timestamp,
|
|
984
|
+
sso_token: payload.ssoToken
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
if (response.error || !response.data) {
|
|
988
|
+
console.warn("[API] Login call failed:", response.error);
|
|
989
|
+
return null;
|
|
990
|
+
}
|
|
991
|
+
const data = response.data;
|
|
992
|
+
return {
|
|
993
|
+
accessToken: data.access_token,
|
|
994
|
+
sessionKey: base64ToBytes(data.session_key),
|
|
995
|
+
userExtId: data.user.external_id ?? void 0
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
const workspaceKeyRefreshProvider = {
|
|
1000
|
+
async getWrappedKey(accessToken, workspaceId) {
|
|
1001
|
+
const workspacesResponse = await fetch(`${baseUrl}/api/user/workspaces`, {
|
|
1002
|
+
method: "GET",
|
|
1003
|
+
headers: {
|
|
1004
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1005
|
+
"Content-Type": "application/json"
|
|
1006
|
+
},
|
|
1007
|
+
credentials
|
|
1008
|
+
});
|
|
1009
|
+
if (!workspacesResponse.ok) return null;
|
|
1010
|
+
const workspacesData = await workspacesResponse.json();
|
|
1011
|
+
const workspace = workspacesData.find(
|
|
1012
|
+
(w) => w.external_id === workspaceId
|
|
1013
|
+
);
|
|
1014
|
+
return workspace?.wrapped_key ?? null;
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
const reloginHandler = createReloginHandler({
|
|
1018
|
+
crypto: cryptoProvider,
|
|
1019
|
+
sessionStorage: { getSession, saveSession },
|
|
1020
|
+
authState,
|
|
1021
|
+
ssoTokenProvider,
|
|
1022
|
+
loginProvider,
|
|
1023
|
+
workspaceKeyRefreshProvider,
|
|
1024
|
+
onReloginSuccess
|
|
1025
|
+
});
|
|
1026
|
+
const fetchClient = createFetchClient__default.default({
|
|
1027
|
+
baseUrl,
|
|
1028
|
+
credentials
|
|
1029
|
+
});
|
|
1030
|
+
fetchClient.use(createBearerAuthMiddleware({ tokenProvider }));
|
|
1031
|
+
fetchClient.use(
|
|
1032
|
+
createWorkspaceKeyMiddleware({
|
|
1033
|
+
workspaceKeyProvider,
|
|
1034
|
+
urlConfig: workspaceKeyUrlConfig
|
|
1035
|
+
})
|
|
1036
|
+
);
|
|
1037
|
+
fetchClient.use(
|
|
1038
|
+
createAutoReloginMiddleware({
|
|
1039
|
+
reloginHandler,
|
|
1040
|
+
workspaceKeyProvider,
|
|
1041
|
+
urlConfig: workspaceKeyUrlConfig,
|
|
1042
|
+
reloginExcludePatterns
|
|
1043
|
+
})
|
|
1044
|
+
);
|
|
1045
|
+
const authDeps = {
|
|
1046
|
+
fetchClient,
|
|
1047
|
+
session,
|
|
1048
|
+
deploymentDomain
|
|
1049
|
+
};
|
|
1050
|
+
return {
|
|
1051
|
+
fetch: fetchClient,
|
|
1052
|
+
session,
|
|
1053
|
+
auth: {
|
|
1054
|
+
register: (params) => register(params, authDeps),
|
|
1055
|
+
login: (params) => login(params, authDeps),
|
|
1056
|
+
loginWithKey: (params) => loginWithKey(params, authDeps),
|
|
1057
|
+
logout: () => logout(session),
|
|
1058
|
+
changePassword: (params) => changePassword(params, authDeps),
|
|
1059
|
+
relogin: reloginHandler
|
|
1060
|
+
},
|
|
1061
|
+
crypto: {
|
|
1062
|
+
initSodium,
|
|
1063
|
+
generateUserKeypairs: (email, password) => generateUserKeypairs(email, password, deploymentDomain),
|
|
1064
|
+
signMessage,
|
|
1065
|
+
sealedBoxDecrypt,
|
|
1066
|
+
sealedBoxEncrypt,
|
|
1067
|
+
createWorkspaceKeyHeader,
|
|
1068
|
+
deriveEncryptionKeypairFromSigning,
|
|
1069
|
+
derivePublicKey,
|
|
1070
|
+
base64ToBytes,
|
|
1071
|
+
bytesToBase64,
|
|
1072
|
+
base64Encode,
|
|
1073
|
+
base64Decode,
|
|
1074
|
+
computeSharedSecret,
|
|
1075
|
+
encryptMessage,
|
|
1076
|
+
decryptMessage,
|
|
1077
|
+
encryptMessageWithSharedSecret,
|
|
1078
|
+
decryptMessageWithSharedSecret
|
|
1079
|
+
},
|
|
1080
|
+
storage: {
|
|
1081
|
+
getSession,
|
|
1082
|
+
saveSession
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// src/websocket.ts
|
|
1088
|
+
function buildWebSocketUrl(baseUrl) {
|
|
1089
|
+
const wsScheme = baseUrl.startsWith("https") ? "wss" : "ws";
|
|
1090
|
+
const hostAndPath = baseUrl.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
1091
|
+
return `${wsScheme}://${hostAndPath}/api/notifications/ws`;
|
|
1092
|
+
}
|
|
1093
|
+
function createAuthMessage(accessToken) {
|
|
1094
|
+
const msg = { type: "auth", token: accessToken };
|
|
1095
|
+
return JSON.stringify(msg);
|
|
1096
|
+
}
|
|
1097
|
+
function parseServerMessage(data) {
|
|
1098
|
+
try {
|
|
1099
|
+
const parsed = JSON.parse(data);
|
|
1100
|
+
if (typeof parsed === "object" && parsed !== null && typeof parsed.type === "string") {
|
|
1101
|
+
return parsed;
|
|
1102
|
+
}
|
|
1103
|
+
return null;
|
|
1104
|
+
} catch {
|
|
1105
|
+
return null;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function isMessageType(msg, type) {
|
|
1109
|
+
return msg.type === type;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
exports.base64Decode = base64Decode;
|
|
1113
|
+
exports.base64Encode = base64Encode;
|
|
1114
|
+
exports.base64ToBytes = base64ToBytes;
|
|
1115
|
+
exports.buildWebSocketUrl = buildWebSocketUrl;
|
|
1116
|
+
exports.bytesToBase64 = bytesToBase64;
|
|
1117
|
+
exports.clearAllData = clearAllData;
|
|
1118
|
+
exports.clearSession = clearSession;
|
|
1119
|
+
exports.computeSharedSecret = computeSharedSecret;
|
|
1120
|
+
exports.createArbiClient = createArbiClient;
|
|
1121
|
+
exports.createAuthMessage = createAuthMessage;
|
|
1122
|
+
exports.createAutoReloginMiddleware = createAutoReloginMiddleware;
|
|
1123
|
+
exports.createBearerAuthMiddleware = createBearerAuthMiddleware;
|
|
1124
|
+
exports.createReloginHandler = createReloginHandler;
|
|
1125
|
+
exports.createSessionManager = createSessionManager;
|
|
1126
|
+
exports.createWorkspaceKeyHeader = createWorkspaceKeyHeader;
|
|
1127
|
+
exports.createWorkspaceKeyMiddleware = createWorkspaceKeyMiddleware;
|
|
1128
|
+
exports.decryptMessage = decryptMessage;
|
|
1129
|
+
exports.decryptMessageWithSharedSecret = decryptMessageWithSharedSecret;
|
|
1130
|
+
exports.deriveEncryptionKeypairFromSigning = deriveEncryptionKeypairFromSigning;
|
|
1131
|
+
exports.derivePublicKey = derivePublicKey;
|
|
1132
|
+
exports.encryptMessage = encryptMessage;
|
|
1133
|
+
exports.encryptMessageWithSharedSecret = encryptMessageWithSharedSecret;
|
|
1134
|
+
exports.generateKeyPairs = generateKeyPairs;
|
|
1135
|
+
exports.generateLoginCredentials = generateLoginCredentials;
|
|
1136
|
+
exports.generateLoginCredentialsFromKey = generateLoginCredentialsFromKey;
|
|
1137
|
+
exports.generatePasswordChangeCredentials = generatePasswordChangeCredentials;
|
|
1138
|
+
exports.generateRecoveryPasswordChangeCredentials = generateRecoveryPasswordChangeCredentials;
|
|
1139
|
+
exports.generateRegistrationCredentials = generateRegistrationCredentials;
|
|
1140
|
+
exports.generateUserKeypairs = generateUserKeypairs;
|
|
1141
|
+
exports.getSession = getSession;
|
|
1142
|
+
exports.hasSession = hasSession;
|
|
1143
|
+
exports.initSodium = initSodium;
|
|
1144
|
+
exports.initializeDatabase = initializeDatabase;
|
|
1145
|
+
exports.isMessageType = isMessageType;
|
|
1146
|
+
exports.needsWorkspaceKey = needsWorkspaceKey;
|
|
1147
|
+
exports.parseServerMessage = parseServerMessage;
|
|
1148
|
+
exports.saveSession = saveSession;
|
|
1149
|
+
exports.sealedBoxDecrypt = sealedBoxDecrypt;
|
|
1150
|
+
exports.sealedBoxEncrypt = sealedBoxEncrypt;
|
|
1151
|
+
exports.signMessage = signMessage;
|
|
1152
|
+
exports.updateSigningPrivateKey = updateSigningPrivateKey;
|
|
1153
|
+
//# sourceMappingURL=index.cjs.map
|
|
1154
|
+
//# sourceMappingURL=index.cjs.map
|