@oxyhq/core 12.9.0 → 12.10.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.
@@ -122,6 +122,24 @@ const V2_STORAGE_KEYS = {
122
122
  BACKUP_PUBLIC_KEY: 'oxy_identity_backup_public_key_v2',
123
123
  BACKUP_TIMESTAMP: 'oxy_identity_backup_timestamp_v2',
124
124
  };
125
+ /**
126
+ * Dedicated keychain slot for the recovery mnemonic (the 12-word phrase).
127
+ *
128
+ * Stored under its OWN keychain service — distinct from the v2 primary, backup,
129
+ * and shared slots — so it shares an AndroidKeyStore key with none of them
130
+ * (blast-radius isolation, same rationale as the v2 primary/backup split).
131
+ * Written `WHEN_UNLOCKED_THIS_DEVICE_ONLY` and NEVER exported off-device: it
132
+ * exists solely so the user can RE-READ their phrase from Settings on the SAME
133
+ * device that generated/imported it.
134
+ *
135
+ * This is convenience persistence, NOT a recovery mechanism — a keystore death
136
+ * wipes it alongside the keys, exactly like the private key itself. The user's
137
+ * written-down phrase remains the sole out-of-band recovery path. The mnemonic
138
+ * lives ONLY in this slot: it is never mirrored into the identity marker,
139
+ * {@link KeyManager.getIdentityStatus}, logs, or any exported bundle.
140
+ */
141
+ const RECOVERY_MNEMONIC_KEYCHAIN_SERVICE = 'oxy_identity_mnemonic';
142
+ const RECOVERY_MNEMONIC_STORAGE_KEY = 'oxy_identity_mnemonic_v1';
125
143
  /**
126
144
  * Advisory AsyncStorage fast-path flag: set once the v2 slots own the identity.
127
145
  * Re-derivable (its loss just re-runs the cheap slot check), so it lives in
@@ -1476,6 +1494,73 @@ class KeyManager {
1476
1494
  throw new IdentityUnavailableError('Failed to read identity from secure storage.', error);
1477
1495
  }
1478
1496
  }
1497
+ /**
1498
+ * Persist the recovery mnemonic (the 12-word phrase) into its dedicated,
1499
+ * device-only keychain slot so the user can re-reveal it from Settings after
1500
+ * onboarding.
1501
+ *
1502
+ * Called best-effort at identity creation/import, where the phrase is already
1503
+ * in memory: a failure to persist it must NEVER fail the identity itself, so
1504
+ * callers deliberately swallow the thrown error (logging it). Storage errors
1505
+ * throw {@link IdentityUnavailableError} — same "cannot determine" semantics as
1506
+ * the other getters — so a caller MAY observe/log the failure.
1507
+ *
1508
+ * The mnemonic is stored ONLY here — never in the marker, `getIdentityStatus`,
1509
+ * logs, or any exported bundle.
1510
+ */
1511
+ static async storeRecoveryMnemonic(mnemonic) {
1512
+ if (isWebPlatform()) {
1513
+ return; // Identity storage is only available on native platforms
1514
+ }
1515
+ try {
1516
+ const store = await initSecureStore();
1517
+ await store.setItemAsync(RECOVERY_MNEMONIC_STORAGE_KEY, mnemonic, KeyManager._privateWriteOpts(store, RECOVERY_MNEMONIC_KEYCHAIN_SERVICE));
1518
+ }
1519
+ catch (error) {
1520
+ if ((0, logger_1.isDev)()) {
1521
+ logger_1.logger.warn('Failed to persist recovery mnemonic', { component: 'KeyManager' }, error);
1522
+ }
1523
+ throw new IdentityUnavailableError('Failed to persist recovery mnemonic.', error);
1524
+ }
1525
+ }
1526
+ /**
1527
+ * Read the stored recovery mnemonic for re-reveal in Settings.
1528
+ *
1529
+ * Returns the phrase, or `null` when a read SUCCEEDS and finds none — the
1530
+ * expected result for any identity created/imported before this feature
1531
+ * existed, since the phrase was never captured for those. THROWS
1532
+ * {@link IdentityUnavailableError} when storage is unreadable (keychain locked
1533
+ * / module load failure), matching {@link getPublicKey}'s contract — a thrown
1534
+ * read is never flattened to `null`, so a caller distinguishes "phrase was
1535
+ * never stored" from "keychain temporarily locked, retry".
1536
+ */
1537
+ static async getRecoveryMnemonic() {
1538
+ if (isWebPlatform()) {
1539
+ return null; // Identity storage is only available on native platforms
1540
+ }
1541
+ try {
1542
+ const store = await initSecureStore();
1543
+ return await store.getItemAsync(RECOVERY_MNEMONIC_STORAGE_KEY, KeyManager._slotOpts(RECOVERY_MNEMONIC_KEYCHAIN_SERVICE));
1544
+ }
1545
+ catch (error) {
1546
+ if ((0, logger_1.isDev)()) {
1547
+ logger_1.logger.warn('Failed to read recovery mnemonic', { component: 'KeyManager' }, error);
1548
+ }
1549
+ throw new IdentityUnavailableError('Failed to read recovery mnemonic from secure storage.', error);
1550
+ }
1551
+ }
1552
+ /**
1553
+ * Delete the stored recovery mnemonic. Best-effort: a delete failure is logged
1554
+ * and swallowed, never thrown — it runs inside the identity-deletion path where
1555
+ * an unreadable keychain must not abort the wider teardown.
1556
+ */
1557
+ static async deleteRecoveryMnemonic() {
1558
+ if (isWebPlatform()) {
1559
+ return; // Identity storage is only available on native platforms
1560
+ }
1561
+ const store = await initSecureStore();
1562
+ await KeyManager._bestEffortDelete(store, RECOVERY_MNEMONIC_STORAGE_KEY, RECOVERY_MNEMONIC_KEYCHAIN_SERVICE);
1563
+ }
1479
1564
  /**
1480
1565
  * Check if a complete, parseable identity exists on this device.
1481
1566
  *
@@ -1691,6 +1776,10 @@ class KeyManager {
1691
1776
  await KeyManager._bestEffortDeleteV2Primary(store);
1692
1777
  await KeyManager._bestEffortDelete(store, STORAGE_KEYS.PRIVATE_KEY);
1693
1778
  await KeyManager._bestEffortDelete(store, STORAGE_KEYS.PUBLIC_KEY);
1779
+ // Always drop the stored recovery mnemonic — it is scoped to the identity
1780
+ // being deleted, so a leftover would let Settings reveal a stale phrase for
1781
+ // an identity that no longer exists (or a DIFFERENT one after re-onboarding).
1782
+ await KeyManager.deleteRecoveryMnemonic();
1694
1783
  // Also clear backups + the shared slot on force deletion, so a deleted
1695
1784
  // identity cannot be resurrected from any recovery source.
1696
1785
  if (force) {
package/dist/cjs/index.js CHANGED
@@ -18,10 +18,10 @@
18
18
  * If a symbol does not appear here, it is NOT part of the public API.
19
19
  */
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.normalizeLocale = exports.getBaseLanguage = exports.FALLBACK_LOCALE = exports.SUPPORTED_LANGUAGES = exports.TopicSource = exports.TopicType = exports.SECURITY_EVENT_SEVERITY_MAP = exports.DeviceManager = exports.deriveSharedSecret = exports.AEAD_NONCE_LENGTH = exports.AEAD_KEY_LENGTH = exports.decryptAead = exports.encryptAead = exports.hkdfSha256 = exports.RecoveryPhraseService = exports.SignatureService = exports.updateIdentityMarker = exports.readIdentityMarker = exports.IdentityUnavailableError = exports.IdentityPersistError = exports.IdentityAlreadyExistsError = exports.KeyManager = exports.sessionsArraysEqual = exports.normalizeAndSortSessions = exports.mergeSessions = exports.authenticatedApiCall = exports.withAuthErrorHandling = exports.isAuthenticationError = exports.ensureValidToken = exports.AuthenticationFailedError = exports.SessionSyncRequiredError = exports.verifyPublicCardAttestation = exports.parseAttestPayload = exports.parseIdPayload = exports.buildUserDid = exports.ORGANIZATION_CATEGORIES = exports.normalizeProfileLinks = exports.getNormalizedUserHandle = exports.getCanonicalUserHandle = exports.normalizeUserIdentityOrNull = exports.normalizeUserIdentity = exports.getNormalizedUserId = exports.OxyAppDataIdentifierError = exports.ServiceCredentialMismatchError = exports.oxyClient = exports.OXY_CLOUD_URL = exports.OxyAuthenticationTimeoutError = exports.OxyAuthenticationError = exports.AssetUrlResolutionError = exports.OxyServices = void 0;
22
- exports.safeJsonParse = exports.buildPaginationParams = exports.buildUrl = exports.buildSearchParams = exports.translate = exports.withRetry = exports.delay = exports.shouldAllowRequest = exports.recordSuccess = exports.recordFailure = exports.calculateBackoffInterval = exports.createCircuitBreakerState = exports.DEFAULT_CIRCUIT_BREAKER_CONFIG = exports.isRetryableError = exports.isNetworkError = exports.isServerError = exports.isRateLimitError = exports.isNotFoundError = exports.isForbiddenError = exports.isUnauthorizedError = exports.isAlreadyRegisteredError = exports.getErrorMessage = exports.getErrorStatus = exports.HttpStatus = exports.getSystemColorScheme = exports.systemPrefersDarkMode = exports.getOppositeTheme = exports.normalizeColorScheme = exports.normalizeTheme = exports.getContrastTextColor = exports.isLightColor = exports.withOpacity = exports.rgbToHex = exports.hexToRgb = exports.lightenColor = exports.darkenColor = exports.isWebBrowser = exports.isAndroid = exports.isIOS = exports.isNative = exports.isWeb = exports.setPlatformOS = exports.getPlatformOS = exports.getPrimaryLanguage = exports.getUserLanguages = exports.isRTLLocale = exports.getNativeLanguageName = exports.getLanguageName = exports.getLanguageMetadata = exports.isSupportedLocale = void 0;
23
- exports.buildOAuthAuthorizeUrl = exports.runColdBoot = exports.isOxyRpOrigin = exports.CENTRAL_IDP_APEX = exports.registrableApex = exports.getAccountColor = exports.formatPublicKeyHandle = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.createQuickAccount = exports.buildAccountsArray = exports.updateAvatarVisibility = exports.isDev = exports.consoleSink = exports.resetLoggerConfig = exports.getLoggerConfig = exports.configureLogger = exports.createLogger = exports.logger = exports.normalizeMultilineText = exports.normalizeInlineText = exports.validateAndSanitizeUserInput = exports.isValidObjectId = exports.sanitizeHTML = exports.sanitizeString = exports.isValidFileType = exports.isValidFileSize = exports.isValidDate = exports.isValidURL = exports.isValidUUID = exports.isValidObject = exports.isValidArray = exports.isRequiredBoolean = exports.isRequiredNumber = exports.isRequiredString = exports.DISPLAY_NAME_ORPHANED_MARK_SOURCE = exports.DISPLAY_NAME_DISALLOWED_SOURCE = exports.DISPLAY_NAME_ALLOWED_SCRIPTS = exports.isValidDisplayName = exports.isValidPassword = exports.isValidUsername = exports.isValidEmail = exports.PASSWORD_REGEX = exports.USERNAME_REGEX = exports.EMAIL_REGEX = exports.retryAsync = exports.validateRequiredFields = exports.handleHttpError = exports.createApiError = exports.ErrorCodes = void 0;
24
- exports.packageInfo = exports.runSessionColdBoot = exports.TOKEN_REFRESH_LEAD_MS = exports.startTokenRefreshScheduler = exports.installAuthRefreshHandler = exports.createAuthRefreshHandler = exports.refreshDeviceSecretArm = exports.refreshPersistedSession = exports.AUTH_STATE_STORAGE_KEY = exports.createMemoryAuthStateStore = exports.createNativeAuthStateStore = exports.createWebAuthStateStore = exports.createAccountDialogController = exports.AccountDialogController = exports.switchableAccountIds = exports.projectSwitchableAccounts = exports.accountIdsOf = exports.activeUserOf = exports.activeSessionIdOf = exports.deviceStateToClientSessions = exports.createSessionClient = exports.createSessionClientHost = exports.SessionClient = exports.redeemHubTicketOnHub = exports.syncHubAfterSignIn = exports.parseHubSyncReturnUrl = exports.normalizeOfficialReturnOrigin = exports.isAllowedDeviceJoinOrigin = exports.isOfficialWebOrigin = exports.isLoopbackOrigin = exports.isIdpHubOrigin = exports.buildHubSyncUrl = exports.buildIdpHubOrigin = exports.clearOAuthHandshake = exports.readOAuthHandshake = exports.persistOAuthHandshake = exports.normalizeOAuthRedirectUri = exports.OXY_CROSS_ORIGIN_RESTORE_ATTEMPTED_KEY = exports.OXY_SILENT_OAUTH_ATTEMPTED_KEY = exports.OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY = exports.OXY_OAUTH_STATE_STORAGE_KEY = exports.OXY_AUTHORIZE_URL = exports.DEFAULT_OAUTH_SCOPE = exports.generatePkcePair = exports.generateOAuthState = exports.computeCodeChallenge = void 0;
21
+ exports.FALLBACK_LOCALE = exports.SUPPORTED_LANGUAGES = exports.TopicSource = exports.TopicType = exports.SECURITY_EVENT_SEVERITY_MAP = exports.DeviceManager = exports.deriveSharedSecret = exports.AEAD_NONCE_LENGTH = exports.AEAD_KEY_LENGTH = exports.decryptAead = exports.encryptAead = exports.hkdfSha256 = exports.RecoveryPhraseService = exports.SignatureService = exports.updateIdentityMarker = exports.readIdentityMarker = exports.IdentityUnavailableError = exports.IdentityPersistError = exports.IdentityAlreadyExistsError = exports.KeyManager = exports.sessionsArraysEqual = exports.normalizeAndSortSessions = exports.mergeSessions = exports.authenticatedApiCall = exports.withAuthErrorHandling = exports.isAuthenticationError = exports.ensureValidToken = exports.AuthenticationFailedError = exports.SessionSyncRequiredError = exports.verifyPublicCardAttestation = exports.parseAttestPayload = exports.parseIdPayload = exports.buildUserDid = exports.ORGANIZATION_CATEGORIES = exports.normalizeProfileLinks = exports.getNormalizedUserHandle = exports.getCanonicalUserHandle = exports.normalizeUserIdentityOrNull = exports.normalizeUserIdentity = exports.getNormalizedUserId = exports.OxyAppDataIdentifierError = exports.parseCommonsApprovalExpiresAt = exports.getCommonsApprovalBlockingReason = exports.ServiceCredentialMismatchError = exports.oxyClient = exports.OXY_CLOUD_URL = exports.OxyAuthenticationTimeoutError = exports.OxyAuthenticationError = exports.AssetUrlResolutionError = exports.OxyServices = void 0;
22
+ exports.buildUrl = exports.buildSearchParams = exports.translate = exports.withRetry = exports.delay = exports.shouldAllowRequest = exports.recordSuccess = exports.recordFailure = exports.calculateBackoffInterval = exports.createCircuitBreakerState = exports.DEFAULT_CIRCUIT_BREAKER_CONFIG = exports.isRetryableError = exports.isNetworkError = exports.isServerError = exports.isRateLimitError = exports.isNotFoundError = exports.isForbiddenError = exports.isUnauthorizedError = exports.isAlreadyRegisteredError = exports.getErrorMessage = exports.getErrorStatus = exports.HttpStatus = exports.getSystemColorScheme = exports.systemPrefersDarkMode = exports.getOppositeTheme = exports.normalizeColorScheme = exports.normalizeTheme = exports.getContrastTextColor = exports.isLightColor = exports.withOpacity = exports.rgbToHex = exports.hexToRgb = exports.lightenColor = exports.darkenColor = exports.isWebBrowser = exports.isAndroid = exports.isIOS = exports.isNative = exports.isWeb = exports.setPlatformOS = exports.getPlatformOS = exports.getPrimaryLanguage = exports.getUserLanguages = exports.isRTLLocale = exports.getNativeLanguageName = exports.getLanguageName = exports.getLanguageMetadata = exports.isSupportedLocale = exports.normalizeLocale = exports.getBaseLanguage = void 0;
23
+ exports.isOxyRpOrigin = exports.CENTRAL_IDP_APEX = exports.registrableApex = exports.getAccountColor = exports.formatPublicKeyHandle = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.createQuickAccount = exports.buildAccountsArray = exports.updateAvatarVisibility = exports.isDev = exports.consoleSink = exports.resetLoggerConfig = exports.getLoggerConfig = exports.configureLogger = exports.createLogger = exports.logger = exports.normalizeMultilineText = exports.normalizeInlineText = exports.validateAndSanitizeUserInput = exports.isValidObjectId = exports.sanitizeHTML = exports.sanitizeString = exports.isValidFileType = exports.isValidFileSize = exports.isValidDate = exports.isValidURL = exports.isValidUUID = exports.isValidObject = exports.isValidArray = exports.isRequiredBoolean = exports.isRequiredNumber = exports.isRequiredString = exports.DISPLAY_NAME_ORPHANED_MARK_SOURCE = exports.DISPLAY_NAME_DISALLOWED_SOURCE = exports.DISPLAY_NAME_ALLOWED_SCRIPTS = exports.isValidDisplayName = exports.isValidPassword = exports.isValidUsername = exports.isValidEmail = exports.PASSWORD_REGEX = exports.USERNAME_REGEX = exports.EMAIL_REGEX = exports.retryAsync = exports.validateRequiredFields = exports.handleHttpError = exports.createApiError = exports.ErrorCodes = exports.safeJsonParse = exports.buildPaginationParams = void 0;
24
+ exports.packageInfo = exports.runSessionColdBoot = exports.TOKEN_REFRESH_LEAD_MS = exports.startTokenRefreshScheduler = exports.installAuthRefreshHandler = exports.createAuthRefreshHandler = exports.refreshDeviceSecretArm = exports.refreshPersistedSession = exports.AUTH_STATE_STORAGE_KEY = exports.createMemoryAuthStateStore = exports.createNativeAuthStateStore = exports.createWebAuthStateStore = exports.createAccountDialogController = exports.AccountDialogController = exports.switchableAccountIds = exports.projectSwitchableAccounts = exports.accountIdsOf = exports.activeUserOf = exports.activeSessionIdOf = exports.deviceStateToClientSessions = exports.createSessionClient = exports.createSessionClientHost = exports.SessionClient = exports.redeemHubTicketOnHub = exports.syncHubAfterSignIn = exports.parseHubSyncReturnUrl = exports.normalizeOfficialReturnOrigin = exports.isAllowedDeviceJoinOrigin = exports.isOfficialWebOrigin = exports.isLoopbackOrigin = exports.isIdpHubOrigin = exports.buildHubSyncUrl = exports.buildIdpHubOrigin = exports.clearOAuthHandshake = exports.readOAuthHandshake = exports.persistOAuthHandshake = exports.normalizeOAuthRedirectUri = exports.OXY_CROSS_ORIGIN_RESTORE_ATTEMPTED_KEY = exports.OXY_SILENT_OAUTH_ATTEMPTED_KEY = exports.OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY = exports.OXY_OAUTH_STATE_STORAGE_KEY = exports.OXY_AUTHORIZE_URL = exports.DEFAULT_OAUTH_SCOPE = exports.generatePkcePair = exports.generateOAuthState = exports.computeCodeChallenge = exports.buildOAuthAuthorizeUrl = exports.runColdBoot = void 0;
25
25
  // Ensure crypto polyfills are loaded before anything else
26
26
  require("./crypto/polyfill");
27
27
  // ---------------------------------------------------------------------------
@@ -40,6 +40,9 @@ Object.defineProperty(exports, "oxyClient", { enumerable: true, get: function ()
40
40
  // ---------------------------------------------------------------------------
41
41
  var OxyServices_auth_1 = require("./mixins/OxyServices.auth");
42
42
  Object.defineProperty(exports, "ServiceCredentialMismatchError", { enumerable: true, get: function () { return OxyServices_auth_1.ServiceCredentialMismatchError; } });
43
+ var commonsApproval_1 = require("./utils/commonsApproval");
44
+ Object.defineProperty(exports, "getCommonsApprovalBlockingReason", { enumerable: true, get: function () { return commonsApproval_1.getCommonsApprovalBlockingReason; } });
45
+ Object.defineProperty(exports, "parseCommonsApprovalExpiresAt", { enumerable: true, get: function () { return commonsApproval_1.parseCommonsApprovalExpiresAt; } });
43
46
  var OxyServices_appData_1 = require("./mixins/OxyServices.appData");
44
47
  Object.defineProperty(exports, "OxyAppDataIdentifierError", { enumerable: true, get: function () { return OxyServices_appData_1.OxyAppDataIdentifierError; } });
45
48
  // ---------------------------------------------------------------------------
@@ -1,8 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ServiceCredentialMismatchError = void 0;
3
+ exports.ServiceCredentialMismatchError = exports.parseCommonsApprovalExpiresAt = exports.getCommonsApprovalBlockingReason = void 0;
4
4
  exports.OxyServicesAuthMixin = OxyServicesAuthMixin;
5
5
  const contracts_1 = require("@oxyhq/contracts");
6
+ var commonsApproval_1 = require("../utils/commonsApproval");
7
+ Object.defineProperty(exports, "getCommonsApprovalBlockingReason", { enumerable: true, get: function () { return commonsApproval_1.getCommonsApprovalBlockingReason; } });
8
+ Object.defineProperty(exports, "parseCommonsApprovalExpiresAt", { enumerable: true, get: function () { return commonsApproval_1.parseCommonsApprovalExpiresAt; } });
6
9
  const OxyServices_errors_1 = require("../OxyServices.errors");
7
10
  const keyManager_1 = require("../crypto/keyManager");
8
11
  const signatureService_1 = require("../crypto/signatureService");
@@ -141,6 +141,17 @@ function OxyServicesIdentityBackupMixin(Base) {
141
141
  const aad = buildBackupAad(envelope.version, envelope.publicKeyHint);
142
142
  const plaintext = (0, aead_1.decryptAead)(backupKey, fromHex(envelope.nonce), fromHex(envelope.ciphertext), aad);
143
143
  const payload = JSON.parse(new TextDecoder().decode(plaintext));
144
+ if (!payload.privateKey || !payload.publicKey) {
145
+ throw new Error('Backup payload is missing key material');
146
+ }
147
+ const derivedFromPhrase = await recoveryPhrase_1.RecoveryPhraseService.derivePublicKeyFromPhrase(phrase);
148
+ const derivedFromPrivate = keyManager_1.KeyManager.derivePublicKey(payload.privateKey);
149
+ const phrasePk = derivedFromPhrase.toLowerCase();
150
+ const payloadPk = payload.publicKey.toLowerCase();
151
+ const privatePk = derivedFromPrivate.toLowerCase();
152
+ if (phrasePk !== payloadPk || privatePk !== payloadPk) {
153
+ throw new Error('Backup payload does not match the recovery phrase');
154
+ }
144
155
  // Persist the recovered key. Native-only; refuses to clobber a different
145
156
  // identity unless overwrite — the IdentityAlreadyExistsError propagates.
146
157
  return await keyManager_1.KeyManager.importKeyPair(payload.privateKey, {
@@ -423,6 +423,10 @@ function OxyServicesUserMixin(Base) {
423
423
  const result = await this.makeRequest('PATCH', `/privacy/${id}/privacy`, settings, {
424
424
  cache: false,
425
425
  });
426
+ this.clearCacheByPrefix('GET:/session/user/');
427
+ this.clearCacheByPrefix('GET:/users/me');
428
+ this.clearCacheByPrefix('GET:/profiles/username/');
429
+ this.clearCacheEntry(`GET:/users/${id}`);
426
430
  this.clearCacheEntry(`GET:/privacy/${id}/privacy`);
427
431
  return result;
428
432
  }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getCommonsApprovalBlockingReason = getCommonsApprovalBlockingReason;
4
+ exports.parseCommonsApprovalExpiresAt = parseCommonsApprovalExpiresAt;
5
+ /**
6
+ * Returns a user-facing blocking reason when an approval payload must not be
7
+ * shown as actionable, or `null` when the request is still pending and valid.
8
+ */
9
+ function getCommonsApprovalBlockingReason(info) {
10
+ if (!info.application?.id) {
11
+ return 'The requesting application could not be resolved.';
12
+ }
13
+ if (info.status !== 'pending') {
14
+ return 'This sign-in request is invalid, already used, or expired.';
15
+ }
16
+ const expiresAtMs = parseCommonsApprovalExpiresAt(info.expiresAt);
17
+ if (expiresAtMs !== null && expiresAtMs < Date.now()) {
18
+ return 'This sign-in request has expired. Ask for a new QR code.';
19
+ }
20
+ return null;
21
+ }
22
+ /** Normalize API `expiresAt` (number or ISO string) to epoch ms. */
23
+ function parseCommonsApprovalExpiresAt(expiresAt) {
24
+ if (typeof expiresAt === 'number' && Number.isFinite(expiresAt))
25
+ return expiresAt;
26
+ if (typeof expiresAt === 'string') {
27
+ const ms = Date.parse(expiresAt);
28
+ return Number.isFinite(ms) ? ms : null;
29
+ }
30
+ return null;
31
+ }