@lvce-editor/auth-worker 3.15.0 → 3.16.1

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 +792 -485
  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,89 +2120,32 @@ 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;
2145
+ if (!globalThis.location || typeof globalThis.location.href !== 'string' || !globalThis.location.href) {
2146
+ return '';
2205
2147
  }
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 '';
2259
- }
2260
- return globalThis.location.href;
2148
+ return globalThis.location.href;
2261
2149
  };
2262
2150
 
2263
2151
  const getLoggedOutBackendAuthState = (authErrorMessage = '') => {
@@ -2298,6 +2186,188 @@ 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 pendingReturnUrlKey = 'pendingOidcReturnUrl';
2335
+ const pendingStateKey = 'pendingOidcState';
2336
+ const clearOidcCallbackUrl = async () => {
2337
+ await clearPersistentAuthValue(callbackUrlKey);
2338
+ };
2339
+ const clearStoredOidcClientId = async () => {
2340
+ await clearPersistentAuthValue(clientIdKey);
2341
+ };
2342
+ const clearPendingOidcAuthState = async () => {
2343
+ await Promise.all([clearPersistentAuthValue(pendingClientIdKey), clearPersistentAuthValue(pendingCodeVerifierKey), clearPersistentAuthValue(pendingRedirectUriKey), clearPersistentAuthValue(pendingReturnUrlKey), clearPersistentAuthValue(pendingStateKey)]);
2344
+ };
2345
+ const getOidcCallbackUrl = async () => {
2346
+ return getPersistentAuthValue(callbackUrlKey);
2347
+ };
2348
+ const getStoredOidcClientId = async () => {
2349
+ return getPersistentAuthValue(clientIdKey);
2350
+ };
2351
+ const loadPendingOidcAuthState = async () => {
2352
+ const [clientId, codeVerifier, redirectUri, returnUrl, state] = await Promise.all([getPersistentAuthValue(pendingClientIdKey), getPersistentAuthValue(pendingCodeVerifierKey), getPersistentAuthValue(pendingRedirectUriKey), getPersistentAuthValue(pendingReturnUrlKey), getPersistentAuthValue(pendingStateKey)]);
2353
+ if (!clientId || !codeVerifier || !redirectUri || !state) {
2354
+ return undefined;
2355
+ }
2356
+ return {
2357
+ clientId,
2358
+ codeVerifier,
2359
+ redirectUri,
2360
+ returnUrl,
2361
+ state
2362
+ };
2363
+ };
2364
+ const saveOidcClientId = async clientId => {
2365
+ await setPersistentAuthValue(clientIdKey, clientId);
2366
+ };
2367
+ const savePendingOidcAuthState = async value => {
2368
+ await Promise.all([setPersistentAuthValue(pendingClientIdKey, value.clientId), setPersistentAuthValue(pendingCodeVerifierKey, value.codeVerifier), setPersistentAuthValue(pendingRedirectUriKey, value.redirectUri), setPersistentAuthValue(pendingReturnUrlKey, value.returnUrl), setPersistentAuthValue(pendingStateKey, value.state)]);
2369
+ };
2370
+
2301
2371
  const normalizeRedirectUri = value => {
2302
2372
  const url = new URL(value);
2303
2373
  url.hash = '';
@@ -2352,18 +2422,139 @@ const completeBrowserOidcLogin = async backendUrl => {
2352
2422
  const authAccessTokenExpiresAt = getAccessTokenExpiresAt(exchanged.expiresIn);
2353
2423
  await clearPendingOidcAuthState();
2354
2424
  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'
2425
+ authAccessToken: exchanged.accessToken,
2426
+ ...(authAccessTokenExpiresAt && {
2427
+ authAccessTokenExpiresAt
2428
+ }),
2429
+ authClientId: pendingAuthState.clientId,
2430
+ authErrorMessage: '',
2431
+ authRefreshToken: exchanged.refreshToken,
2432
+ userName,
2433
+ userState: exchanged.accessToken ? 'loggedIn' : 'loggedOut'
2434
+ };
2435
+ };
2436
+
2437
+ const getAccountProfile = async (backendUrl, accessToken, fetchFn = fetch) => {
2438
+ const response = await fetchFn(new URL('/account/me', backendUrl), {
2439
+ headers: {
2440
+ Accept: 'application/json',
2441
+ Authorization: `Bearer ${accessToken}`
2442
+ }
2443
+ });
2444
+ if (!response.ok) {
2445
+ throw new Error('Unable to load the signed in account.');
2446
+ }
2447
+ const value = await response.json();
2448
+ if (!value || typeof value.id !== 'string' || !value.id) {
2449
+ throw new Error('The backend did not return an account identity.');
2450
+ }
2451
+ return {
2452
+ displayName: typeof value.displayName === 'string' ? value.displayName : '',
2453
+ email: typeof value.email === 'string' ? value.email : '',
2454
+ id: value.id,
2455
+ provider: typeof value.provider === 'string' ? value.provider : 'LVCE Editor'
2456
+ };
2457
+ };
2458
+
2459
+ const getBackendRefreshUrl = backendUrl => {
2460
+ return getBackendAuthUrl(backendUrl, '/auth/refresh');
2461
+ };
2462
+
2463
+ const delay = async ms => {
2464
+ await new Promise(resolve => setTimeout(resolve, ms));
2465
+ };
2466
+
2467
+ const state$1 = {
2468
+ nextLoginResponse: undefined,
2469
+ nextRefreshResponse: undefined
2470
+ };
2471
+ const setNextLoginResponse = response => {
2472
+ state$1.nextLoginResponse = response;
2473
+ };
2474
+ const setNextRefreshResponse = response => {
2475
+ state$1.nextRefreshResponse = response;
2476
+ };
2477
+ const clear = () => {
2478
+ state$1.nextLoginResponse = undefined;
2479
+ state$1.nextRefreshResponse = undefined;
2480
+ };
2481
+ const hasPendingMockLoginResponse = () => {
2482
+ return !!state$1.nextLoginResponse;
2483
+ };
2484
+ const hasPendingMockRefreshResponse = () => {
2485
+ return !!state$1.nextRefreshResponse;
2486
+ };
2487
+ const consumeNextLoginResponse = async () => {
2488
+ if (!state$1.nextLoginResponse) {
2489
+ return undefined;
2490
+ }
2491
+ const response = state$1.nextLoginResponse;
2492
+ state$1.nextLoginResponse = undefined;
2493
+ if (response.delay > 0) {
2494
+ await delay(response.delay);
2495
+ }
2496
+ if (response.type === 'error') {
2497
+ throw new Error(response.message);
2498
+ }
2499
+ return response.response;
2500
+ };
2501
+ const consumeNextRefreshResponse = async () => {
2502
+ if (!state$1.nextRefreshResponse) {
2503
+ return undefined;
2504
+ }
2505
+ const response = state$1.nextRefreshResponse;
2506
+ state$1.nextRefreshResponse = undefined;
2507
+ if (response.delay > 0) {
2508
+ await new Promise(resolve => setTimeout(resolve, response.delay));
2509
+ }
2510
+ if (response.type === 'error') {
2511
+ throw new Error(response.message);
2512
+ }
2513
+ return response.response;
2514
+ };
2515
+
2516
+ const isObject = value => {
2517
+ return !!value && typeof value === 'object';
2518
+ };
2519
+
2520
+ const isBackendAuthResponse = value => {
2521
+ return isObject(value);
2522
+ };
2523
+
2524
+ const getNumber = (value, fallback = 0) => {
2525
+ return typeof value === 'number' ? value : fallback;
2526
+ };
2527
+
2528
+ const getString = (value, fallback = '') => {
2529
+ return typeof value === 'string' ? value : fallback;
2530
+ };
2531
+
2532
+ const getUserName = value => {
2533
+ if (typeof value.userName === 'string') {
2534
+ return value.userName;
2535
+ }
2536
+ return getString(value.user?.displayName);
2537
+ };
2538
+ const toBackendAuthState = value => {
2539
+ return {
2540
+ authAccessToken: getString(value.accessToken),
2541
+ authErrorMessage: getString(value.error),
2542
+ authRefreshToken: getString(value.refreshToken),
2543
+ userName: getUserName(value),
2544
+ userState: value.accessToken ? 'loggedIn' : 'loggedOut',
2545
+ userSubscriptionPlan: getString(value.subscriptionPlan),
2546
+ userSubscriptionStatus: getString(value.subscriptionStatus),
2547
+ userUsedTokens: getNumber(value.usedTokens)
2364
2548
  };
2365
2549
  };
2366
2550
 
2551
+ const parseBackendAuthResponse = value => {
2552
+ if (!isBackendAuthResponse(value)) {
2553
+ return getLoggedOutBackendAuthState('Backend returned an invalid authentication response.');
2554
+ }
2555
+ return toBackendAuthState(value);
2556
+ };
2557
+
2367
2558
  const accessTokenKey = 'accessToken';
2368
2559
  const accessTokenExpiresAtKey = 'accessTokenExpiresAt';
2369
2560
  const refreshTokenKey = 'refreshToken';
@@ -2436,6 +2627,32 @@ const persistLoginResult = async loginResult => {
2436
2627
  return loginResult;
2437
2628
  };
2438
2629
 
2630
+ const getAuthorizationServer = backendUrl => {
2631
+ return {
2632
+ issuer: getBackendAuthUrl(backendUrl, '/oidc'),
2633
+ jwks_uri: getBackendAuthUrl(backendUrl, '/oidc/jwks'),
2634
+ token_endpoint: getBackendOidcTokenUrl(backendUrl)
2635
+ };
2636
+ };
2637
+ const getClient = clientId => {
2638
+ return {
2639
+ client_id: clientId
2640
+ };
2641
+ };
2642
+ const refreshOidcTokens = async (backendUrl, clientId, refreshToken, requestTokenEndpoint = genericTokenEndpointRequest, processTokenEndpointResponse = processGenericTokenEndpointResponse) => {
2643
+ const authorizationServer = getAuthorizationServer(backendUrl);
2644
+ const client = getClient(clientId);
2645
+ const response = await requestTokenEndpoint(authorizationServer, client, None(), 'refresh_token', new URLSearchParams({
2646
+ refresh_token: refreshToken
2647
+ }));
2648
+ const tokenResponse = await processTokenEndpointResponse(authorizationServer, client, response);
2649
+ return {
2650
+ accessToken: tokenResponse.access_token,
2651
+ expiresIn: typeof tokenResponse.expires_in === 'number' ? tokenResponse.expires_in : undefined,
2652
+ refreshToken: typeof tokenResponse.refresh_token === 'string' ? tokenResponse.refresh_token : refreshToken
2653
+ };
2654
+ };
2655
+
2439
2656
  const clearStoredOidcAuth = async () => {
2440
2657
  await clearPersistedAuthSession();
2441
2658
  };
@@ -2474,163 +2691,6 @@ const restoreOidcAuth = async backendUrl => {
2474
2691
  }
2475
2692
  };
2476
2693
 
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
2694
  const getPayload = async response => {
2635
2695
  try {
2636
2696
  return await response.json();
@@ -2638,7 +2698,7 @@ const getPayload = async response => {
2638
2698
  return undefined;
2639
2699
  }
2640
2700
  };
2641
- const syncBackendAuth = async backendUrl => {
2701
+ const syncBackendAuth$1 = async backendUrl => {
2642
2702
  if (!backendUrl) {
2643
2703
  return getLoggedOutBackendAuthState('Backend URL is missing.');
2644
2704
  }
@@ -2688,7 +2748,7 @@ const waitForBackendLogin = async (backendUrl, timeoutMs = 30_000, pollIntervalM
2688
2748
  const deadline = Date.now() + timeoutMs;
2689
2749
  let lastErrorMessage = '';
2690
2750
  while (Date.now() < deadline) {
2691
- const authState = await syncBackendAuth(backendUrl);
2751
+ const authState = await syncBackendAuth$1(backendUrl);
2692
2752
  if (authState.userState === 'loggedIn') {
2693
2753
  return authState;
2694
2754
  }
@@ -2700,6 +2760,13 @@ const waitForBackendLogin = async (backendUrl, timeoutMs = 30_000, pollIntervalM
2700
2760
  return getLoggedOutBackendAuthState(lastErrorMessage);
2701
2761
  };
2702
2762
 
2763
+ const getAccountLoginUrl = (backendUrl, authorizationUrl) => {
2764
+ const url = new URL('/login', backendUrl);
2765
+ url.searchParams.set('selectAccount', 'true');
2766
+ url.searchParams.set('returnTo', authorizationUrl);
2767
+ return url.href;
2768
+ };
2769
+
2703
2770
  const getAuthUseRedirect = platform => {
2704
2771
  return platform !== Electron;
2705
2772
  };
@@ -3091,7 +3158,7 @@ const waitForElectronBackendLogin = async (backendUrl, uid, redirectUri, codeVer
3091
3158
  return getLoggedOutBackendAuthState('Timed out waiting for backend login.');
3092
3159
  };
3093
3160
 
3094
- const getMockLoginResult = async signingInState => {
3161
+ const getMockLoginResult = async (signingInState, persistResult) => {
3095
3162
  if (!hasPendingMockLoginResponse()) {
3096
3163
  return undefined;
3097
3164
  }
@@ -3108,9 +3175,9 @@ const getMockLoginResult = async signingInState => {
3108
3175
  userState: 'loggedOut'
3109
3176
  };
3110
3177
  }
3111
- return persistLoginResult(getLoggedInState(signingInState, response));
3178
+ return persistResult(getLoggedInState(signingInState, response));
3112
3179
  };
3113
- const getInteractiveLoginResult = async (backendUrl, platform, signingInState) => {
3180
+ const getInteractiveLoginResult = async (backendUrl, platform, signingInState, persistResult, selectAccount) => {
3114
3181
  const authUseRedirect = getAuthUseRedirect(platform);
3115
3182
  const uid = 0;
3116
3183
  const {
@@ -3121,14 +3188,16 @@ const getInteractiveLoginResult = async (backendUrl, platform, signingInState) =
3121
3188
  state
3122
3189
  } = await getBackendLoginRequest(backendUrl, platform, uid);
3123
3190
  if (platform !== Electron) {
3191
+ const returnUrl = await getCurrentHref();
3124
3192
  await savePendingOidcAuthState({
3125
3193
  clientId,
3126
3194
  codeVerifier,
3127
3195
  redirectUri,
3196
+ returnUrl,
3128
3197
  state
3129
3198
  });
3130
3199
  }
3131
- await invoke$1('Open.openUrl', loginUrl, platform, authUseRedirect);
3200
+ await invoke$1('Open.openUrl', selectAccount ? getAccountLoginUrl(backendUrl, loginUrl) : loginUrl, platform, authUseRedirect);
3132
3201
  if (platform !== Electron && authUseRedirect) {
3133
3202
  return signingInState;
3134
3203
  }
@@ -3136,12 +3205,12 @@ const getInteractiveLoginResult = async (backendUrl, platform, signingInState) =
3136
3205
  if (platform !== Electron) {
3137
3206
  await clearPendingOidcAuthState();
3138
3207
  }
3139
- return persistLoginResult({
3208
+ return persistResult({
3140
3209
  ...authState,
3141
3210
  authClientId: clientId
3142
3211
  });
3143
3212
  };
3144
- const handleClickLogin = async options => {
3213
+ const handleClickLogin = async (options, persistResult = persistLoginResult) => {
3145
3214
  const {
3146
3215
  backendUrl,
3147
3216
  platform
@@ -3158,11 +3227,11 @@ const handleClickLogin = async options => {
3158
3227
  userState: 'loggingIn'
3159
3228
  };
3160
3229
  try {
3161
- const mockLoginResult = await getMockLoginResult(signingInState);
3230
+ const mockLoginResult = await getMockLoginResult(signingInState, persistResult);
3162
3231
  if (mockLoginResult) {
3163
3232
  return mockLoginResult;
3164
3233
  }
3165
- return getInteractiveLoginResult(backendUrl, platform, signingInState);
3234
+ return getInteractiveLoginResult(backendUrl, platform, signingInState, persistResult, options.selectAccount === true);
3166
3235
  } catch (error) {
3167
3236
  await clearPendingOidcAuthState();
3168
3237
  const errorMessage = error instanceof Error && error.message ? error.message : 'Backend authentication failed.';
@@ -3173,17 +3242,252 @@ const handleClickLogin = async options => {
3173
3242
  }
3174
3243
  };
3175
3244
 
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
- };
3245
+ const storageKey = 'accountSessions';
3246
+ const accounts = createAccountSessions({
3247
+ read: () => getPersistentAuthValue(storageKey),
3248
+ write: value => setPersistentAuthValue(storageKey, value)
3249
+ });
3250
+ const refreshes = new Map();
3251
+ const state = {
3252
+ login: undefined,
3253
+ migration: undefined
3254
+ };
3255
+ const normalizeBackendUrl = backendUrl => new URL(backendUrl).origin;
3256
+ const addSession = async (backendUrl, session, revision) => {
3257
+ if (session.userState !== 'loggedIn' || !session.authAccessToken) {
3258
+ return;
3259
+ }
3260
+ const profile = await getAccountProfile(backendUrl, session.authAccessToken);
3261
+ await accounts.add({
3262
+ backendUrl,
3263
+ id: `${backendUrl}/${profile.id}`,
3264
+ profile,
3265
+ session: {
3266
+ ...session,
3267
+ userName: profile.displayName
3268
+ }
3269
+ }, revision);
3270
+ };
3271
+ const migrate = async backendUrl => {
3272
+ if (await getPersistentAuthValue(storageKey)) {
3273
+ return;
3274
+ }
3275
+ const legacy = await getPersistedAuthSession();
3276
+ if (legacy) {
3277
+ if (legacy.authClientId && legacy.authRefreshToken) {
3278
+ const tokens = await refreshOidcTokens(backendUrl, legacy.authClientId, legacy.authRefreshToken);
3279
+ const authAccessTokenExpiresAt = getAccessTokenExpiresAt(tokens.expiresIn);
3280
+ const {
3281
+ authAccessTokenExpiresAt: _legacyExpiry,
3282
+ ...legacySession
3283
+ } = legacy;
3284
+ await addSession(backendUrl, {
3285
+ ...legacySession,
3286
+ authAccessToken: tokens.accessToken,
3287
+ ...(authAccessTokenExpiresAt && {
3288
+ authAccessTokenExpiresAt
3289
+ }),
3290
+ authRefreshToken: tokens.refreshToken
3291
+ }, 0);
3292
+ } else {
3293
+ await addSession(backendUrl, legacy, 0);
3294
+ }
3295
+ } else {
3296
+ await setPersistentAuthValue(storageKey, JSON.stringify({
3297
+ accounts: [],
3298
+ activeId: '',
3299
+ revision: 0
3300
+ }));
3301
+ }
3302
+ // Clear legacy credentials only after the registry is durably committed.
3303
+ await clearPersistedAuthSession();
3304
+ };
3305
+ const ensureMigrated = async (backendUrl = getAuthBackendUrl()) => {
3306
+ const url = normalizeBackendUrl(backendUrl);
3307
+ if (!state.migration) {
3308
+ state.migration = (async () => {
3309
+ try {
3310
+ await migrate(url);
3311
+ } catch (error) {
3312
+ state.migration = undefined;
3313
+ throw error;
3314
+ }
3315
+ })();
3316
+ }
3317
+ await state.migration;
3318
+ return url;
3319
+ };
3320
+ const getActive = async backendUrl => {
3321
+ const registry = await accounts.read();
3322
+ return registry.accounts.find(account => account.id === registry.activeId && account.backendUrl === backendUrl);
3323
+ };
3324
+ const getCurrentState = async (backendUrl, authErrorMessage = '') => {
3325
+ const account = await getActive(backendUrl);
3326
+ return account ? {
3327
+ ...account.session,
3328
+ authErrorMessage
3329
+ } : getLoggedOutBackendAuthState(authErrorMessage);
3330
+ };
3331
+ const refresh = async account => {
3332
+ const {
3333
+ backendUrl,
3334
+ session
3335
+ } = account;
3336
+ if (!session.authClientId || !session.authRefreshToken) {
3337
+ if (session.authAccessTokenExpiresAt && session.authAccessTokenExpiresAt <= Date.now()) {
3338
+ await accounts.update(account, getLoggedOutBackendAuthState('Please sign in to this account again.'));
3339
+ }
3340
+ return;
3341
+ }
3342
+ const key = `${account.id}/${session.authRefreshToken}`;
3343
+ const pending = refreshes.get(key);
3344
+ if (pending) {
3345
+ return pending;
3346
+ }
3347
+ const operation = (async () => {
3348
+ const tokens = await refreshOidcTokens(backendUrl, session.authClientId, session.authRefreshToken);
3349
+ const authAccessTokenExpiresAt = getAccessTokenExpiresAt(tokens.expiresIn);
3350
+ const {
3351
+ authAccessTokenExpiresAt: _previousExpiry,
3352
+ ...previousSession
3353
+ } = session;
3354
+ await accounts.update(account, {
3355
+ ...previousSession,
3356
+ authAccessToken: tokens.accessToken,
3357
+ ...(authAccessTokenExpiresAt && {
3358
+ authAccessTokenExpiresAt
3359
+ }),
3360
+ authRefreshToken: tokens.refreshToken
3361
+ });
3362
+ })();
3363
+ refreshes.set(key, operation);
3364
+ try {
3365
+ await operation;
3366
+ } catch (error) {
3367
+ // Network errors must not destroy a retained session. A rejected grant needs another login.
3368
+ if (error && typeof error === 'object' && 'error' in error && error.error === 'invalid_grant') {
3369
+ await accounts.update(account, getLoggedOutBackendAuthState('Please sign in to this account again.'));
3370
+ }
3371
+ throw error;
3372
+ } finally {
3373
+ refreshes.delete(key);
3374
+ }
3375
+ };
3376
+ const getAccounts = async () => {
3377
+ const backendUrl = await ensureMigrated();
3378
+ const registry = await accounts.read();
3379
+ return registry.accounts.filter(account => account.backendUrl === backendUrl).map(account => ({
3380
+ ...account.profile,
3381
+ active: account.id === registry.activeId,
3382
+ color: 'blue',
3383
+ id: account.id,
3384
+ signedIn: account.session.userState === 'loggedIn'
3385
+ }));
3386
+ };
3387
+ const getAccessToken = async (options = {}) => {
3388
+ const backendUrl = await ensureMigrated();
3389
+ const account = await getActive(backendUrl);
3390
+ if (!account) {
3391
+ return '';
3392
+ }
3393
+ if (options.refresh === 'always' || options.refresh === 'if-needed' && !isAccessTokenValid(account.session.authAccessToken || '', String(account.session.authAccessTokenExpiresAt || ''))) {
3394
+ try {
3395
+ await refresh(account);
3396
+ } catch (error) {
3397
+ const selected = await getActive(backendUrl);
3398
+ if (selected?.id === account.id) {
3399
+ throw error;
3400
+ }
3401
+ }
3402
+ }
3403
+ // A refresh belongs to the identity it started with; selection can change while it is in flight.
3404
+ const current = await getActive(backendUrl);
3405
+ if (current?.id !== account.id) {
3406
+ return getAccessToken(options);
3407
+ }
3408
+ return current?.session.authAccessToken || '';
3409
+ };
3410
+ const initialize = async (options = {}) => {
3411
+ if (typeof options.platform === 'number') {
3412
+ setAuthPlatform(options.platform);
3413
+ }
3414
+ const backendUrl = await ensureMigrated(options.backendUrl);
3415
+ const registry = await accounts.read();
3416
+ const completed = await completeBrowserOidcLogin(backendUrl);
3417
+ if (completed) {
3418
+ await addSession(backendUrl, completed, registry.revision);
3419
+ return getCurrentState(backendUrl, completed.authErrorMessage);
3420
+ }
3421
+ try {
3422
+ await getAccessToken({
3423
+ refresh: 'if-needed'
3424
+ });
3425
+ return getCurrentState(backendUrl);
3426
+ } catch (error) {
3427
+ return getCurrentState(backendUrl, error instanceof Error ? error.message : 'Unable to refresh the account.');
3428
+ }
3429
+ };
3430
+ const login = async options => {
3431
+ if (state.login) {
3432
+ return state.login;
3433
+ }
3434
+ const operation = (async () => {
3435
+ const backendUrl = await ensureMigrated(options.backendUrl);
3436
+ const registry = await accounts.read();
3437
+ const result = await handleClickLogin({
3438
+ ...options,
3439
+ selectAccount: registry.accounts.length > 0
3440
+ }, async value => value);
3441
+ await addSession(backendUrl, result, registry.revision);
3442
+ return getCurrentState(backendUrl, result.authErrorMessage);
3443
+ })();
3444
+ state.login = operation;
3445
+ try {
3446
+ return await operation;
3447
+ } finally {
3448
+ state.login = undefined;
3449
+ }
3450
+ };
3451
+ const useAccount = async id => {
3452
+ const backendUrl = await ensureMigrated();
3453
+ const registry = await accounts.read();
3454
+ if (registry.accounts.every(account => !(account.id === id && account.backendUrl === backendUrl))) {
3455
+ throw new Error('Account is no longer available.');
3456
+ }
3457
+ await accounts.select(id);
3458
+ return getCurrentState(backendUrl);
3459
+ };
3460
+ const removeAccount = async id => {
3461
+ const backendUrl = await ensureMigrated();
3462
+ const registry = await accounts.read();
3463
+ if (registry.accounts.some(account => account.id === id && account.backendUrl === backendUrl)) {
3464
+ await accounts.remove(id);
3465
+ }
3466
+ return getCurrentState(backendUrl);
3467
+ };
3468
+ const logout = async () => {
3469
+ const backendUrl = await ensureMigrated();
3470
+ const account = await getActive(backendUrl);
3471
+ return account ? removeAccount(account.id) : getLoggedOutBackendAuthState();
3472
+ };
3473
+ const syncBackendAuth = async backendUrl => initialize({
3474
+ backendUrl
3475
+ });
3476
+
3477
+ const configure = options => {
3478
+ setAuthBackendUrl(options.backendUrl);
3479
+ setAuthPlatform(options.platform);
3480
+ };
3481
+
3482
+ const handleMessagePort = async (port, rpcId) => {
3483
+ const rpc = await create$4({
3484
+ commandMap: commandMap,
3485
+ isMessagePortOpen: true,
3486
+ messagePort: port
3487
+ });
3488
+ if (rpcId) {
3489
+ set$3(rpcId, rpc);
3490
+ }
3187
3491
  };
3188
3492
 
3189
3493
  const commandMap = {
@@ -3192,14 +3496,17 @@ const commandMap = {
3192
3496
  'Auth.consumeNextLoginResponse': consumeNextLoginResponse,
3193
3497
  'Auth.consumeNextRefreshResponse': consumeNextRefreshResponse,
3194
3498
  'Auth.getAccessToken': getAccessToken,
3499
+ 'Auth.getAccounts': getAccounts,
3195
3500
  'Auth.hasPendingMockLoginResponse': hasPendingMockLoginResponse,
3196
3501
  'Auth.hasPendingMockRefreshResponse': hasPendingMockRefreshResponse,
3197
3502
  'Auth.initialize': initialize,
3198
- 'Auth.login': handleClickLogin,
3503
+ 'Auth.login': login,
3199
3504
  'Auth.logout': logout,
3505
+ 'Auth.removeAccount': removeAccount,
3200
3506
  'Auth.setNextLoginResponse': setNextLoginResponse,
3201
3507
  'Auth.setNextRefreshResponse': setNextRefreshResponse,
3202
3508
  'Auth.syncBackendAuth': syncBackendAuth,
3509
+ 'Auth.useAccount': useAccount,
3203
3510
  'HandleMessagePort.handleMessagePort': handleMessagePort,
3204
3511
  initialize: (_, port) => handleMessagePort(port, RendererWorker)
3205
3512
  };