@lvce-editor/auth-worker 3.15.0 → 3.16.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 (2) hide show
  1. package/dist/authWorkerMain.js +787 -484
  2. package/package.json +1 -1
@@ -543,6 +543,9 @@ const restoreExistingError = (error, currentStack) => {
543
543
  };
544
544
  const restoreMethodNotFoundError = (error, currentStack) => {
545
545
  const restoredError = new JsonRpcError(error.message);
546
+ Object.assign(restoredError, {
547
+ code: error.code
548
+ });
546
549
  const parentStack = getParentStack(error);
547
550
  setStack(restoredError, `${parentStack}${NewLine}${currentStack}`);
548
551
  return restoredError;
@@ -562,9 +565,13 @@ const applyDataProperties = (restoredError, error) => {
562
565
  // @ts-ignore
563
566
  restoredError.codeFrame = error.data.codeFrame;
564
567
  }
565
- if (error.data.code) {
568
+ if (typeof error.data.code === 'string' || typeof error.data.code === 'number') {
566
569
  // @ts-ignore
567
- restoredError.code = error.data.code;
570
+ Object.defineProperty(restoredError, 'code', {
571
+ configurable: true,
572
+ value: error.data.code,
573
+ writable: true
574
+ });
568
575
  }
569
576
  if (error.data.type) {
570
577
  // @ts-ignore
@@ -585,7 +592,14 @@ const applyDirectProperties = (restoredError, error) => {
585
592
  }
586
593
  };
587
594
  const restoreMessageError = (error, _currentStack) => {
588
- const restoredError = constructError(error.message, error.type, error.name);
595
+ const restoredError = constructError(error.message, error.type || error.data?.type, error.name);
596
+ if (typeof error.code === 'string' || typeof error.code === 'number') {
597
+ Object.defineProperty(restoredError, 'code', {
598
+ configurable: true,
599
+ value: error.code,
600
+ writable: true
601
+ });
602
+ }
589
603
  if (error.data) {
590
604
  applyDataProperties(restoredError, error);
591
605
  } else {
@@ -593,6 +607,23 @@ const restoreMessageError = (error, _currentStack) => {
593
607
  }
594
608
  return restoredError;
595
609
  };
610
+ const getFallbackMessage = error => {
611
+ if (error && typeof error.message === 'string' && error.message) {
612
+ return error.message;
613
+ }
614
+ if (error && error.data) {
615
+ return getFallbackMessage(error.data);
616
+ }
617
+ const stringified = String(error);
618
+ if (stringified !== '[object Object]') {
619
+ return stringified;
620
+ }
621
+ try {
622
+ return JSON.stringify(error);
623
+ } catch {
624
+ return stringified;
625
+ }
626
+ };
596
627
  const restoreJsonRpcError = error => {
597
628
  const currentStack = getCurrentStack();
598
629
  if (error && error instanceof Error) {
@@ -601,13 +632,13 @@ const restoreJsonRpcError = error => {
601
632
  if (error && error.code && error.code === MethodNotFound) {
602
633
  return restoreMethodNotFoundError(error, currentStack);
603
634
  }
635
+ if (error && error.error) {
636
+ return restoreJsonRpcError(error.error);
637
+ }
604
638
  if (error && error.message) {
605
639
  return restoreMessageError(error);
606
640
  }
607
- if (typeof error === 'string') {
608
- return new Error(`JsonRpc Error: ${error}`);
609
- }
610
- return new Error(`JsonRpc Error: ${error}`);
641
+ return new Error(`JsonRpc Error: ${getFallbackMessage(error)}`);
611
642
  };
612
643
  const unwrapJsonRpcResult = responseMessage => {
613
644
  if ('error' in responseMessage) {
@@ -664,9 +695,9 @@ const getErrorProperty = (error, prettyError) => {
664
695
  return {
665
696
  code: Custom,
666
697
  data: {
667
- code: prettyError.code,
698
+ code: prettyError.code ?? error?.code,
668
699
  codeFrame: prettyError.codeFrame,
669
- name: prettyError.name,
700
+ name: prettyError.name ?? error?.name,
670
701
  stack: getStack(prettyError),
671
702
  type: getErrorType(prettyError)
672
703
  },
@@ -701,7 +732,12 @@ const getErrorResponseSimple = (id, error) => {
701
732
  return {
702
733
  error: {
703
734
  code: Custom,
704
- data: error,
735
+ data: error instanceof Error ? {
736
+ ...error,
737
+ code: 'code' in error ? error.code : undefined,
738
+ stack: error.stack,
739
+ type: error.name
740
+ } : error,
705
741
  // @ts-ignore
706
742
  message: error.message
707
743
  },
@@ -883,6 +919,9 @@ const logError = () => {
883
919
  // handled by renderer worker
884
920
  };
885
921
  const handleMessage = event => {
922
+ if (event.data === 'ready') {
923
+ return Promise.resolve();
924
+ }
886
925
  const actualRequiresSocket = event?.target?.requiresSocket || requiresSocket;
887
926
  const actualExecute = event?.target?.execute || execute;
888
927
  return handleJsonRpcMessage(event.target, event.data, actualExecute, event.target._resolve, preparePrettyError, logError, actualRequiresSocket);
@@ -1134,31 +1173,6 @@ const initializeOpenerWorker = async () => {
1134
1173
  set(rpc);
1135
1174
  };
1136
1175
 
1137
- const state$3 = {
1138
- backendUrl: ''
1139
- };
1140
- const getAuthBackendUrl = () => {
1141
- return state$3.backendUrl;
1142
- };
1143
- const setAuthBackendUrl = backendUrl => {
1144
- state$3.backendUrl = backendUrl;
1145
- };
1146
-
1147
- const state$2 = {
1148
- platform: Web
1149
- };
1150
- const getAuthPlatform = () => {
1151
- return state$2.platform;
1152
- };
1153
- const setAuthPlatform = value => {
1154
- state$2.platform = value;
1155
- };
1156
-
1157
- const configure = options => {
1158
- setAuthBackendUrl(options.backendUrl);
1159
- setAuthPlatform(options.platform);
1160
- };
1161
-
1162
1176
  const millisecondsPerSecond = 1000;
1163
1177
  const getAccessTokenExpiresAt = (expiresIn, now = Date.now()) => {
1164
1178
  if (typeof expiresIn !== 'number' || !Number.isFinite(expiresIn) || expiresIn <= 0) {
@@ -1174,190 +1188,106 @@ const isAccessTokenValid = (accessToken, expiresAt, now = Date.now()) => {
1174
1188
  return Number.isFinite(expiresAtNumber) && expiresAtNumber > now;
1175
1189
  };
1176
1190
 
1177
- const authStorageId = 'lvce-editor.auth';
1178
- const deleteSecret = async key => {
1179
- await deleteSecret$1(authStorageId, key);
1180
- };
1181
- const getSecret = async key => {
1182
- return (await getSecret$1(authStorageId, key)) ?? '';
1183
- };
1184
- const storeSecret = async (key, value) => {
1185
- await storeSecret$1(authStorageId, key, value);
1186
- };
1187
-
1188
- const databaseName = 'auth-worker';
1189
- const objectStoreName = 'auth';
1190
- const memoryStorage = new Map();
1191
- const secretKeys = new Set(['accessToken', 'refreshToken']);
1192
- const state$1 = {
1193
- databasePromise: undefined
1194
- };
1195
-
1196
- // IndexedDB request objects are mutable browser primitives and cannot satisfy the readonly rule structurally.
1197
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1198
- const requestToPromise = request => {
1199
- return new Promise((resolve, reject) => {
1200
- request.addEventListener('success', () => {
1201
- resolve(request.result);
1202
- });
1203
- request.addEventListener('error', () => {
1204
- reject(request.error ?? new Error('Persistent storage request failed.'));
1191
+ // One auth worker owns the registry. Serialize its storage transactions, not network requests.
1192
+ const createAccountSessions = storage => {
1193
+ const state = {
1194
+ tail: Promise.resolve()
1195
+ };
1196
+ const transact = operation => {
1197
+ const previous = state.tail;
1198
+ const result = (async () => {
1199
+ await previous;
1200
+ const value = await storage.read();
1201
+ const registry = value ? JSON.parse(value) : {
1202
+ accounts: [],
1203
+ activeId: '',
1204
+ revision: 0
1205
+ };
1206
+ return operation(registry);
1207
+ })();
1208
+ state.tail = (async () => {
1209
+ try {
1210
+ await result;
1211
+ } catch {
1212
+ // A rejected transaction must not block subsequent requests.
1213
+ }
1214
+ })();
1215
+ return result;
1216
+ };
1217
+ const save = async registry => storage.write(JSON.stringify(registry));
1218
+ const read = () => transact(async registry => registry);
1219
+ const add = (account, revision) => transact(async registry => {
1220
+ const accounts = [...registry.accounts.filter(item => item.id !== account.id), account];
1221
+ const activeId = registry.revision === revision || !registry.activeId ? account.id : registry.activeId;
1222
+ await save({
1223
+ accounts,
1224
+ activeId,
1225
+ revision: registry.revision + 1
1205
1226
  });
1206
1227
  });
1207
- };
1208
-
1209
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1210
- const transactionToPromise = transaction => {
1211
- return new Promise((resolve, reject) => {
1212
- transaction.addEventListener('complete', () => {
1213
- resolve();
1214
- });
1215
- transaction.addEventListener('abort', () => {
1216
- reject(transaction.error ?? new Error('Persistent storage transaction failed.'));
1228
+ const select = id => transact(async registry => {
1229
+ if (registry.accounts.every(account => account.id !== id)) {
1230
+ throw new Error('Account is no longer available.');
1231
+ }
1232
+ await save({
1233
+ ...registry,
1234
+ activeId: id,
1235
+ revision: registry.revision + 1
1217
1236
  });
1218
- transaction.addEventListener('error', () => {
1219
- reject(transaction.error ?? new Error('Persistent storage transaction failed.'));
1237
+ });
1238
+ const remove = id => transact(async registry => {
1239
+ const accounts = registry.accounts.filter(account => account.id !== id);
1240
+ const activeId = registry.activeId === id ? accounts[0]?.id || '' : registry.activeId;
1241
+ await save({
1242
+ accounts,
1243
+ activeId,
1244
+ revision: registry.revision + 1
1220
1245
  });
1221
1246
  });
1222
- };
1223
- const getDatabase = async () => {
1224
- if (typeof indexedDB === 'undefined') {
1225
- return undefined;
1226
- }
1227
- if (!state$1.databasePromise) {
1228
- state$1.databasePromise = new Promise((resolve, reject) => {
1229
- const request = indexedDB.open(databaseName, 1);
1230
- request.addEventListener('upgradeneeded', () => {
1231
- const database = request.result;
1232
- if (!database.objectStoreNames.contains(objectStoreName)) {
1233
- database.createObjectStore(objectStoreName);
1234
- }
1235
- });
1236
- request.addEventListener('success', () => {
1237
- resolve(request.result);
1238
- });
1239
- request.addEventListener('error', () => {
1240
- reject(request.error ?? new Error('Failed to open persistent auth storage.'));
1241
- });
1247
+ const update = (account, session) => transact(async registry => {
1248
+ // A logout or another login for this identity invalidates an old refresh result.
1249
+ const accounts = registry.accounts.map(item => item.id === account.id && item.session.authRefreshToken === account.session.authRefreshToken ? {
1250
+ ...item,
1251
+ session
1252
+ } : item);
1253
+ await save({
1254
+ ...registry,
1255
+ accounts
1242
1256
  });
1243
- }
1244
- return state$1.databasePromise;
1245
- };
1246
- const getIndexedDbValue = async key => {
1247
- const database = await getDatabase();
1248
- if (!database) {
1249
- return memoryStorage.get(key) ?? '';
1250
- }
1251
- const transaction = database.transaction(objectStoreName, 'readonly');
1252
- const objectStore = transaction.objectStore(objectStoreName);
1253
- const value = await requestToPromise(objectStore.get(key));
1254
- return typeof value === 'string' ? value : '';
1255
- };
1256
- const setIndexedDbValue = async (key, value) => {
1257
- memoryStorage.set(key, value);
1258
- const database = await getDatabase();
1259
- if (!database) {
1260
- return;
1261
- }
1262
- const transaction = database.transaction(objectStoreName, 'readwrite');
1263
- const objectStore = transaction.objectStore(objectStoreName);
1264
- objectStore.put(value, key);
1265
- await transactionToPromise(transaction);
1266
- };
1267
- const clearIndexedDbValue = async key => {
1268
- memoryStorage.delete(key);
1269
- const database = await getDatabase();
1270
- if (!database) {
1271
- return;
1272
- }
1273
- const transaction = database.transaction(objectStoreName, 'readwrite');
1274
- const objectStore = transaction.objectStore(objectStoreName);
1275
- objectStore.delete(key);
1276
- await transactionToPromise(transaction);
1277
- };
1278
- const shouldUseSecretStorage = key => {
1279
- return getAuthPlatform() === Electron && secretKeys.has(key);
1280
- };
1281
- const getPersistentAuthValue = async key => {
1282
- if (!shouldUseSecretStorage(key)) {
1283
- return getIndexedDbValue(key);
1284
- }
1285
- const secret = await getSecret(key);
1286
- if (secret) {
1287
- return secret;
1288
- }
1289
- const legacyValue = await getIndexedDbValue(key);
1290
- if (!legacyValue) {
1291
- return '';
1292
- }
1293
- await storeSecret(key, legacyValue);
1294
- await clearIndexedDbValue(key);
1295
- return legacyValue;
1296
- };
1297
- const setPersistentAuthValue = async (key, value) => {
1298
- if (!shouldUseSecretStorage(key)) {
1299
- await setIndexedDbValue(key, value);
1300
- return;
1301
- }
1302
- if (value) {
1303
- await storeSecret(key, value);
1304
- } else {
1305
- await deleteSecret(key);
1306
- }
1307
- await clearIndexedDbValue(key);
1308
- };
1309
- const clearPersistentAuthValue = async key => {
1310
- if (shouldUseSecretStorage(key)) {
1311
- await Promise.all([deleteSecret(key), clearIndexedDbValue(key)]);
1312
- return;
1313
- }
1314
- await clearIndexedDbValue(key);
1257
+ });
1258
+ return {
1259
+ add,
1260
+ read,
1261
+ remove,
1262
+ select,
1263
+ update
1264
+ };
1315
1265
  };
1316
1266
 
1317
- const callbackUrlKey = 'oidcCallbackUrl';
1318
- const clientIdKey = 'oidcClientId';
1319
- const pendingClientIdKey = 'pendingOidcClientId';
1320
- const pendingCodeVerifierKey = 'pendingOidcCodeVerifier';
1321
- const pendingRedirectUriKey = 'pendingOidcRedirectUri';
1322
- const pendingStateKey = 'pendingOidcState';
1323
- const clearOidcCallbackUrl = async () => {
1324
- await clearPersistentAuthValue(callbackUrlKey);
1325
- };
1326
- const clearStoredOidcClientId = async () => {
1327
- await clearPersistentAuthValue(clientIdKey);
1328
- };
1329
- const clearPendingOidcAuthState = async () => {
1330
- await Promise.all([clearPersistentAuthValue(pendingClientIdKey), clearPersistentAuthValue(pendingCodeVerifierKey), clearPersistentAuthValue(pendingRedirectUriKey), clearPersistentAuthValue(pendingStateKey)]);
1267
+ const state$4 = {
1268
+ backendUrl: ''
1331
1269
  };
1332
- const getOidcCallbackUrl = async () => {
1333
- return getPersistentAuthValue(callbackUrlKey);
1270
+ const getAuthBackendUrl = () => {
1271
+ return state$4.backendUrl;
1334
1272
  };
1335
- const getStoredOidcClientId = async () => {
1336
- return getPersistentAuthValue(clientIdKey);
1273
+ const setAuthBackendUrl = backendUrl => {
1274
+ state$4.backendUrl = backendUrl;
1337
1275
  };
1338
- const loadPendingOidcAuthState = async () => {
1339
- const [clientId, codeVerifier, redirectUri, state] = await Promise.all([getPersistentAuthValue(pendingClientIdKey), getPersistentAuthValue(pendingCodeVerifierKey), getPersistentAuthValue(pendingRedirectUriKey), getPersistentAuthValue(pendingStateKey)]);
1340
- if (!clientId || !codeVerifier || !redirectUri || !state) {
1341
- return undefined;
1342
- }
1343
- return {
1344
- clientId,
1345
- codeVerifier,
1346
- redirectUri,
1347
- state
1348
- };
1276
+
1277
+ const state$3 = {
1278
+ platform: Web
1349
1279
  };
1350
- const saveOidcClientId = async clientId => {
1351
- await setPersistentAuthValue(clientIdKey, clientId);
1280
+ const getAuthPlatform = () => {
1281
+ return state$3.platform;
1352
1282
  };
1353
- const savePendingOidcAuthState = async value => {
1354
- await Promise.all([setPersistentAuthValue(pendingClientIdKey, value.clientId), setPersistentAuthValue(pendingCodeVerifierKey, value.codeVerifier), setPersistentAuthValue(pendingRedirectUriKey, value.redirectUri), setPersistentAuthValue(pendingStateKey, value.state)]);
1283
+ const setAuthPlatform = value => {
1284
+ state$3.platform = value;
1355
1285
  };
1356
1286
 
1357
1287
  let USER_AGENT;
1358
1288
  if (typeof navigator === 'undefined' || !navigator.userAgent?.startsWith?.('Mozilla/5.0 ')) {
1359
1289
  const NAME = 'oauth4webapi';
1360
- const VERSION = 'v3.8.7';
1290
+ const VERSION = 'v3.8.8';
1361
1291
  USER_AGENT = `${NAME}/${VERSION}`;
1362
1292
  }
1363
1293
  function looseInstanceOf(input, expected) {
@@ -1711,9 +1641,7 @@ function parseWwwAuthenticateChallenges(response) {
1711
1641
  if (match = rest.match(quotedParamRE)) {
1712
1642
  [, key, value, rest] = match;
1713
1643
  if (value.includes('\\')) {
1714
- try {
1715
- value = JSON.parse(`"${value}"`);
1716
- } catch {}
1644
+ value = value.replace(/\\([\s\S])/g, '$1');
1717
1645
  }
1718
1646
  parameters[key.toLowerCase()] = value;
1719
1647
  continue;
@@ -1847,7 +1775,7 @@ async function processGenericAccessTokenResponse(as, client, response, additiona
1847
1775
  assertString(json.id_token, '"response" body "id_token" property', INVALID_RESPONSE, {
1848
1776
  body: json
1849
1777
  });
1850
- const requiredClaims = ['aud', 'exp', 'iat', 'iss', 'sub'];
1778
+ const requiredClaims = [];
1851
1779
  if (client.require_auth_time === true) {
1852
1780
  requiredClaims.push('auth_time');
1853
1781
  }
@@ -1858,27 +1786,9 @@ async function processGenericAccessTokenResponse(as, client, response, additiona
1858
1786
  const {
1859
1787
  claims,
1860
1788
  jwt
1861
- } = await validateJwt(json.id_token, checkSigningAlgorithm.bind(undefined, client.id_token_signed_response_alg, as.id_token_signing_alg_values_supported, 'RS256'), getClockSkew(client), getClockTolerance(client), decryptFn).then(validatePresence.bind(undefined, requiredClaims)).then(validateIssuer.bind(undefined, as)).then(validateAudience.bind(undefined, client.client_id));
1862
- if (Array.isArray(claims.aud) && claims.aud.length !== 1) {
1863
- if (claims.azp === undefined) {
1864
- throw OPE('ID Token "aud" (audience) claim includes additional untrusted audiences', JWT_CLAIM_COMPARISON, {
1865
- claims,
1866
- claim: 'aud'
1867
- });
1868
- }
1869
- if (claims.azp !== client.client_id) {
1870
- throw OPE('unexpected ID Token "azp" (authorized party) claim value', JWT_CLAIM_COMPARISON, {
1871
- expected: client.client_id,
1872
- claims,
1873
- claim: 'azp'
1874
- });
1875
- }
1876
- }
1877
- if (claims.auth_time !== undefined) {
1878
- assertNumber(claims.auth_time, true, 'ID Token "auth_time" (authentication time)', INVALID_RESPONSE, {
1879
- claims
1880
- });
1881
- }
1789
+ } = await validateIdTokenClaims(as, client, json.id_token, requiredClaims, decryptFn);
1790
+ validateIdTokenAuthorizedParty(client, claims);
1791
+ validateIdTokenAuthTimeClaim(claims);
1882
1792
  jwtRefs.set(response, jwt);
1883
1793
  idTokenClaims.set(json, claims);
1884
1794
  }
@@ -1959,6 +1869,41 @@ function validatePresence(required, result) {
1959
1869
  }
1960
1870
  return result;
1961
1871
  }
1872
+ function validateStringClaim(claim, result) {
1873
+ if (typeof result.claims[claim] !== 'string') {
1874
+ throw OPE(`unexpected JWT "${claim}" (${jwtClaimNames[claim]}) claim type`, INVALID_RESPONSE, {
1875
+ claims: result.claims
1876
+ });
1877
+ }
1878
+ return result;
1879
+ }
1880
+ function validateIdTokenClaims(as, client, idToken, requiredClaims, decryptFn) {
1881
+ return validateJwt(idToken, checkSigningAlgorithm.bind(undefined, client.id_token_signed_response_alg, as.id_token_signing_alg_values_supported, 'RS256'), getClockSkew(client), getClockTolerance(client), decryptFn).then(validatePresence.bind(undefined, ['aud', 'exp', 'iat', 'iss', 'sub', ...requiredClaims])).then(validateIssuer.bind(undefined, as)).then(validateAudience.bind(undefined, client.client_id)).then(validateStringClaim.bind(undefined, 'sub'));
1882
+ }
1883
+ function validateIdTokenAuthTimeClaim(claims) {
1884
+ if (claims.auth_time !== undefined) {
1885
+ assertNumber(claims.auth_time, true, 'ID Token "auth_time" (authentication time)', INVALID_RESPONSE, {
1886
+ claims
1887
+ });
1888
+ }
1889
+ }
1890
+ function validateIdTokenAuthorizedParty(client, claims) {
1891
+ if (Array.isArray(claims.aud) && claims.aud.length !== 1) {
1892
+ if (claims.azp === undefined) {
1893
+ throw OPE('ID Token "aud" (audience) claim includes additional untrusted audiences', JWT_CLAIM_COMPARISON, {
1894
+ claims,
1895
+ claim: 'aud'
1896
+ });
1897
+ }
1898
+ if (claims.azp !== client.client_id) {
1899
+ throw OPE('unexpected ID Token "azp" (authorized party) claim value', JWT_CLAIM_COMPARISON, {
1900
+ expected: client.client_id,
1901
+ claims,
1902
+ claim: 'azp'
1903
+ });
1904
+ }
1905
+ }
1906
+ }
1962
1907
  const WWW_AUTHENTICATE_CHALLENGE = 'OAUTH_WWW_AUTHENTICATE_CHALLENGE';
1963
1908
  const RESPONSE_BODY_ERROR = 'OAUTH_RESPONSE_BODY_ERROR';
1964
1909
  const UNSUPPORTED_OPERATION = 'OAUTH_UNSUPPORTED_OPERATION';
@@ -2175,87 +2120,30 @@ const getClient$1 = clientId => {
2175
2120
  client_id: clientId
2176
2121
  };
2177
2122
  };
2178
- const refreshOidcTokens = async (backendUrl, clientId, refreshToken, requestTokenEndpoint = genericTokenEndpointRequest, processTokenEndpointResponse = processGenericTokenEndpointResponse) => {
2123
+ const exchangeAuthorizationCode = async (backendUrl, clientId, code, redirectUri, codeVerifier, requestTokenEndpoint = genericTokenEndpointRequest, processTokenEndpointResponse = processGenericTokenEndpointResponse) => {
2179
2124
  const authorizationServer = getAuthorizationServer$1(backendUrl);
2180
2125
  const client = getClient$1(clientId);
2181
- const response = await requestTokenEndpoint(authorizationServer, client, None(), 'refresh_token', new URLSearchParams({
2182
- refresh_token: refreshToken
2126
+ const response = await requestTokenEndpoint(authorizationServer, client, None(), 'authorization_code', new URLSearchParams({
2127
+ code,
2128
+ code_verifier: codeVerifier,
2129
+ redirect_uri: redirectUri
2183
2130
  }));
2184
2131
  const tokenResponse = await processTokenEndpointResponse(authorizationServer, client, response);
2185
2132
  return {
2186
2133
  accessToken: tokenResponse.access_token,
2187
2134
  expiresIn: typeof tokenResponse.expires_in === 'number' ? tokenResponse.expires_in : undefined,
2188
- refreshToken: typeof tokenResponse.refresh_token === 'string' ? tokenResponse.refresh_token : refreshToken
2135
+ refreshToken: typeof tokenResponse.refresh_token === 'string' ? tokenResponse.refresh_token : ''
2189
2136
  };
2190
2137
  };
2191
2138
 
2192
- const getAccessToken = async (options = {}, refreshTokens = refreshOidcTokens, now = Date.now()) => {
2193
- const [accessToken, accessTokenExpiresAt] = await Promise.all([getPersistentAuthValue('accessToken'), getPersistentAuthValue('accessTokenExpiresAt')]);
2194
- if (!options.refresh || options.refresh === 'if-needed' && isAccessTokenValid(accessToken, accessTokenExpiresAt, now)) {
2195
- return accessToken;
2139
+ const getCurrentHref = async () => {
2140
+ try {
2141
+ return await invoke('Layout.getHref');
2142
+ } catch {
2143
+ // ignore
2196
2144
  }
2197
- const [clientId, refreshToken] = await Promise.all([getStoredOidcClientId(), getPersistentAuthValue('refreshToken')]);
2198
- const backendUrl = getAuthBackendUrl();
2199
- if (!backendUrl || !clientId || !refreshToken) {
2200
- if (options.refresh === 'always') {
2201
- return '';
2202
- }
2203
- const expiresAt = Number(accessTokenExpiresAt);
2204
- return accessTokenExpiresAt && Number.isFinite(expiresAt) && expiresAt <= now ? '' : accessToken;
2205
- }
2206
- const refreshedTokens = await refreshTokens(backendUrl, clientId, refreshToken);
2207
- const expiresAt = getAccessTokenExpiresAt(refreshedTokens.expiresIn, now);
2208
- await Promise.all([setPersistentAuthValue('accessToken', refreshedTokens.accessToken), setPersistentAuthValue('accessTokenExpiresAt', typeof expiresAt === 'number' ? String(expiresAt) : ''), setPersistentAuthValue('refreshToken', refreshedTokens.refreshToken)]);
2209
- return refreshedTokens.accessToken;
2210
- };
2211
-
2212
- const handleMessagePort = async (port, rpcId) => {
2213
- const rpc = await create$4({
2214
- commandMap: commandMap,
2215
- isMessagePortOpen: true,
2216
- messagePort: port
2217
- });
2218
- if (rpcId) {
2219
- set$3(rpcId, rpc);
2220
- }
2221
- };
2222
-
2223
- const getAuthorizationServer = backendUrl => {
2224
- return {
2225
- issuer: getBackendAuthUrl(backendUrl, '/oidc'),
2226
- jwks_uri: getBackendAuthUrl(backendUrl, '/oidc/jwks'),
2227
- token_endpoint: getBackendOidcTokenUrl(backendUrl)
2228
- };
2229
- };
2230
- const getClient = clientId => {
2231
- return {
2232
- client_id: clientId
2233
- };
2234
- };
2235
- const exchangeAuthorizationCode = async (backendUrl, clientId, code, redirectUri, codeVerifier, requestTokenEndpoint = genericTokenEndpointRequest, processTokenEndpointResponse = processGenericTokenEndpointResponse) => {
2236
- const authorizationServer = getAuthorizationServer(backendUrl);
2237
- const client = getClient(clientId);
2238
- const response = await requestTokenEndpoint(authorizationServer, client, None(), 'authorization_code', new URLSearchParams({
2239
- code,
2240
- code_verifier: codeVerifier,
2241
- redirect_uri: redirectUri
2242
- }));
2243
- const tokenResponse = await processTokenEndpointResponse(authorizationServer, client, response);
2244
- return {
2245
- accessToken: tokenResponse.access_token,
2246
- expiresIn: typeof tokenResponse.expires_in === 'number' ? tokenResponse.expires_in : undefined,
2247
- refreshToken: typeof tokenResponse.refresh_token === 'string' ? tokenResponse.refresh_token : ''
2248
- };
2249
- };
2250
-
2251
- const getCurrentHref = async () => {
2252
- try {
2253
- return await invoke('Layout.getHref');
2254
- } catch {
2255
- // ignore
2256
- }
2257
- if (!globalThis.location || typeof globalThis.location.href !== 'string' || !globalThis.location.href) {
2258
- return '';
2145
+ if (!globalThis.location || typeof globalThis.location.href !== 'string' || !globalThis.location.href) {
2146
+ return '';
2259
2147
  }
2260
2148
  return globalThis.location.href;
2261
2149
  };
@@ -2298,6 +2186,186 @@ const getOidcUserName = async (backendUrl, accessToken, fetchFn = fetch) => {
2298
2186
  return typeof displayName === 'string' ? displayName : '';
2299
2187
  };
2300
2188
 
2189
+ const authStorageId = 'lvce-editor.auth';
2190
+ const deleteSecret = async key => {
2191
+ await deleteSecret$1(authStorageId, key);
2192
+ };
2193
+ const getSecret = async key => {
2194
+ return (await getSecret$1(authStorageId, key)) ?? '';
2195
+ };
2196
+ const storeSecret = async (key, value) => {
2197
+ await storeSecret$1(authStorageId, key, value);
2198
+ };
2199
+
2200
+ const databaseName = 'auth-worker';
2201
+ const objectStoreName = 'auth';
2202
+ const memoryStorage = new Map();
2203
+ const secretKeys = new Set(['accessToken', 'refreshToken', 'accountSessions']);
2204
+ const state$2 = {
2205
+ databasePromise: undefined
2206
+ };
2207
+
2208
+ // IndexedDB request objects are mutable browser primitives and cannot satisfy the readonly rule structurally.
2209
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
2210
+ const requestToPromise = request => {
2211
+ return new Promise((resolve, reject) => {
2212
+ request.addEventListener('success', () => {
2213
+ resolve(request.result);
2214
+ });
2215
+ request.addEventListener('error', () => {
2216
+ reject(request.error ?? new Error('Persistent storage request failed.'));
2217
+ });
2218
+ });
2219
+ };
2220
+
2221
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
2222
+ const transactionToPromise = transaction => {
2223
+ return new Promise((resolve, reject) => {
2224
+ transaction.addEventListener('complete', () => {
2225
+ resolve();
2226
+ });
2227
+ transaction.addEventListener('abort', () => {
2228
+ reject(transaction.error ?? new Error('Persistent storage transaction failed.'));
2229
+ });
2230
+ transaction.addEventListener('error', () => {
2231
+ reject(transaction.error ?? new Error('Persistent storage transaction failed.'));
2232
+ });
2233
+ });
2234
+ };
2235
+ const getDatabase = async () => {
2236
+ if (typeof indexedDB === 'undefined') {
2237
+ return undefined;
2238
+ }
2239
+ if (!state$2.databasePromise) {
2240
+ state$2.databasePromise = new Promise((resolve, reject) => {
2241
+ const request = indexedDB.open(databaseName, 1);
2242
+ request.addEventListener('upgradeneeded', () => {
2243
+ const database = request.result;
2244
+ if (!database.objectStoreNames.contains(objectStoreName)) {
2245
+ database.createObjectStore(objectStoreName);
2246
+ }
2247
+ });
2248
+ request.addEventListener('success', () => {
2249
+ resolve(request.result);
2250
+ });
2251
+ request.addEventListener('error', () => {
2252
+ reject(request.error ?? new Error('Failed to open persistent auth storage.'));
2253
+ });
2254
+ });
2255
+ }
2256
+ return state$2.databasePromise;
2257
+ };
2258
+ const getIndexedDbValue = async key => {
2259
+ const database = await getDatabase();
2260
+ if (!database) {
2261
+ return memoryStorage.get(key) ?? '';
2262
+ }
2263
+ const transaction = database.transaction(objectStoreName, 'readonly');
2264
+ const objectStore = transaction.objectStore(objectStoreName);
2265
+ const value = await requestToPromise(objectStore.get(key));
2266
+ return typeof value === 'string' ? value : '';
2267
+ };
2268
+ const setIndexedDbValue = async (key, value) => {
2269
+ memoryStorage.set(key, value);
2270
+ const database = await getDatabase();
2271
+ if (!database) {
2272
+ return;
2273
+ }
2274
+ const transaction = database.transaction(objectStoreName, 'readwrite');
2275
+ const objectStore = transaction.objectStore(objectStoreName);
2276
+ objectStore.put(value, key);
2277
+ await transactionToPromise(transaction);
2278
+ };
2279
+ const clearIndexedDbValue = async key => {
2280
+ memoryStorage.delete(key);
2281
+ const database = await getDatabase();
2282
+ if (!database) {
2283
+ return;
2284
+ }
2285
+ const transaction = database.transaction(objectStoreName, 'readwrite');
2286
+ const objectStore = transaction.objectStore(objectStoreName);
2287
+ objectStore.delete(key);
2288
+ await transactionToPromise(transaction);
2289
+ };
2290
+ const shouldUseSecretStorage = key => {
2291
+ return getAuthPlatform() === Electron && secretKeys.has(key);
2292
+ };
2293
+ const getPersistentAuthValue = async key => {
2294
+ if (!shouldUseSecretStorage(key)) {
2295
+ return getIndexedDbValue(key);
2296
+ }
2297
+ const secret = await getSecret(key);
2298
+ if (secret) {
2299
+ return secret;
2300
+ }
2301
+ const legacyValue = await getIndexedDbValue(key);
2302
+ if (!legacyValue) {
2303
+ return '';
2304
+ }
2305
+ await storeSecret(key, legacyValue);
2306
+ await clearIndexedDbValue(key);
2307
+ return legacyValue;
2308
+ };
2309
+ const setPersistentAuthValue = async (key, value) => {
2310
+ if (!shouldUseSecretStorage(key)) {
2311
+ await setIndexedDbValue(key, value);
2312
+ return;
2313
+ }
2314
+ if (value) {
2315
+ await storeSecret(key, value);
2316
+ } else {
2317
+ await deleteSecret(key);
2318
+ }
2319
+ await clearIndexedDbValue(key);
2320
+ };
2321
+ const clearPersistentAuthValue = async key => {
2322
+ if (shouldUseSecretStorage(key)) {
2323
+ await Promise.all([deleteSecret(key), clearIndexedDbValue(key)]);
2324
+ return;
2325
+ }
2326
+ await clearIndexedDbValue(key);
2327
+ };
2328
+
2329
+ const callbackUrlKey = 'oidcCallbackUrl';
2330
+ const clientIdKey = 'oidcClientId';
2331
+ const pendingClientIdKey = 'pendingOidcClientId';
2332
+ const pendingCodeVerifierKey = 'pendingOidcCodeVerifier';
2333
+ const pendingRedirectUriKey = 'pendingOidcRedirectUri';
2334
+ const pendingStateKey = 'pendingOidcState';
2335
+ const clearOidcCallbackUrl = async () => {
2336
+ await clearPersistentAuthValue(callbackUrlKey);
2337
+ };
2338
+ const clearStoredOidcClientId = async () => {
2339
+ await clearPersistentAuthValue(clientIdKey);
2340
+ };
2341
+ const clearPendingOidcAuthState = async () => {
2342
+ await Promise.all([clearPersistentAuthValue(pendingClientIdKey), clearPersistentAuthValue(pendingCodeVerifierKey), clearPersistentAuthValue(pendingRedirectUriKey), clearPersistentAuthValue(pendingStateKey)]);
2343
+ };
2344
+ const getOidcCallbackUrl = async () => {
2345
+ return getPersistentAuthValue(callbackUrlKey);
2346
+ };
2347
+ const getStoredOidcClientId = async () => {
2348
+ return getPersistentAuthValue(clientIdKey);
2349
+ };
2350
+ const loadPendingOidcAuthState = async () => {
2351
+ const [clientId, codeVerifier, redirectUri, state] = await Promise.all([getPersistentAuthValue(pendingClientIdKey), getPersistentAuthValue(pendingCodeVerifierKey), getPersistentAuthValue(pendingRedirectUriKey), getPersistentAuthValue(pendingStateKey)]);
2352
+ if (!clientId || !codeVerifier || !redirectUri || !state) {
2353
+ return undefined;
2354
+ }
2355
+ return {
2356
+ clientId,
2357
+ codeVerifier,
2358
+ redirectUri,
2359
+ state
2360
+ };
2361
+ };
2362
+ const saveOidcClientId = async clientId => {
2363
+ await setPersistentAuthValue(clientIdKey, clientId);
2364
+ };
2365
+ const savePendingOidcAuthState = async value => {
2366
+ await Promise.all([setPersistentAuthValue(pendingClientIdKey, value.clientId), setPersistentAuthValue(pendingCodeVerifierKey, value.codeVerifier), setPersistentAuthValue(pendingRedirectUriKey, value.redirectUri), setPersistentAuthValue(pendingStateKey, value.state)]);
2367
+ };
2368
+
2301
2369
  const normalizeRedirectUri = value => {
2302
2370
  const url = new URL(value);
2303
2371
  url.hash = '';
@@ -2352,18 +2420,139 @@ const completeBrowserOidcLogin = async backendUrl => {
2352
2420
  const authAccessTokenExpiresAt = getAccessTokenExpiresAt(exchanged.expiresIn);
2353
2421
  await clearPendingOidcAuthState();
2354
2422
  return {
2355
- authAccessToken: exchanged.accessToken,
2356
- ...(authAccessTokenExpiresAt && {
2357
- authAccessTokenExpiresAt
2358
- }),
2359
- authClientId: pendingAuthState.clientId,
2360
- authErrorMessage: '',
2361
- authRefreshToken: exchanged.refreshToken,
2362
- userName,
2363
- userState: exchanged.accessToken ? 'loggedIn' : 'loggedOut'
2423
+ authAccessToken: exchanged.accessToken,
2424
+ ...(authAccessTokenExpiresAt && {
2425
+ authAccessTokenExpiresAt
2426
+ }),
2427
+ authClientId: pendingAuthState.clientId,
2428
+ authErrorMessage: '',
2429
+ authRefreshToken: exchanged.refreshToken,
2430
+ userName,
2431
+ userState: exchanged.accessToken ? 'loggedIn' : 'loggedOut'
2432
+ };
2433
+ };
2434
+
2435
+ const getAccountProfile = async (backendUrl, accessToken, fetchFn = fetch) => {
2436
+ const response = await fetchFn(new URL('/account/me', backendUrl), {
2437
+ headers: {
2438
+ Accept: 'application/json',
2439
+ Authorization: `Bearer ${accessToken}`
2440
+ }
2441
+ });
2442
+ if (!response.ok) {
2443
+ throw new Error('Unable to load the signed in account.');
2444
+ }
2445
+ const value = await response.json();
2446
+ if (!value || typeof value.id !== 'string' || !value.id) {
2447
+ throw new Error('The backend did not return an account identity.');
2448
+ }
2449
+ return {
2450
+ displayName: typeof value.displayName === 'string' ? value.displayName : '',
2451
+ email: typeof value.email === 'string' ? value.email : '',
2452
+ id: value.id,
2453
+ provider: typeof value.provider === 'string' ? value.provider : 'LVCE Editor'
2454
+ };
2455
+ };
2456
+
2457
+ const getBackendRefreshUrl = backendUrl => {
2458
+ return getBackendAuthUrl(backendUrl, '/auth/refresh');
2459
+ };
2460
+
2461
+ const delay = async ms => {
2462
+ await new Promise(resolve => setTimeout(resolve, ms));
2463
+ };
2464
+
2465
+ const state$1 = {
2466
+ nextLoginResponse: undefined,
2467
+ nextRefreshResponse: undefined
2468
+ };
2469
+ const setNextLoginResponse = response => {
2470
+ state$1.nextLoginResponse = response;
2471
+ };
2472
+ const setNextRefreshResponse = response => {
2473
+ state$1.nextRefreshResponse = response;
2474
+ };
2475
+ const clear = () => {
2476
+ state$1.nextLoginResponse = undefined;
2477
+ state$1.nextRefreshResponse = undefined;
2478
+ };
2479
+ const hasPendingMockLoginResponse = () => {
2480
+ return !!state$1.nextLoginResponse;
2481
+ };
2482
+ const hasPendingMockRefreshResponse = () => {
2483
+ return !!state$1.nextRefreshResponse;
2484
+ };
2485
+ const consumeNextLoginResponse = async () => {
2486
+ if (!state$1.nextLoginResponse) {
2487
+ return undefined;
2488
+ }
2489
+ const response = state$1.nextLoginResponse;
2490
+ state$1.nextLoginResponse = undefined;
2491
+ if (response.delay > 0) {
2492
+ await delay(response.delay);
2493
+ }
2494
+ if (response.type === 'error') {
2495
+ throw new Error(response.message);
2496
+ }
2497
+ return response.response;
2498
+ };
2499
+ const consumeNextRefreshResponse = async () => {
2500
+ if (!state$1.nextRefreshResponse) {
2501
+ return undefined;
2502
+ }
2503
+ const response = state$1.nextRefreshResponse;
2504
+ state$1.nextRefreshResponse = undefined;
2505
+ if (response.delay > 0) {
2506
+ await new Promise(resolve => setTimeout(resolve, response.delay));
2507
+ }
2508
+ if (response.type === 'error') {
2509
+ throw new Error(response.message);
2510
+ }
2511
+ return response.response;
2512
+ };
2513
+
2514
+ const isObject = value => {
2515
+ return !!value && typeof value === 'object';
2516
+ };
2517
+
2518
+ const isBackendAuthResponse = value => {
2519
+ return isObject(value);
2520
+ };
2521
+
2522
+ const getNumber = (value, fallback = 0) => {
2523
+ return typeof value === 'number' ? value : fallback;
2524
+ };
2525
+
2526
+ const getString = (value, fallback = '') => {
2527
+ return typeof value === 'string' ? value : fallback;
2528
+ };
2529
+
2530
+ const getUserName = value => {
2531
+ if (typeof value.userName === 'string') {
2532
+ return value.userName;
2533
+ }
2534
+ return getString(value.user?.displayName);
2535
+ };
2536
+ const toBackendAuthState = value => {
2537
+ return {
2538
+ authAccessToken: getString(value.accessToken),
2539
+ authErrorMessage: getString(value.error),
2540
+ authRefreshToken: getString(value.refreshToken),
2541
+ userName: getUserName(value),
2542
+ userState: value.accessToken ? 'loggedIn' : 'loggedOut',
2543
+ userSubscriptionPlan: getString(value.subscriptionPlan),
2544
+ userSubscriptionStatus: getString(value.subscriptionStatus),
2545
+ userUsedTokens: getNumber(value.usedTokens)
2364
2546
  };
2365
2547
  };
2366
2548
 
2549
+ const parseBackendAuthResponse = value => {
2550
+ if (!isBackendAuthResponse(value)) {
2551
+ return getLoggedOutBackendAuthState('Backend returned an invalid authentication response.');
2552
+ }
2553
+ return toBackendAuthState(value);
2554
+ };
2555
+
2367
2556
  const accessTokenKey = 'accessToken';
2368
2557
  const accessTokenExpiresAtKey = 'accessTokenExpiresAt';
2369
2558
  const refreshTokenKey = 'refreshToken';
@@ -2436,6 +2625,32 @@ const persistLoginResult = async loginResult => {
2436
2625
  return loginResult;
2437
2626
  };
2438
2627
 
2628
+ const getAuthorizationServer = backendUrl => {
2629
+ return {
2630
+ issuer: getBackendAuthUrl(backendUrl, '/oidc'),
2631
+ jwks_uri: getBackendAuthUrl(backendUrl, '/oidc/jwks'),
2632
+ token_endpoint: getBackendOidcTokenUrl(backendUrl)
2633
+ };
2634
+ };
2635
+ const getClient = clientId => {
2636
+ return {
2637
+ client_id: clientId
2638
+ };
2639
+ };
2640
+ const refreshOidcTokens = async (backendUrl, clientId, refreshToken, requestTokenEndpoint = genericTokenEndpointRequest, processTokenEndpointResponse = processGenericTokenEndpointResponse) => {
2641
+ const authorizationServer = getAuthorizationServer(backendUrl);
2642
+ const client = getClient(clientId);
2643
+ const response = await requestTokenEndpoint(authorizationServer, client, None(), 'refresh_token', new URLSearchParams({
2644
+ refresh_token: refreshToken
2645
+ }));
2646
+ const tokenResponse = await processTokenEndpointResponse(authorizationServer, client, response);
2647
+ return {
2648
+ accessToken: tokenResponse.access_token,
2649
+ expiresIn: typeof tokenResponse.expires_in === 'number' ? tokenResponse.expires_in : undefined,
2650
+ refreshToken: typeof tokenResponse.refresh_token === 'string' ? tokenResponse.refresh_token : refreshToken
2651
+ };
2652
+ };
2653
+
2439
2654
  const clearStoredOidcAuth = async () => {
2440
2655
  await clearPersistedAuthSession();
2441
2656
  };
@@ -2474,163 +2689,6 @@ const restoreOidcAuth = async backendUrl => {
2474
2689
  }
2475
2690
  };
2476
2691
 
2477
- const getBackendUrl = options => {
2478
- if (typeof options === 'number') {
2479
- return '';
2480
- }
2481
- return options.backendUrl || '';
2482
- };
2483
- const getPlatform = options => {
2484
- return typeof options === 'number' ? options : options.platform ?? Web;
2485
- };
2486
- const initialize = async options => {
2487
- const backendUrl = getBackendUrl(options);
2488
- configure({
2489
- backendUrl,
2490
- platform: getPlatform(options)
2491
- });
2492
- try {
2493
- if (backendUrl) {
2494
- const completedBrowserLogin = await completeBrowserOidcLogin(backendUrl);
2495
- if (completedBrowserLogin) {
2496
- return persistLoginResult(completedBrowserLogin);
2497
- }
2498
- const restoredOidcAuth = await restoreOidcAuth(backendUrl);
2499
- if (restoredOidcAuth) {
2500
- return persistLoginResult(restoredOidcAuth);
2501
- }
2502
- }
2503
- const persistedAuthSession = await getPersistedAuthSession();
2504
- if (persistedAuthSession) {
2505
- return persistedAuthSession;
2506
- }
2507
- return getLoggedOutBackendAuthState();
2508
- } catch (error) {
2509
- const authErrorMessage = error instanceof Error && error.message ? error.message : 'Backend authentication failed.';
2510
- return getLoggedOutBackendAuthState(authErrorMessage);
2511
- }
2512
- };
2513
-
2514
- const getBackendLogoutUrl = backendUrl => {
2515
- return getBackendAuthUrl(backendUrl, '/auth/logout');
2516
- };
2517
-
2518
- const logoutFromBackend = async backendUrl => {
2519
- if (!backendUrl) {
2520
- return;
2521
- }
2522
- try {
2523
- await fetch(getBackendLogoutUrl(backendUrl), {
2524
- credentials: 'include',
2525
- headers: {
2526
- Accept: 'application/json'
2527
- },
2528
- method: 'POST'
2529
- });
2530
- } catch {
2531
- // Ignore logout failures and still clear local auth state.
2532
- }
2533
- };
2534
-
2535
- const getBackendRefreshUrl = backendUrl => {
2536
- return getBackendAuthUrl(backendUrl, '/auth/refresh');
2537
- };
2538
-
2539
- const delay = async ms => {
2540
- await new Promise(resolve => setTimeout(resolve, ms));
2541
- };
2542
-
2543
- const state = {
2544
- nextLoginResponse: undefined,
2545
- nextRefreshResponse: undefined
2546
- };
2547
- const setNextLoginResponse = response => {
2548
- state.nextLoginResponse = response;
2549
- };
2550
- const setNextRefreshResponse = response => {
2551
- state.nextRefreshResponse = response;
2552
- };
2553
- const clear = () => {
2554
- state.nextLoginResponse = undefined;
2555
- state.nextRefreshResponse = undefined;
2556
- };
2557
- const hasPendingMockLoginResponse = () => {
2558
- return !!state.nextLoginResponse;
2559
- };
2560
- const hasPendingMockRefreshResponse = () => {
2561
- return !!state.nextRefreshResponse;
2562
- };
2563
- const consumeNextLoginResponse = async () => {
2564
- if (!state.nextLoginResponse) {
2565
- return undefined;
2566
- }
2567
- const response = state.nextLoginResponse;
2568
- state.nextLoginResponse = undefined;
2569
- if (response.delay > 0) {
2570
- await delay(response.delay);
2571
- }
2572
- if (response.type === 'error') {
2573
- throw new Error(response.message);
2574
- }
2575
- return response.response;
2576
- };
2577
- const consumeNextRefreshResponse = async () => {
2578
- if (!state.nextRefreshResponse) {
2579
- return undefined;
2580
- }
2581
- const response = state.nextRefreshResponse;
2582
- state.nextRefreshResponse = undefined;
2583
- if (response.delay > 0) {
2584
- await new Promise(resolve => setTimeout(resolve, response.delay));
2585
- }
2586
- if (response.type === 'error') {
2587
- throw new Error(response.message);
2588
- }
2589
- return response.response;
2590
- };
2591
-
2592
- const isObject = value => {
2593
- return !!value && typeof value === 'object';
2594
- };
2595
-
2596
- const isBackendAuthResponse = value => {
2597
- return isObject(value);
2598
- };
2599
-
2600
- const getNumber = (value, fallback = 0) => {
2601
- return typeof value === 'number' ? value : fallback;
2602
- };
2603
-
2604
- const getString = (value, fallback = '') => {
2605
- return typeof value === 'string' ? value : fallback;
2606
- };
2607
-
2608
- const getUserName = value => {
2609
- if (typeof value.userName === 'string') {
2610
- return value.userName;
2611
- }
2612
- return getString(value.user?.displayName);
2613
- };
2614
- const toBackendAuthState = value => {
2615
- return {
2616
- authAccessToken: getString(value.accessToken),
2617
- authErrorMessage: getString(value.error),
2618
- authRefreshToken: getString(value.refreshToken),
2619
- userName: getUserName(value),
2620
- userState: value.accessToken ? 'loggedIn' : 'loggedOut',
2621
- userSubscriptionPlan: getString(value.subscriptionPlan),
2622
- userSubscriptionStatus: getString(value.subscriptionStatus),
2623
- userUsedTokens: getNumber(value.usedTokens)
2624
- };
2625
- };
2626
-
2627
- const parseBackendAuthResponse = value => {
2628
- if (!isBackendAuthResponse(value)) {
2629
- return getLoggedOutBackendAuthState('Backend returned an invalid authentication response.');
2630
- }
2631
- return toBackendAuthState(value);
2632
- };
2633
-
2634
2692
  const getPayload = async response => {
2635
2693
  try {
2636
2694
  return await response.json();
@@ -2638,7 +2696,7 @@ const getPayload = async response => {
2638
2696
  return undefined;
2639
2697
  }
2640
2698
  };
2641
- const syncBackendAuth = async backendUrl => {
2699
+ const syncBackendAuth$1 = async backendUrl => {
2642
2700
  if (!backendUrl) {
2643
2701
  return getLoggedOutBackendAuthState('Backend URL is missing.');
2644
2702
  }
@@ -2688,7 +2746,7 @@ const waitForBackendLogin = async (backendUrl, timeoutMs = 30_000, pollIntervalM
2688
2746
  const deadline = Date.now() + timeoutMs;
2689
2747
  let lastErrorMessage = '';
2690
2748
  while (Date.now() < deadline) {
2691
- const authState = await syncBackendAuth(backendUrl);
2749
+ const authState = await syncBackendAuth$1(backendUrl);
2692
2750
  if (authState.userState === 'loggedIn') {
2693
2751
  return authState;
2694
2752
  }
@@ -2700,6 +2758,13 @@ const waitForBackendLogin = async (backendUrl, timeoutMs = 30_000, pollIntervalM
2700
2758
  return getLoggedOutBackendAuthState(lastErrorMessage);
2701
2759
  };
2702
2760
 
2761
+ const getAccountLoginUrl = (backendUrl, authorizationUrl) => {
2762
+ const url = new URL('/login', backendUrl);
2763
+ url.searchParams.set('selectAccount', 'true');
2764
+ url.searchParams.set('returnTo', authorizationUrl);
2765
+ return url.href;
2766
+ };
2767
+
2703
2768
  const getAuthUseRedirect = platform => {
2704
2769
  return platform !== Electron;
2705
2770
  };
@@ -3091,7 +3156,7 @@ const waitForElectronBackendLogin = async (backendUrl, uid, redirectUri, codeVer
3091
3156
  return getLoggedOutBackendAuthState('Timed out waiting for backend login.');
3092
3157
  };
3093
3158
 
3094
- const getMockLoginResult = async signingInState => {
3159
+ const getMockLoginResult = async (signingInState, persistResult) => {
3095
3160
  if (!hasPendingMockLoginResponse()) {
3096
3161
  return undefined;
3097
3162
  }
@@ -3108,9 +3173,9 @@ const getMockLoginResult = async signingInState => {
3108
3173
  userState: 'loggedOut'
3109
3174
  };
3110
3175
  }
3111
- return persistLoginResult(getLoggedInState(signingInState, response));
3176
+ return persistResult(getLoggedInState(signingInState, response));
3112
3177
  };
3113
- const getInteractiveLoginResult = async (backendUrl, platform, signingInState) => {
3178
+ const getInteractiveLoginResult = async (backendUrl, platform, signingInState, persistResult, selectAccount) => {
3114
3179
  const authUseRedirect = getAuthUseRedirect(platform);
3115
3180
  const uid = 0;
3116
3181
  const {
@@ -3128,7 +3193,7 @@ const getInteractiveLoginResult = async (backendUrl, platform, signingInState) =
3128
3193
  state
3129
3194
  });
3130
3195
  }
3131
- await invoke$1('Open.openUrl', loginUrl, platform, authUseRedirect);
3196
+ await invoke$1('Open.openUrl', selectAccount ? getAccountLoginUrl(backendUrl, loginUrl) : loginUrl, platform, authUseRedirect);
3132
3197
  if (platform !== Electron && authUseRedirect) {
3133
3198
  return signingInState;
3134
3199
  }
@@ -3136,12 +3201,12 @@ const getInteractiveLoginResult = async (backendUrl, platform, signingInState) =
3136
3201
  if (platform !== Electron) {
3137
3202
  await clearPendingOidcAuthState();
3138
3203
  }
3139
- return persistLoginResult({
3204
+ return persistResult({
3140
3205
  ...authState,
3141
3206
  authClientId: clientId
3142
3207
  });
3143
3208
  };
3144
- const handleClickLogin = async options => {
3209
+ const handleClickLogin = async (options, persistResult = persistLoginResult) => {
3145
3210
  const {
3146
3211
  backendUrl,
3147
3212
  platform
@@ -3158,11 +3223,11 @@ const handleClickLogin = async options => {
3158
3223
  userState: 'loggingIn'
3159
3224
  };
3160
3225
  try {
3161
- const mockLoginResult = await getMockLoginResult(signingInState);
3226
+ const mockLoginResult = await getMockLoginResult(signingInState, persistResult);
3162
3227
  if (mockLoginResult) {
3163
3228
  return mockLoginResult;
3164
3229
  }
3165
- return getInteractiveLoginResult(backendUrl, platform, signingInState);
3230
+ return getInteractiveLoginResult(backendUrl, platform, signingInState, persistResult, options.selectAccount === true);
3166
3231
  } catch (error) {
3167
3232
  await clearPendingOidcAuthState();
3168
3233
  const errorMessage = error instanceof Error && error.message ? error.message : 'Backend authentication failed.';
@@ -3173,17 +3238,252 @@ const handleClickLogin = async options => {
3173
3238
  }
3174
3239
  };
3175
3240
 
3176
- const logout = async state => {
3177
- const loggingOutState = {
3178
- authErrorMessage: '',
3179
- userState: 'loggingOut'
3180
- };
3181
- await logoutFromBackend(state.backendUrl);
3182
- await Promise.all([clearOidcCallbackUrl(), clearPendingOidcAuthState(), clearPersistedAuthSession()]);
3183
- return {
3184
- ...loggingOutState,
3185
- ...getLoggedOutBackendAuthState()
3186
- };
3241
+ const storageKey = 'accountSessions';
3242
+ const accounts = createAccountSessions({
3243
+ read: () => getPersistentAuthValue(storageKey),
3244
+ write: value => setPersistentAuthValue(storageKey, value)
3245
+ });
3246
+ const refreshes = new Map();
3247
+ const state = {
3248
+ login: undefined,
3249
+ migration: undefined
3250
+ };
3251
+ const normalizeBackendUrl = backendUrl => new URL(backendUrl).origin;
3252
+ const addSession = async (backendUrl, session, revision) => {
3253
+ if (session.userState !== 'loggedIn' || !session.authAccessToken) {
3254
+ return;
3255
+ }
3256
+ const profile = await getAccountProfile(backendUrl, session.authAccessToken);
3257
+ await accounts.add({
3258
+ backendUrl,
3259
+ id: `${backendUrl}/${profile.id}`,
3260
+ profile,
3261
+ session: {
3262
+ ...session,
3263
+ userName: profile.displayName
3264
+ }
3265
+ }, revision);
3266
+ };
3267
+ const migrate = async backendUrl => {
3268
+ if (await getPersistentAuthValue(storageKey)) {
3269
+ return;
3270
+ }
3271
+ const legacy = await getPersistedAuthSession();
3272
+ if (legacy) {
3273
+ if (legacy.authClientId && legacy.authRefreshToken) {
3274
+ const tokens = await refreshOidcTokens(backendUrl, legacy.authClientId, legacy.authRefreshToken);
3275
+ const authAccessTokenExpiresAt = getAccessTokenExpiresAt(tokens.expiresIn);
3276
+ const {
3277
+ authAccessTokenExpiresAt: _legacyExpiry,
3278
+ ...legacySession
3279
+ } = legacy;
3280
+ await addSession(backendUrl, {
3281
+ ...legacySession,
3282
+ authAccessToken: tokens.accessToken,
3283
+ ...(authAccessTokenExpiresAt && {
3284
+ authAccessTokenExpiresAt
3285
+ }),
3286
+ authRefreshToken: tokens.refreshToken
3287
+ }, 0);
3288
+ } else {
3289
+ await addSession(backendUrl, legacy, 0);
3290
+ }
3291
+ } else {
3292
+ await setPersistentAuthValue(storageKey, JSON.stringify({
3293
+ accounts: [],
3294
+ activeId: '',
3295
+ revision: 0
3296
+ }));
3297
+ }
3298
+ // Clear legacy credentials only after the registry is durably committed.
3299
+ await clearPersistedAuthSession();
3300
+ };
3301
+ const ensureMigrated = async (backendUrl = getAuthBackendUrl()) => {
3302
+ const url = normalizeBackendUrl(backendUrl);
3303
+ if (!state.migration) {
3304
+ state.migration = (async () => {
3305
+ try {
3306
+ await migrate(url);
3307
+ } catch (error) {
3308
+ state.migration = undefined;
3309
+ throw error;
3310
+ }
3311
+ })();
3312
+ }
3313
+ await state.migration;
3314
+ return url;
3315
+ };
3316
+ const getActive = async backendUrl => {
3317
+ const registry = await accounts.read();
3318
+ return registry.accounts.find(account => account.id === registry.activeId && account.backendUrl === backendUrl);
3319
+ };
3320
+ const getCurrentState = async (backendUrl, authErrorMessage = '') => {
3321
+ const account = await getActive(backendUrl);
3322
+ return account ? {
3323
+ ...account.session,
3324
+ authErrorMessage
3325
+ } : getLoggedOutBackendAuthState(authErrorMessage);
3326
+ };
3327
+ const refresh = async account => {
3328
+ const {
3329
+ backendUrl,
3330
+ session
3331
+ } = account;
3332
+ if (!session.authClientId || !session.authRefreshToken) {
3333
+ if (session.authAccessTokenExpiresAt && session.authAccessTokenExpiresAt <= Date.now()) {
3334
+ await accounts.update(account, getLoggedOutBackendAuthState('Please sign in to this account again.'));
3335
+ }
3336
+ return;
3337
+ }
3338
+ const key = `${account.id}/${session.authRefreshToken}`;
3339
+ const pending = refreshes.get(key);
3340
+ if (pending) {
3341
+ return pending;
3342
+ }
3343
+ const operation = (async () => {
3344
+ const tokens = await refreshOidcTokens(backendUrl, session.authClientId, session.authRefreshToken);
3345
+ const authAccessTokenExpiresAt = getAccessTokenExpiresAt(tokens.expiresIn);
3346
+ const {
3347
+ authAccessTokenExpiresAt: _previousExpiry,
3348
+ ...previousSession
3349
+ } = session;
3350
+ await accounts.update(account, {
3351
+ ...previousSession,
3352
+ authAccessToken: tokens.accessToken,
3353
+ ...(authAccessTokenExpiresAt && {
3354
+ authAccessTokenExpiresAt
3355
+ }),
3356
+ authRefreshToken: tokens.refreshToken
3357
+ });
3358
+ })();
3359
+ refreshes.set(key, operation);
3360
+ try {
3361
+ await operation;
3362
+ } catch (error) {
3363
+ // Network errors must not destroy a retained session. A rejected grant needs another login.
3364
+ if (error && typeof error === 'object' && 'error' in error && error.error === 'invalid_grant') {
3365
+ await accounts.update(account, getLoggedOutBackendAuthState('Please sign in to this account again.'));
3366
+ }
3367
+ throw error;
3368
+ } finally {
3369
+ refreshes.delete(key);
3370
+ }
3371
+ };
3372
+ const getAccounts = async () => {
3373
+ const backendUrl = await ensureMigrated();
3374
+ const registry = await accounts.read();
3375
+ return registry.accounts.filter(account => account.backendUrl === backendUrl).map(account => ({
3376
+ ...account.profile,
3377
+ active: account.id === registry.activeId,
3378
+ color: 'blue',
3379
+ id: account.id,
3380
+ signedIn: account.session.userState === 'loggedIn'
3381
+ }));
3382
+ };
3383
+ const getAccessToken = async (options = {}) => {
3384
+ const backendUrl = await ensureMigrated();
3385
+ const account = await getActive(backendUrl);
3386
+ if (!account) {
3387
+ return '';
3388
+ }
3389
+ if (options.refresh === 'always' || options.refresh === 'if-needed' && !isAccessTokenValid(account.session.authAccessToken || '', String(account.session.authAccessTokenExpiresAt || ''))) {
3390
+ try {
3391
+ await refresh(account);
3392
+ } catch (error) {
3393
+ const selected = await getActive(backendUrl);
3394
+ if (selected?.id === account.id) {
3395
+ throw error;
3396
+ }
3397
+ }
3398
+ }
3399
+ // A refresh belongs to the identity it started with; selection can change while it is in flight.
3400
+ const current = await getActive(backendUrl);
3401
+ if (current?.id !== account.id) {
3402
+ return getAccessToken(options);
3403
+ }
3404
+ return current?.session.authAccessToken || '';
3405
+ };
3406
+ const initialize = async (options = {}) => {
3407
+ if (typeof options.platform === 'number') {
3408
+ setAuthPlatform(options.platform);
3409
+ }
3410
+ const backendUrl = await ensureMigrated(options.backendUrl);
3411
+ const registry = await accounts.read();
3412
+ const completed = await completeBrowserOidcLogin(backendUrl);
3413
+ if (completed) {
3414
+ await addSession(backendUrl, completed, registry.revision);
3415
+ return getCurrentState(backendUrl, completed.authErrorMessage);
3416
+ }
3417
+ try {
3418
+ await getAccessToken({
3419
+ refresh: 'if-needed'
3420
+ });
3421
+ return getCurrentState(backendUrl);
3422
+ } catch (error) {
3423
+ return getCurrentState(backendUrl, error instanceof Error ? error.message : 'Unable to refresh the account.');
3424
+ }
3425
+ };
3426
+ const login = async options => {
3427
+ if (state.login) {
3428
+ return state.login;
3429
+ }
3430
+ const operation = (async () => {
3431
+ const backendUrl = await ensureMigrated(options.backendUrl);
3432
+ const registry = await accounts.read();
3433
+ const result = await handleClickLogin({
3434
+ ...options,
3435
+ selectAccount: registry.accounts.length > 0
3436
+ }, async value => value);
3437
+ await addSession(backendUrl, result, registry.revision);
3438
+ return getCurrentState(backendUrl, result.authErrorMessage);
3439
+ })();
3440
+ state.login = operation;
3441
+ try {
3442
+ return await operation;
3443
+ } finally {
3444
+ state.login = undefined;
3445
+ }
3446
+ };
3447
+ const useAccount = async id => {
3448
+ const backendUrl = await ensureMigrated();
3449
+ const registry = await accounts.read();
3450
+ if (registry.accounts.every(account => !(account.id === id && account.backendUrl === backendUrl))) {
3451
+ throw new Error('Account is no longer available.');
3452
+ }
3453
+ await accounts.select(id);
3454
+ return getCurrentState(backendUrl);
3455
+ };
3456
+ const removeAccount = async id => {
3457
+ const backendUrl = await ensureMigrated();
3458
+ const registry = await accounts.read();
3459
+ if (registry.accounts.some(account => account.id === id && account.backendUrl === backendUrl)) {
3460
+ await accounts.remove(id);
3461
+ }
3462
+ return getCurrentState(backendUrl);
3463
+ };
3464
+ const logout = async () => {
3465
+ const backendUrl = await ensureMigrated();
3466
+ const account = await getActive(backendUrl);
3467
+ return account ? removeAccount(account.id) : getLoggedOutBackendAuthState();
3468
+ };
3469
+ const syncBackendAuth = async backendUrl => initialize({
3470
+ backendUrl
3471
+ });
3472
+
3473
+ const configure = options => {
3474
+ setAuthBackendUrl(options.backendUrl);
3475
+ setAuthPlatform(options.platform);
3476
+ };
3477
+
3478
+ const handleMessagePort = async (port, rpcId) => {
3479
+ const rpc = await create$4({
3480
+ commandMap: commandMap,
3481
+ isMessagePortOpen: true,
3482
+ messagePort: port
3483
+ });
3484
+ if (rpcId) {
3485
+ set$3(rpcId, rpc);
3486
+ }
3187
3487
  };
3188
3488
 
3189
3489
  const commandMap = {
@@ -3192,14 +3492,17 @@ const commandMap = {
3192
3492
  'Auth.consumeNextLoginResponse': consumeNextLoginResponse,
3193
3493
  'Auth.consumeNextRefreshResponse': consumeNextRefreshResponse,
3194
3494
  'Auth.getAccessToken': getAccessToken,
3495
+ 'Auth.getAccounts': getAccounts,
3195
3496
  'Auth.hasPendingMockLoginResponse': hasPendingMockLoginResponse,
3196
3497
  'Auth.hasPendingMockRefreshResponse': hasPendingMockRefreshResponse,
3197
3498
  'Auth.initialize': initialize,
3198
- 'Auth.login': handleClickLogin,
3499
+ 'Auth.login': login,
3199
3500
  'Auth.logout': logout,
3501
+ 'Auth.removeAccount': removeAccount,
3200
3502
  'Auth.setNextLoginResponse': setNextLoginResponse,
3201
3503
  'Auth.setNextRefreshResponse': setNextRefreshResponse,
3202
3504
  'Auth.syncBackendAuth': syncBackendAuth,
3505
+ 'Auth.useAccount': useAccount,
3203
3506
  'HandleMessagePort.handleMessagePort': handleMessagePort,
3204
3507
  initialize: (_, port) => handleMessagePort(port, RendererWorker)
3205
3508
  };