@oxyhq/core 11.0.1 → 12.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/index.js +7 -5
- package/dist/cjs/mixins/OxyServices.auth.js +6 -117
- package/dist/cjs/mixins/OxyServices.identity.js +16 -12
- package/dist/cjs/session/accountDialogController.js +84 -75
- package/dist/cjs/utils/officialOrigins.js +6 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/index.js +6 -5
- package/dist/esm/mixins/OxyServices.auth.js +6 -117
- package/dist/esm/mixins/OxyServices.identity.js +16 -12
- package/dist/esm/session/accountDialogController.js +84 -75
- package/dist/esm/utils/officialOrigins.js +6 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/mixins/OxyServices.auth.d.ts +4 -48
- package/dist/types/mixins/OxyServices.identity.d.ts +13 -9
- package/dist/types/session/accountDialogController.d.ts +84 -49
- package/dist/types/utils/officialOrigins.d.ts +6 -0
- package/package.json +2 -2
- package/src/index.ts +7 -4
- package/src/mixins/OxyServices.auth.ts +6 -144
- package/src/mixins/OxyServices.identity.ts +19 -15
- package/src/mixins/__tests__/OxyServices.identity.test.ts +26 -14
- package/src/mixins/__tests__/webauthnAuth.test.ts +1 -1
- package/src/session/__tests__/accountDialogController.test.ts +75 -64
- package/src/session/__tests__/accountProjection.test.ts +30 -0
- package/src/session/accountDialogController.ts +131 -104
- package/src/utils/__tests__/officialOrigins.test.ts +14 -0
- package/src/utils/officialOrigins.ts +6 -1
- package/src/mixins/__tests__/passwordSignIn.test.ts +0 -115
package/dist/cjs/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* ```ts
|
|
12
12
|
* import { OxyServices, oxyClient } from '@oxyhq/core';
|
|
13
13
|
*
|
|
14
|
-
* const user = await oxyClient.
|
|
14
|
+
* const user = await oxyClient.getCurrentUser();
|
|
15
15
|
* ```
|
|
16
16
|
*
|
|
17
17
|
* Every export below is NOMINAL — no `export *`, no barrels, no compat shims.
|
|
@@ -21,7 +21,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
21
21
|
exports.isWeb = exports.setPlatformOS = exports.getPlatformOS = exports.getPrimaryLanguage = exports.getUserLanguages = exports.isRTLLocale = exports.getNativeLanguageName = exports.getLanguageName = exports.getLanguageMetadata = exports.isSupportedLocale = exports.normalizeLocale = exports.getBaseLanguage = exports.FALLBACK_LOCALE = exports.SUPPORTED_LANGUAGES = exports.TopicSource = exports.TopicType = exports.SECURITY_EVENT_SEVERITY_MAP = exports.DeviceManager = exports.RecoveryPhraseService = exports.SignatureService = 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.OxyServices = void 0;
|
|
22
22
|
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 = 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 = void 0;
|
|
23
23
|
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 = 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.isValidDisplayName = exports.isValidPassword = 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.isIdpHubOrigin = exports.buildHubSyncUrl = exports.buildIdpHubOrigin = 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 = void 0;
|
|
25
25
|
// Ensure crypto polyfills are loaded before anything else
|
|
26
26
|
require("./crypto/polyfill");
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
@@ -300,6 +300,7 @@ var officialOrigins_1 = require("./utils/officialOrigins");
|
|
|
300
300
|
Object.defineProperty(exports, "buildIdpHubOrigin", { enumerable: true, get: function () { return officialOrigins_1.buildIdpHubOrigin; } });
|
|
301
301
|
Object.defineProperty(exports, "buildHubSyncUrl", { enumerable: true, get: function () { return officialOrigins_1.buildHubSyncUrl; } });
|
|
302
302
|
Object.defineProperty(exports, "isIdpHubOrigin", { enumerable: true, get: function () { return officialOrigins_1.isIdpHubOrigin; } });
|
|
303
|
+
Object.defineProperty(exports, "isLoopbackOrigin", { enumerable: true, get: function () { return officialOrigins_1.isLoopbackOrigin; } });
|
|
303
304
|
Object.defineProperty(exports, "isOfficialWebOrigin", { enumerable: true, get: function () { return officialOrigins_1.isOfficialWebOrigin; } });
|
|
304
305
|
Object.defineProperty(exports, "isAllowedDeviceJoinOrigin", { enumerable: true, get: function () { return officialOrigins_1.isAllowedDeviceJoinOrigin; } });
|
|
305
306
|
Object.defineProperty(exports, "normalizeOfficialReturnOrigin", { enumerable: true, get: function () { return officialOrigins_1.normalizeOfficialReturnOrigin; } });
|
|
@@ -335,9 +336,10 @@ Object.defineProperty(exports, "projectSwitchableAccounts", { enumerable: true,
|
|
|
335
336
|
Object.defineProperty(exports, "switchableAccountIds", { enumerable: true, get: function () { return accountProjection_1.switchableAccountIds; } });
|
|
336
337
|
// Headless controller for the unified account dialog. Framework-agnostic
|
|
337
338
|
// state machine + subscribe/getSnapshot store (bind via `useSyncExternalStore`)
|
|
338
|
-
// —
|
|
339
|
-
//
|
|
340
|
-
//
|
|
339
|
+
// — sign-in is passkey (WebAuthn) or the Commons QR / shared-keychain handoff;
|
|
340
|
+
// password, social login, and 2FA were removed ecosystem-wide. Reuses
|
|
341
|
+
// `SessionClient.switchAccount` / `oxyServices.switchToAccount` for the uniform
|
|
342
|
+
// switch and the existing device-flow methods for sign-in.
|
|
341
343
|
var accountDialogController_1 = require("./session/accountDialogController");
|
|
342
344
|
Object.defineProperty(exports, "AccountDialogController", { enumerable: true, get: function () { return accountDialogController_1.AccountDialogController; } });
|
|
343
345
|
Object.defineProperty(exports, "createAccountDialogController", { enumerable: true, get: function () { return accountDialogController_1.createAccountDialogController; } });
|
|
@@ -741,117 +741,6 @@ function OxyServicesAuthMixin(Base) {
|
|
|
741
741
|
throw this.handleError(error);
|
|
742
742
|
}
|
|
743
743
|
}
|
|
744
|
-
/**
|
|
745
|
-
* Register a new user with email/username and password
|
|
746
|
-
*/
|
|
747
|
-
async signUp(username, email, password, deviceName, deviceFingerprint) {
|
|
748
|
-
try {
|
|
749
|
-
const session = await this.makeRequest('POST', '/auth/signup', {
|
|
750
|
-
username,
|
|
751
|
-
email,
|
|
752
|
-
password,
|
|
753
|
-
deviceName,
|
|
754
|
-
deviceFingerprint,
|
|
755
|
-
}, { cache: false });
|
|
756
|
-
return {
|
|
757
|
-
...session,
|
|
758
|
-
user: (0, userIdentity_1.normalizeUserIdentity)(session.user),
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
catch (error) {
|
|
762
|
-
throw this.handleError(error);
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
/**
|
|
766
|
-
* Sign in with email or username and password
|
|
767
|
-
*/
|
|
768
|
-
async signIn(identifier, password, deviceName, deviceFingerprint) {
|
|
769
|
-
try {
|
|
770
|
-
const session = await this.makeRequest('POST', '/auth/login', {
|
|
771
|
-
identifier,
|
|
772
|
-
password,
|
|
773
|
-
deviceName,
|
|
774
|
-
deviceFingerprint,
|
|
775
|
-
}, { cache: false });
|
|
776
|
-
return {
|
|
777
|
-
...session,
|
|
778
|
-
user: (0, userIdentity_1.normalizeUserIdentity)(session.user),
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
catch (error) {
|
|
782
|
-
throw this.handleError(error);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
/**
|
|
786
|
-
* Convenience helper for email sign-in
|
|
787
|
-
*/
|
|
788
|
-
async signInWithEmail(email, password, deviceName, deviceFingerprint) {
|
|
789
|
-
return this.signIn(email, password, deviceName, deviceFingerprint);
|
|
790
|
-
}
|
|
791
|
-
/**
|
|
792
|
-
* Device-first password sign-in. Unlike the legacy {@link signIn} (which
|
|
793
|
-
* assumes a one-step session and is kept intact for existing callers until
|
|
794
|
-
* the F4 cutover), this returns the FULL `POST /auth/login` contract — the
|
|
795
|
-
* discriminated {@link LoginResult}: either a 2FA challenge
|
|
796
|
-
* (`{ twoFactorRequired, loginToken }`) to complete via
|
|
797
|
-
* {@link completeTwoFactorSignIn}, or a session arm.
|
|
798
|
-
*
|
|
799
|
-
* On the session arm, a returned access token is planted immediately
|
|
800
|
-
* (mirroring {@link verifyChallenge}), so the caller has an authenticated
|
|
801
|
-
* client without a second round-trip. The response's `deviceId` +
|
|
802
|
-
* `deviceSecret` are the zero-cookie restore credential the caller persists.
|
|
803
|
-
*/
|
|
804
|
-
async passwordSignIn(identifier, password, options = {}) {
|
|
805
|
-
try {
|
|
806
|
-
const res = await this.makeRequest('POST', '/auth/login', {
|
|
807
|
-
identifier,
|
|
808
|
-
password,
|
|
809
|
-
deviceName: options.deviceName,
|
|
810
|
-
deviceFingerprint: options.deviceFingerprint,
|
|
811
|
-
...(options.deviceId ? { deviceId: options.deviceId } : {}),
|
|
812
|
-
}, { cache: false });
|
|
813
|
-
const parsed = (0, contracts_1.safeParseContract)(contracts_1.loginResultSchema, res);
|
|
814
|
-
if (!parsed) {
|
|
815
|
-
throw new Error('auth/login returned an unexpected response shape');
|
|
816
|
-
}
|
|
817
|
-
if (!('twoFactorRequired' in parsed) && parsed.accessToken) {
|
|
818
|
-
this.setTokens(parsed.accessToken);
|
|
819
|
-
}
|
|
820
|
-
return parsed;
|
|
821
|
-
}
|
|
822
|
-
catch (error) {
|
|
823
|
-
throw this.handleError(error);
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
/**
|
|
827
|
-
* Complete a 2FA-gated sign-in started by {@link passwordSignIn}. Presents
|
|
828
|
-
* the short-lived `loginToken` with either a TOTP `token` or a `backupCode`
|
|
829
|
-
* to `POST /security/2fa/verify-login`, which must resolve to the session
|
|
830
|
-
* arm of {@link LoginResult} (a second 2FA challenge here is a protocol
|
|
831
|
-
* error). A returned access token is planted immediately.
|
|
832
|
-
*/
|
|
833
|
-
async completeTwoFactorSignIn(params) {
|
|
834
|
-
try {
|
|
835
|
-
const res = await this.makeRequest('POST', '/security/2fa/verify-login', {
|
|
836
|
-
loginToken: params.loginToken,
|
|
837
|
-
token: params.token,
|
|
838
|
-
backupCode: params.backupCode,
|
|
839
|
-
deviceName: params.deviceName,
|
|
840
|
-
...(params.deviceId ? { deviceId: params.deviceId } : {}),
|
|
841
|
-
}, { cache: false });
|
|
842
|
-
const parsed = (0, contracts_1.safeParseContract)(contracts_1.loginResultSchema, res);
|
|
843
|
-
if (!parsed || 'twoFactorRequired' in parsed) {
|
|
844
|
-
throw new Error('security/2fa/verify-login returned an unexpected response shape');
|
|
845
|
-
}
|
|
846
|
-
if (parsed.accessToken) {
|
|
847
|
-
this.setTokens(parsed.accessToken);
|
|
848
|
-
}
|
|
849
|
-
return parsed;
|
|
850
|
-
}
|
|
851
|
-
catch (error) {
|
|
852
|
-
throw this.handleError(error);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
744
|
/**
|
|
856
745
|
* Begin a WebAuthn / passkey REGISTRATION ceremony. Requests the
|
|
857
746
|
* `PublicKeyCredentialCreationOptions` the browser's `navigator.credentials
|
|
@@ -896,7 +785,7 @@ function OxyServicesAuthMixin(Base) {
|
|
|
896
785
|
if (!parsed) {
|
|
897
786
|
throw new Error('auth/webauthn/register/verify returned an unexpected response shape');
|
|
898
787
|
}
|
|
899
|
-
if (
|
|
788
|
+
if (parsed.accessToken) {
|
|
900
789
|
this.setTokens(parsed.accessToken);
|
|
901
790
|
}
|
|
902
791
|
return parsed;
|
|
@@ -934,9 +823,9 @@ function OxyServicesAuthMixin(Base) {
|
|
|
934
823
|
* Finish a WebAuthn / passkey AUTHENTICATION ceremony. Forwards the opaque
|
|
935
824
|
* browser `AuthenticationResponseJSON` (`response`) alongside the
|
|
936
825
|
* device-session envelope. Resolves to the SAME {@link LoginResult} contract
|
|
937
|
-
* as `POST /auth/verify`;
|
|
938
|
-
*
|
|
939
|
-
*
|
|
826
|
+
* as `POST /auth/verify`; the access token is planted immediately, and the
|
|
827
|
+
* response's `deviceId` + `deviceSecret` are the zero-cookie restore
|
|
828
|
+
* credential.
|
|
940
829
|
*/
|
|
941
830
|
async webauthnLoginVerify(response, envelope = {}) {
|
|
942
831
|
try {
|
|
@@ -945,7 +834,7 @@ function OxyServicesAuthMixin(Base) {
|
|
|
945
834
|
if (!parsed) {
|
|
946
835
|
throw new Error('auth/webauthn/login/verify returned an unexpected response shape');
|
|
947
836
|
}
|
|
948
|
-
if (
|
|
837
|
+
if (parsed.accessToken) {
|
|
949
838
|
this.setTokens(parsed.accessToken);
|
|
950
839
|
}
|
|
951
840
|
return parsed;
|
|
@@ -956,7 +845,7 @@ function OxyServicesAuthMixin(Base) {
|
|
|
956
845
|
}
|
|
957
846
|
/**
|
|
958
847
|
* Exchange an OAuth authorization code (returned to the RP redirect URI
|
|
959
|
-
* after
|
|
848
|
+
* after sign-in at auth.oxy.so) for a device-first session.
|
|
960
849
|
* Public first-party clients use PKCE (`codeVerifier`); the access token is
|
|
961
850
|
* planted immediately on success.
|
|
962
851
|
*/
|
|
@@ -104,15 +104,15 @@ function OxyServicesIdentityMixin(Base) {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
107
|
+
* Unlink an authentication method from the current account. The server
|
|
108
|
+
* refuses to remove the last remaining method (the account would become
|
|
109
|
+
* inaccessible). Unlinking `identity` downgrades the account to custodial.
|
|
109
110
|
*
|
|
110
|
-
* @param
|
|
111
|
-
* @param password - The new password (server enforces strength rules).
|
|
111
|
+
* @param type - The auth-method type to remove.
|
|
112
112
|
*/
|
|
113
|
-
async
|
|
113
|
+
async unlinkAuthMethod(type) {
|
|
114
114
|
try {
|
|
115
|
-
const result = await this.makeRequest('
|
|
115
|
+
const result = await this.makeRequest('DELETE', `/auth/link/${encodeURIComponent(type)}`, undefined, { cache: false });
|
|
116
116
|
this._invalidateIdentityCaches(this.getCurrentUserId());
|
|
117
117
|
return result;
|
|
118
118
|
}
|
|
@@ -121,15 +121,19 @@ function OxyServicesIdentityMixin(Base) {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
/**
|
|
124
|
-
*
|
|
125
|
-
* refuses to remove the last remaining method (the account would become
|
|
126
|
-
* inaccessible). Unlinking `identity` downgrades the account to custodial.
|
|
124
|
+
* Remove ONE passkey (WebAuthn credential) from the current account.
|
|
127
125
|
*
|
|
128
|
-
*
|
|
126
|
+
* Passkeys are per-credential, so unlike {@link unlinkAuthMethod} (which
|
|
127
|
+
* removes an auth method by type) this targets a specific credential id.
|
|
128
|
+
* The server refuses to remove the last remaining auth method (the account
|
|
129
|
+
* would become inaccessible) and deletes the stored `WebauthnCredential`.
|
|
130
|
+
*
|
|
131
|
+
* @param credentialId - The passkey's public credential id
|
|
132
|
+
* (`AuthMethodEntry.credentialId`).
|
|
129
133
|
*/
|
|
130
|
-
async
|
|
134
|
+
async removePasskey(credentialId) {
|
|
131
135
|
try {
|
|
132
|
-
const result = await this.makeRequest('DELETE', `/auth/link/${encodeURIComponent(
|
|
136
|
+
const result = await this.makeRequest('DELETE', `/auth/link/webauthn/${encodeURIComponent(credentialId)}`, undefined, { cache: false });
|
|
133
137
|
this._invalidateIdentityCaches(this.getCurrentUserId());
|
|
134
138
|
return result;
|
|
135
139
|
}
|
|
@@ -13,26 +13,29 @@
|
|
|
13
13
|
* - the unified account list (via {@link projectSwitchableAccounts}), fetched
|
|
14
14
|
* from `SessionClient` state ∪ `oxyServices.listAccounts()` and hydrated
|
|
15
15
|
* with `oxyServices.getUsersByIds()`;
|
|
16
|
-
* - the dialog `view` state machine (`accounts` | `signin` | `qr` | `add`
|
|
16
|
+
* - the dialog `view` state machine (`accounts` | `signin` | `qr` | `add` |
|
|
17
|
+
* `signup`);
|
|
17
18
|
* - `switchTo` (the uniform switch: `SessionClient.switchAccount` for an
|
|
18
19
|
* account already on the device, `oxyServices.switchToAccount` to mint on
|
|
19
20
|
* first entry into a graph account — reusing the existing SDK primitives, no
|
|
20
21
|
* new switch path);
|
|
21
22
|
* - the "Sign in with Oxy" device flow (same-device shared-keychain via
|
|
22
23
|
* `oxyServices.signInWithSharedIdentity`, else the cross-device QR handoff
|
|
23
|
-
* via `startCommonsSignIn` → poll → `claimSessionByToken`)
|
|
24
|
+
* via `startCommonsSignIn` → poll → `claimSessionByToken`);
|
|
25
|
+
* - `commonsAvailability` — whether Commons is installed on this device
|
|
26
|
+
* (native only, via the injected `canOpenApp` probe), so the QR view can
|
|
27
|
+
* offer a "Get Commons" fallback instead of a same-device dead end.
|
|
24
28
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
29
|
+
* Sign-in is passkey (WebAuthn) or the Commons QR / shared-keychain handoff —
|
|
30
|
+
* password, social login, and 2FA were removed ecosystem-wide. Account
|
|
31
|
+
* creation (`signup` view) is the same two identity backends: a passkey
|
|
32
|
+
* ceremony on web, or a Commons-created identity.
|
|
28
33
|
*/
|
|
29
34
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
35
|
exports.AccountDialogController = void 0;
|
|
31
36
|
exports.createAccountDialogController = createAccountDialogController;
|
|
32
37
|
const logger_1 = require("../logger");
|
|
33
38
|
const errorUtils_1 = require("../utils/errorUtils");
|
|
34
|
-
const authWebUrl_1 = require("../utils/authWebUrl");
|
|
35
|
-
const oauthPkce_1 = require("../utils/oauthPkce");
|
|
36
39
|
const accountProjection_1 = require("./accountProjection");
|
|
37
40
|
/**
|
|
38
41
|
* Slow FALLBACK poll cadence for the QR flow. The `/auth-session` socket delivers
|
|
@@ -70,6 +73,7 @@ class AccountDialogController {
|
|
|
70
73
|
this.error = null;
|
|
71
74
|
this.switchingAccountId = null;
|
|
72
75
|
this.signIn = IDLE_SIGN_IN;
|
|
76
|
+
this.commonsAvailability = 'unknown';
|
|
73
77
|
// --- Sign-in device-flow bookkeeping ---
|
|
74
78
|
/** The secret device-flow token of the active QR flow (never surfaced). */
|
|
75
79
|
this.signInToken = null;
|
|
@@ -98,9 +102,8 @@ class AccountDialogController {
|
|
|
98
102
|
this.clientId = options.clientId ?? null;
|
|
99
103
|
this.locale = options.locale;
|
|
100
104
|
this.commitSession = options.commitSession;
|
|
105
|
+
this.commitSwitchedSession = options.commitSwitchedSession;
|
|
101
106
|
this.onSignedIn = options.onSignedIn;
|
|
102
|
-
this.idpApex = options.idpApex ?? authWebUrl_1.CENTRAL_IDP_APEX;
|
|
103
|
-
this.authRedirectUri = options.authRedirectUri ?? null;
|
|
104
107
|
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
105
108
|
this.openUrl = options.openUrl;
|
|
106
109
|
this.canOpenApp = options.canOpenApp;
|
|
@@ -156,6 +159,11 @@ class AccountDialogController {
|
|
|
156
159
|
// bearer is already planted (warm start); when signed out (cold boot before
|
|
157
160
|
// restore) it re-projects from device state and makes NO private call.
|
|
158
161
|
void this.refresh();
|
|
162
|
+
// Eager, cached Commons-availability probe (native only — a no-op when no
|
|
163
|
+
// `canOpenApp` was injected). It's a cheap local OS check, so by the time a
|
|
164
|
+
// user actually opens the sign-in entry it has almost always resolved —
|
|
165
|
+
// `showQr`'s own lazy probe below is only the safety net for the rare race.
|
|
166
|
+
void this.resolveCommonsAvailability();
|
|
159
167
|
}
|
|
160
168
|
/**
|
|
161
169
|
* Stop driving the dialog: unsubscribe from `SessionClient` and tear down the
|
|
@@ -239,6 +247,10 @@ class AccountDialogController {
|
|
|
239
247
|
add() {
|
|
240
248
|
this.setView('add');
|
|
241
249
|
}
|
|
250
|
+
/** Switch to the "create account" view (passkey / Commons signup entry). */
|
|
251
|
+
startSignup() {
|
|
252
|
+
this.setView('signup');
|
|
253
|
+
}
|
|
242
254
|
// =========================================================================
|
|
243
255
|
// Account list
|
|
244
256
|
// =========================================================================
|
|
@@ -384,7 +396,11 @@ class AccountDialogController {
|
|
|
384
396
|
expiresAt: result.expiresAt,
|
|
385
397
|
user: result.user,
|
|
386
398
|
accessToken: result.accessToken,
|
|
387
|
-
}, result.user
|
|
399
|
+
}, result.user,
|
|
400
|
+
// A switch is IN-PLACE: commit without the hub-sync redirect (the
|
|
401
|
+
// device is already known/synced). Cross-tab/app propagation rides the
|
|
402
|
+
// server's `session_state` socket broadcast, not a navigation.
|
|
403
|
+
{ fromSwitch: true });
|
|
388
404
|
}
|
|
389
405
|
// Re-project + refetch immediately; the subscription also fires.
|
|
390
406
|
await this.refresh();
|
|
@@ -452,32 +468,63 @@ class AccountDialogController {
|
|
|
452
468
|
// connect, so it now runs at the slow fallback cadence.
|
|
453
469
|
this.openAuthSessionSocket(handle.sessionToken);
|
|
454
470
|
this.scheduleNextPoll(handle.sessionToken);
|
|
455
|
-
// Same-device convenience: if Commons is installed (native
|
|
456
|
-
//
|
|
457
|
-
//
|
|
471
|
+
// Same-device convenience: if Commons is confirmed installed (native
|
|
472
|
+
// only — stays `'unknown'` on web, where this never opens anything),
|
|
473
|
+
// deep-link straight into its approve screen with the same
|
|
474
|
+
// `oxycommons://approve?...` payload the QR encodes. The QR + polling
|
|
458
475
|
// stay live as the fallback, so a user who dismisses the app-open still
|
|
459
476
|
// completes the sign-in by scanning.
|
|
460
|
-
void this.
|
|
477
|
+
void this.deepLinkIntoCommonsIfAvailable(handle.qrPayload);
|
|
461
478
|
}
|
|
462
479
|
catch (error) {
|
|
463
480
|
this.setSignIn({ ...IDLE_SIGN_IN, phase: 'error', error: errorMessage(error) });
|
|
464
481
|
}
|
|
465
482
|
}
|
|
466
483
|
/**
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
484
|
+
* Resolve whether Commons is installed on this device via the injected
|
|
485
|
+
* `canOpenApp` probe, updating {@link commonsAvailability} as durable,
|
|
486
|
+
* observable snapshot state. Native only — a no-op when `canOpenApp` was
|
|
487
|
+
* not injected (web), where `commonsAvailability` stays `'unknown'` forever
|
|
488
|
+
* and the QR view renders unconditionally (no gating).
|
|
489
|
+
*
|
|
490
|
+
* Replaces the old `maybeOpenCommons` fire-and-forget probe, whose outcome
|
|
491
|
+
* was only ever reflected by whether Commons silently opened — a probe
|
|
492
|
+
* failure or "not installed" answer was swallowed into a debug log with no
|
|
493
|
+
* way for the UI to react. `commonsAvailability` fixes that.
|
|
470
494
|
*/
|
|
471
|
-
async
|
|
472
|
-
if (!this.canOpenApp
|
|
495
|
+
async resolveCommonsAvailability() {
|
|
496
|
+
if (!this.canOpenApp)
|
|
473
497
|
return;
|
|
498
|
+
this.commonsAvailability = 'checking';
|
|
499
|
+
this.emit();
|
|
500
|
+
let available = false;
|
|
474
501
|
try {
|
|
475
|
-
|
|
476
|
-
this.openUrl(qrPayload);
|
|
477
|
-
}
|
|
502
|
+
available = await this.canOpenApp(COMMONS_APP_SCHEME);
|
|
478
503
|
}
|
|
479
504
|
catch (error) {
|
|
480
|
-
logger_1.logger.debug('[AccountDialogController] Commons
|
|
505
|
+
logger_1.logger.debug('[AccountDialogController] Commons availability probe failed', { component: 'AccountDialogController' }, error);
|
|
506
|
+
available = false; // fail-closed — treat a probe error as "not installed"
|
|
507
|
+
}
|
|
508
|
+
this.commonsAvailability = available ? 'available' : 'unavailable';
|
|
509
|
+
this.emit();
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* When Commons is confirmed installed, deep-link straight into its approve
|
|
513
|
+
* screen via the injected `openUrl` with the same `oxycommons://approve?...`
|
|
514
|
+
* payload the QR encodes. Best-effort and non-blocking — the QR/polling
|
|
515
|
+
* fallback stays live regardless of the outcome here.
|
|
516
|
+
*/
|
|
517
|
+
async deepLinkIntoCommonsIfAvailable(qrPayload) {
|
|
518
|
+
if (!this.openUrl)
|
|
519
|
+
return;
|
|
520
|
+
if (this.commonsAvailability === 'unknown' || this.commonsAvailability === 'checking') {
|
|
521
|
+
// The eager `start()` probe hasn't resolved yet (or was never run, e.g.
|
|
522
|
+
// `showQr` called without a prior `start()`) — resolve it now rather
|
|
523
|
+
// than skipping the deep link.
|
|
524
|
+
await this.resolveCommonsAvailability();
|
|
525
|
+
}
|
|
526
|
+
if (this.commonsAvailability === 'available') {
|
|
527
|
+
this.openUrl(qrPayload);
|
|
481
528
|
}
|
|
482
529
|
}
|
|
483
530
|
/** Tear down the active sign-in device flow (timers + socket + token) and reset to idle. */
|
|
@@ -489,46 +536,6 @@ class AccountDialogController {
|
|
|
489
536
|
this.setSignIn(IDLE_SIGN_IN);
|
|
490
537
|
}
|
|
491
538
|
}
|
|
492
|
-
/**
|
|
493
|
-
* Build (and, when an `openUrl` handler was supplied, open) the auth.oxy.so
|
|
494
|
-
* password sign-in URL. Password + 2FA are NOT in the SDK — they live at the
|
|
495
|
-
* IdP; this only hands off. Device-first: after login at the IdP the device
|
|
496
|
-
* session converges and the caller is woken via the device socket /
|
|
497
|
-
* `BroadcastChannel`, so the URL only needs to point at the IdP sign-in with
|
|
498
|
-
* the right return.
|
|
499
|
-
*
|
|
500
|
-
* @param params.returnUrl - Where the IdP returns after login. Defaults to the
|
|
501
|
-
* current document URL on web (`globalThis.location.href`); pass explicitly
|
|
502
|
-
* on native (no `location`).
|
|
503
|
-
* @param params.state - Optional opaque state echoed back on return.
|
|
504
|
-
* @returns The absolute auth.oxy.so sign-in URL.
|
|
505
|
-
*/
|
|
506
|
-
async openPasswordAtOxyAuth(params = {}) {
|
|
507
|
-
const base = `https://auth.${this.idpApex}`;
|
|
508
|
-
const url = new URL('/login', base);
|
|
509
|
-
const rawRedirect = params.redirectUri ??
|
|
510
|
-
this.authRedirectUri ??
|
|
511
|
-
params.returnUrl ??
|
|
512
|
-
currentLocationOrigin();
|
|
513
|
-
const redirectUri = rawRedirect ? (0, oauthPkce_1.normalizeOAuthRedirectUri)(rawRedirect) : '';
|
|
514
|
-
if (redirectUri) {
|
|
515
|
-
url.searchParams.set('redirect_uri', redirectUri);
|
|
516
|
-
}
|
|
517
|
-
if (this.clientId) {
|
|
518
|
-
url.searchParams.set('client_id', this.clientId);
|
|
519
|
-
}
|
|
520
|
-
const state = params.state ?? (await (0, oauthPkce_1.generateOAuthState)());
|
|
521
|
-
url.searchParams.set('state', state);
|
|
522
|
-
const { codeChallenge, codeVerifier } = await (0, oauthPkce_1.generatePkcePair)();
|
|
523
|
-
url.searchParams.set('code_challenge', codeChallenge);
|
|
524
|
-
url.searchParams.set('code_challenge_method', 'S256');
|
|
525
|
-
if (!(0, oauthPkce_1.persistOAuthHandshake)(state, codeVerifier)) {
|
|
526
|
-
throw new Error('Could not persist OAuth handshake for password sign-in');
|
|
527
|
-
}
|
|
528
|
-
const href = url.toString();
|
|
529
|
-
this.openUrl?.(href);
|
|
530
|
-
return href;
|
|
531
|
-
}
|
|
532
539
|
// =========================================================================
|
|
533
540
|
// Internal sign-in helpers
|
|
534
541
|
// =========================================================================
|
|
@@ -640,12 +647,21 @@ class AccountDialogController {
|
|
|
640
647
|
}
|
|
641
648
|
/**
|
|
642
649
|
* Register a token-planted session into the device set. Prefers the
|
|
643
|
-
* consumer's
|
|
650
|
+
* consumer's commit funnel (durable persist + hydration); falls back to
|
|
644
651
|
* `SessionClient.registerAndActivate` (registration + activation only).
|
|
652
|
+
*
|
|
653
|
+
* A SWITCH (`opts.fromSwitch`) uses the IN-PLACE `commitSwitchedSession` funnel
|
|
654
|
+
* so it never runs the cross-origin hub-sync redirect; a SIGN-IN uses
|
|
655
|
+
* `commitSession` (which may hub-sync on an official web origin). When the
|
|
656
|
+
* switch funnel is not wired it falls back to the sign-in funnel, then to
|
|
657
|
+
* `registerAndActivate`.
|
|
645
658
|
*/
|
|
646
|
-
async commitAuthorizedSession(session, user) {
|
|
647
|
-
|
|
648
|
-
|
|
659
|
+
async commitAuthorizedSession(session, user, opts) {
|
|
660
|
+
const commit = opts?.fromSwitch
|
|
661
|
+
? this.commitSwitchedSession ?? this.commitSession
|
|
662
|
+
: this.commitSession;
|
|
663
|
+
if (commit) {
|
|
664
|
+
await commit(session);
|
|
649
665
|
}
|
|
650
666
|
else {
|
|
651
667
|
await this.sessionClient.registerAndActivate(user.id);
|
|
@@ -757,6 +773,7 @@ class AccountDialogController {
|
|
|
757
773
|
error: this.error,
|
|
758
774
|
switchingAccountId: this.switchingAccountId,
|
|
759
775
|
signIn: this.signIn,
|
|
776
|
+
commonsAvailability: this.commonsAvailability,
|
|
760
777
|
};
|
|
761
778
|
}
|
|
762
779
|
/** Recompute the snapshot and notify subscribers. */
|
|
@@ -777,11 +794,3 @@ exports.AccountDialogController = AccountDialogController;
|
|
|
777
794
|
function createAccountDialogController(options) {
|
|
778
795
|
return new AccountDialogController(options);
|
|
779
796
|
}
|
|
780
|
-
// ---------------------------------------------------------------------------
|
|
781
|
-
// Local helpers
|
|
782
|
-
// ---------------------------------------------------------------------------
|
|
783
|
-
/** Current document origin on web; empty string where `location` is absent (native/SSR). */
|
|
784
|
-
function currentLocationOrigin() {
|
|
785
|
-
const location = globalThis.location;
|
|
786
|
-
return typeof location?.origin === 'string' ? location.origin : '';
|
|
787
|
-
}
|
|
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
exports.isAllowedDeviceJoinOrigin = void 0;
|
|
8
8
|
exports.buildIdpHubOrigin = buildIdpHubOrigin;
|
|
9
9
|
exports.isIdpHubOrigin = isIdpHubOrigin;
|
|
10
|
+
exports.isLoopbackOrigin = isLoopbackOrigin;
|
|
10
11
|
exports.isOfficialWebOrigin = isOfficialWebOrigin;
|
|
11
12
|
exports.normalizeOfficialReturnOrigin = normalizeOfficialReturnOrigin;
|
|
12
13
|
exports.parseHubSyncReturnUrl = parseHubSyncReturnUrl;
|
|
@@ -46,6 +47,11 @@ function isIdpHubOrigin() {
|
|
|
46
47
|
return false;
|
|
47
48
|
}
|
|
48
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Whether an origin is a loopback / local-dev origin (`localhost`, `127.0.0.1`,
|
|
52
|
+
* or `[::1]` on any port, http or https). Local dev must never be bounced to a
|
|
53
|
+
* hosted IdP for cross-origin session restore.
|
|
54
|
+
*/
|
|
49
55
|
function isLoopbackOrigin(origin) {
|
|
50
56
|
try {
|
|
51
57
|
const parsed = new URL(origin);
|