@oxyhq/core 11.0.0 → 11.0.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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/index.js +7 -2
- package/dist/cjs/mixins/OxyServices.auth.js +102 -0
- package/dist/cjs/utils/webauthnOrigin.js +51 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/index.js +4 -0
- package/dist/esm/mixins/OxyServices.auth.js +102 -0
- package/dist/esm/utils/webauthnOrigin.js +48 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/mixins/OxyServices.auth.d.ts +58 -0
- package/dist/types/utils/webauthnOrigin.d.ts +32 -0
- package/package.json +2 -2
- package/src/index.ts +5 -0
- package/src/mixins/OxyServices.auth.ts +133 -0
- package/src/mixins/__tests__/webauthnAuth.test.ts +206 -0
- package/src/utils/__tests__/webauthnOrigin.test.ts +83 -0
- package/src/utils/webauthnOrigin.ts +52 -0
package/dist/cjs/index.js
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
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
|
-
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.isIdpHubOrigin = exports.buildHubSyncUrl = void 0;
|
|
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;
|
|
25
25
|
// Ensure crypto polyfills are loaded before anything else
|
|
26
26
|
require("./crypto/polyfill");
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
@@ -270,6 +270,11 @@ var registrableApex_1 = require("./utils/registrableApex");
|
|
|
270
270
|
Object.defineProperty(exports, "registrableApex", { enumerable: true, get: function () { return registrableApex_1.registrableApex; } });
|
|
271
271
|
var authWebUrl_1 = require("./utils/authWebUrl");
|
|
272
272
|
Object.defineProperty(exports, "CENTRAL_IDP_APEX", { enumerable: true, get: function () { return authWebUrl_1.CENTRAL_IDP_APEX; } });
|
|
273
|
+
// WebAuthn relying-party origin guard (client side). Mirrors the server's
|
|
274
|
+
// `isOxyApexOrigin` so consumers can decide whether to offer passkey UI on the
|
|
275
|
+
// current page (first-party Oxy origin / loopback only).
|
|
276
|
+
var webauthnOrigin_1 = require("./utils/webauthnOrigin");
|
|
277
|
+
Object.defineProperty(exports, "isOxyRpOrigin", { enumerable: true, get: function () { return webauthnOrigin_1.isOxyRpOrigin; } });
|
|
273
278
|
var coldBoot_1 = require("./utils/coldBoot");
|
|
274
279
|
Object.defineProperty(exports, "runColdBoot", { enumerable: true, get: function () { return coldBoot_1.runColdBoot; } });
|
|
275
280
|
// ---------------------------------------------------------------------------
|
|
@@ -852,6 +852,108 @@ function OxyServicesAuthMixin(Base) {
|
|
|
852
852
|
throw this.handleError(error);
|
|
853
853
|
}
|
|
854
854
|
}
|
|
855
|
+
/**
|
|
856
|
+
* Begin a WebAuthn / passkey REGISTRATION ceremony. Requests the
|
|
857
|
+
* `PublicKeyCredentialCreationOptions` the browser's `navigator.credentials
|
|
858
|
+
* .create()` (or `@simplewebauthn/browser`'s `startRegistration`) needs.
|
|
859
|
+
*
|
|
860
|
+
* With a bearer token planted this links a passkey to the signed-in account
|
|
861
|
+
* (`username` ignored); without one it is a prospective signup and `username`
|
|
862
|
+
* is the desired handle. The returned options are OPAQUE — Oxy does not own
|
|
863
|
+
* their shape (the browser / `@simplewebauthn` does), so they pass through
|
|
864
|
+
* as `unknown` for the caller to hand straight to the ceremony.
|
|
865
|
+
*/
|
|
866
|
+
async webauthnRegisterOptions(username) {
|
|
867
|
+
try {
|
|
868
|
+
return await this.makeRequest('POST', '/auth/webauthn/register/options', { ...(username !== undefined ? { username } : {}) }, { cache: false });
|
|
869
|
+
}
|
|
870
|
+
catch (error) {
|
|
871
|
+
throw this.handleError(error);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Finish a WebAuthn / passkey REGISTRATION ceremony. Forwards the opaque
|
|
876
|
+
* browser `RegistrationResponseJSON` (`response`) alongside the Oxy envelope
|
|
877
|
+
* (desired `username` for signup + the device-session naming fields).
|
|
878
|
+
*
|
|
879
|
+
* Two server branches, disambiguated by the response shape:
|
|
880
|
+
* - **Signup** (no bearer): the account is created and a session minted —
|
|
881
|
+
* the response carries `sessionId`, is the SAME {@link LoginResult}
|
|
882
|
+
* contract as `POST /auth/verify`, and its access token is planted here.
|
|
883
|
+
* - **Link** (bearer present): the passkey is attached to the signed-in
|
|
884
|
+
* account and the server returns `{ success, message }` with no session,
|
|
885
|
+
* which is returned verbatim (no token planting).
|
|
886
|
+
*/
|
|
887
|
+
async webauthnRegisterVerify(response, envelope = {}) {
|
|
888
|
+
try {
|
|
889
|
+
const res = await this.makeRequest('POST', '/auth/webauthn/register/verify', { response, ...envelope }, { cache: false });
|
|
890
|
+
if (res && typeof res === 'object') {
|
|
891
|
+
const record = res;
|
|
892
|
+
// Signup branch: mints a session (LoginSessionResult, carries
|
|
893
|
+
// `sessionId`). Parse against the login contract and plant the token.
|
|
894
|
+
if ('sessionId' in record) {
|
|
895
|
+
const parsed = (0, contracts_1.safeParseContract)(contracts_1.loginResultSchema, record);
|
|
896
|
+
if (!parsed) {
|
|
897
|
+
throw new Error('auth/webauthn/register/verify returned an unexpected response shape');
|
|
898
|
+
}
|
|
899
|
+
if (!('twoFactorRequired' in parsed) && parsed.accessToken) {
|
|
900
|
+
this.setTokens(parsed.accessToken);
|
|
901
|
+
}
|
|
902
|
+
return parsed;
|
|
903
|
+
}
|
|
904
|
+
// Link branch: passkey attached to the signed-in account, no session.
|
|
905
|
+
if (record.success === true && typeof record.message === 'string') {
|
|
906
|
+
return { success: true, message: record.message };
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
throw new Error('auth/webauthn/register/verify returned an unexpected response shape');
|
|
910
|
+
}
|
|
911
|
+
catch (error) {
|
|
912
|
+
throw this.handleError(error);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Begin a WebAuthn / passkey AUTHENTICATION ceremony. Requests the
|
|
917
|
+
* `PublicKeyCredentialRequestOptions` the browser's `navigator.credentials
|
|
918
|
+
* .get()` (or `@simplewebauthn/browser`'s `startAuthentication`) needs.
|
|
919
|
+
*
|
|
920
|
+
* When `username` is present the server scopes `allowCredentials` to that
|
|
921
|
+
* user's passkeys (username-first); when omitted it returns an empty
|
|
922
|
+
* allow-list for the usernameless / discoverable-credential flow. The
|
|
923
|
+
* returned options are OPAQUE and pass through as `unknown`.
|
|
924
|
+
*/
|
|
925
|
+
async webauthnLoginOptions(username) {
|
|
926
|
+
try {
|
|
927
|
+
return await this.makeRequest('POST', '/auth/webauthn/login/options', { ...(username !== undefined ? { username } : {}) }, { cache: false });
|
|
928
|
+
}
|
|
929
|
+
catch (error) {
|
|
930
|
+
throw this.handleError(error);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Finish a WebAuthn / passkey AUTHENTICATION ceremony. Forwards the opaque
|
|
935
|
+
* browser `AuthenticationResponseJSON` (`response`) alongside the
|
|
936
|
+
* device-session envelope. Resolves to the SAME {@link LoginResult} contract
|
|
937
|
+
* as `POST /auth/verify`; on the session arm the access token is planted
|
|
938
|
+
* immediately (mirroring {@link passwordSignIn}), and the response's
|
|
939
|
+
* `deviceId` + `deviceSecret` are the zero-cookie restore credential.
|
|
940
|
+
*/
|
|
941
|
+
async webauthnLoginVerify(response, envelope = {}) {
|
|
942
|
+
try {
|
|
943
|
+
const res = await this.makeRequest('POST', '/auth/webauthn/login/verify', { response, ...envelope }, { cache: false });
|
|
944
|
+
const parsed = (0, contracts_1.safeParseContract)(contracts_1.loginResultSchema, res);
|
|
945
|
+
if (!parsed) {
|
|
946
|
+
throw new Error('auth/webauthn/login/verify returned an unexpected response shape');
|
|
947
|
+
}
|
|
948
|
+
if (!('twoFactorRequired' in parsed) && parsed.accessToken) {
|
|
949
|
+
this.setTokens(parsed.accessToken);
|
|
950
|
+
}
|
|
951
|
+
return parsed;
|
|
952
|
+
}
|
|
953
|
+
catch (error) {
|
|
954
|
+
throw this.handleError(error);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
855
957
|
/**
|
|
856
958
|
* Exchange an OAuth authorization code (returned to the RP redirect URI
|
|
857
959
|
* after password sign-in at auth.oxy.so) for a device-first session.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* WebAuthn relying-party origin guard (client side).
|
|
4
|
+
*
|
|
5
|
+
* The passkey ceremonies (`OxyServices.webauthn*`) are only meaningful when the
|
|
6
|
+
* page is served from a first-party Oxy web origin: a credential minted with
|
|
7
|
+
* `WEBAUTHN_RP_ID=oxy.so` can only be created/asserted from `oxy.so`, one of its
|
|
8
|
+
* subdomains, or a loopback dev server. This is the browser-side mirror of the
|
|
9
|
+
* server's `isOxyApexOrigin` (`packages/api/src/utils/origin.ts`), which forms
|
|
10
|
+
* the server's `expectedOrigin` allow-set — consumers use it to decide whether to
|
|
11
|
+
* even offer the passkey UI on the current page.
|
|
12
|
+
*
|
|
13
|
+
* It reads `globalThis.location` directly (no argument) because that is the only
|
|
14
|
+
* origin the browser will let a WebAuthn ceremony run against. On native / SSR /
|
|
15
|
+
* any environment without a DOM `location`, it returns `false` (there is no
|
|
16
|
+
* relying-party origin, so passkeys are not applicable).
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.isOxyRpOrigin = isOxyRpOrigin;
|
|
20
|
+
/**
|
|
21
|
+
* True iff the current page's host is a first-party Oxy relying-party origin:
|
|
22
|
+
* `oxy.so`, any `*.oxy.so` subdomain, or a loopback dev host
|
|
23
|
+
* (`localhost` / `127.0.0.1` / `[::1]`).
|
|
24
|
+
*
|
|
25
|
+
* Fails closed: no `location` (native/SSR), a non-string/empty hostname, or a
|
|
26
|
+
* host that merely ends in the literal `oxy.so` without the dot boundary
|
|
27
|
+
* (`evil-oxy.so`, `oxy.so.evil.com`) all return `false`.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* isOxyRpOrigin() // true on https://accounts.oxy.so
|
|
31
|
+
* isOxyRpOrigin() // true on http://localhost:8081
|
|
32
|
+
* isOxyRpOrigin() // false on https://evil.com
|
|
33
|
+
* isOxyRpOrigin() // false in a React Native / SSR context (no location)
|
|
34
|
+
*/
|
|
35
|
+
function isOxyRpOrigin() {
|
|
36
|
+
// `globalThis.location` is typed `Location` by the DOM lib, but is genuinely
|
|
37
|
+
// `undefined` on native/SSR — widen to a nullable local so the guard is a real
|
|
38
|
+
// runtime check, not a type-level no-op.
|
|
39
|
+
const location = globalThis.location;
|
|
40
|
+
if (!location || typeof location.hostname !== 'string') {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const hostname = location.hostname.toLowerCase();
|
|
44
|
+
if (hostname.length === 0) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (hostname === 'oxy.so' || hostname.endsWith('.oxy.so')) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
|
|
51
|
+
}
|