@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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/crypto/keyManager.js +89 -0
- package/dist/cjs/index.js +7 -4
- package/dist/cjs/mixins/OxyServices.auth.js +4 -1
- package/dist/cjs/mixins/OxyServices.identityBackup.js +11 -0
- package/dist/cjs/mixins/OxyServices.user.js +4 -0
- package/dist/cjs/utils/commonsApproval.js +31 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/crypto/keyManager.js +89 -0
- package/dist/esm/index.js +2 -1
- package/dist/esm/mixins/OxyServices.auth.js +1 -0
- package/dist/esm/mixins/OxyServices.identityBackup.js +11 -0
- package/dist/esm/mixins/OxyServices.user.js +4 -0
- package/dist/esm/utils/commonsApproval.js +27 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/crypto/keyManager.d.ts +33 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/mixins/OxyServices.auth.d.ts +4 -4
- package/dist/types/utils/commonsApproval.d.ts +13 -0
- package/package.json +1 -1
- package/src/crypto/__tests__/keyManager.recoveryMnemonic.test.ts +138 -0
- package/src/crypto/keyManager.ts +99 -0
- package/src/index.ts +7 -1
- package/src/mixins/OxyServices.auth.ts +9 -5
- package/src/mixins/OxyServices.identityBackup.ts +13 -0
- package/src/mixins/OxyServices.user.ts +4 -0
- package/src/mixins/__tests__/identityBackup.test.ts +57 -0
- package/src/mixins/__tests__/privacyCacheInvalidation.test.ts +6 -0
- package/src/utils/__tests__/commonsApproval.test.ts +60 -0
- package/src/utils/commonsApproval.ts +39 -0
|
@@ -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.
|
|
22
|
-
exports.
|
|
23
|
-
exports.
|
|
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
|
+
}
|