@oxyhq/core 7.1.1 → 8.0.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/README.md +48 -24
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/HttpService.js +6 -6
- package/dist/cjs/i18n/locales/en-US.json +44 -1
- package/dist/cjs/i18n/locales/es-ES.json +44 -1
- package/dist/cjs/i18n/locales/locales/en-US.json +45 -2
- package/dist/cjs/i18n/locales/locales/es-ES.json +45 -2
- package/dist/cjs/index.js +19 -16
- package/dist/cjs/server/index.js +1 -7
- package/dist/cjs/session/accountDialogController.js +1 -1
- package/dist/cjs/session/accountProjection.js +1 -1
- package/dist/cjs/session/projectSessionState.js +1 -1
- package/dist/cjs/session/sessionClientHost.js +1 -2
- package/dist/cjs/utils/accountUtils.js +1 -1
- package/dist/cjs/utils/oauthPkce.js +142 -0
- package/dist/cjs/utils/platform.js +1 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +6 -6
- package/dist/esm/i18n/locales/en-US.json +44 -1
- package/dist/esm/i18n/locales/es-ES.json +44 -1
- package/dist/esm/i18n/locales/locales/en-US.json +45 -2
- package/dist/esm/i18n/locales/locales/es-ES.json +45 -2
- package/dist/esm/index.js +11 -13
- package/dist/esm/server/index.js +0 -5
- package/dist/esm/session/accountDialogController.js +1 -1
- package/dist/esm/session/accountProjection.js +1 -1
- package/dist/esm/session/projectSessionState.js +1 -1
- package/dist/esm/session/sessionClientHost.js +1 -2
- package/dist/esm/utils/accountUtils.js +1 -1
- package/dist/esm/utils/oauthPkce.js +135 -0
- package/dist/esm/utils/platform.js +1 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +1 -1
- package/dist/types/index.d.ts +3 -2
- package/dist/types/mixins/OxyServices.accounts.d.ts +13 -3
- package/dist/types/mixins/OxyServices.connectedApps.d.ts +4 -0
- package/dist/types/mixins/OxyServices.devices.d.ts +3 -2
- package/dist/types/models/interfaces.d.ts +4 -4
- package/dist/types/server/index.d.ts +0 -1
- package/dist/types/session/accountDialogController.d.ts +1 -1
- package/dist/types/session/accountProjection.d.ts +1 -1
- package/dist/types/session/projectSessionState.d.ts +1 -1
- package/dist/types/session/refresh.d.ts +4 -8
- package/dist/types/session/sessionClientHost.d.ts +1 -2
- package/dist/types/utils/accountUtils.d.ts +1 -1
- package/dist/types/utils/oauthPkce.d.ts +74 -0
- package/dist/types/utils/platform.d.ts +1 -1
- package/package.json +3 -3
- package/src/HttpService.ts +6 -6
- package/src/i18n/locales/en-US.json +45 -2
- package/src/i18n/locales/es-ES.json +45 -2
- package/src/index.ts +23 -16
- package/src/mixins/OxyServices.accounts.ts +12 -0
- package/src/mixins/OxyServices.connectedApps.ts +4 -0
- package/src/mixins/OxyServices.devices.ts +6 -5
- package/src/mixins/__tests__/accounts.test.ts +1 -1
- package/src/models/interfaces.ts +7 -5
- package/src/server/index.ts +0 -6
- package/src/session/accountDialogController.ts +1 -1
- package/src/session/accountProjection.ts +1 -1
- package/src/session/projectSessionState.ts +1 -1
- package/src/session/refresh.ts +4 -8
- package/src/session/sessionClientHost.ts +1 -2
- package/src/utils/__tests__/coldBoot.test.ts +55 -65
- package/src/utils/__tests__/oauthPkce.test.ts +154 -0
- package/src/utils/accountUtils.ts +1 -1
- package/src/utils/oauthPkce.ts +189 -0
- package/src/utils/platform.ts +1 -1
- package/dist/cjs/utils/ssoBounce.js +0 -24
- package/dist/esm/utils/ssoBounce.js +0 -21
- package/dist/types/utils/ssoBounce.d.ts +0 -21
- package/src/utils/ssoBounce.ts +0 -22
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 Authorization Code + PKCE helpers for "Sign in with Oxy" third-party
|
|
3
|
+
* sign-in.
|
|
4
|
+
*
|
|
5
|
+
* Third-party Relying Parties (SPAs, static sites, and native apps that are NOT
|
|
6
|
+
* Oxy first-party) authenticate through the standard OAuth flow against
|
|
7
|
+
* `auth.oxy.so/authorize` — never FedCM, SSO bounces, or Oxy session cookies.
|
|
8
|
+
* Public clients (no secret) prove possession of the authorization code with
|
|
9
|
+
* PKCE (RFC 7636, S256): the RP generates a random `code_verifier`, sends its
|
|
10
|
+
* `code_challenge = BASE64URL(SHA-256(code_verifier))` on the authorize
|
|
11
|
+
* redirect, and later replays the raw verifier on the token exchange.
|
|
12
|
+
*
|
|
13
|
+
* All cross-platform crypto (random bytes, SHA-256) is delegated to the shared
|
|
14
|
+
* `@oxyhq/protocol` platform loaders — the exact primitives the rest of core's
|
|
15
|
+
* crypto already uses — so these helpers run identically on web, Node, and
|
|
16
|
+
* React Native. No `require()`, so the ESM build stays bundler-clean.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { isNodeJS, isReactNative, loadExpoCrypto, loadNodeCrypto, sha256 } from '@oxyhq/protocol';
|
|
20
|
+
import { logger } from './loggerUtils';
|
|
21
|
+
|
|
22
|
+
/** The central Oxy IdP authorization endpoint used by default. */
|
|
23
|
+
export const OXY_AUTHORIZE_URL = 'https://auth.oxy.so/authorize';
|
|
24
|
+
|
|
25
|
+
/** Default OAuth scope requested for a "Sign in with Oxy" third-party flow. */
|
|
26
|
+
export const DEFAULT_OAUTH_SCOPE = 'openid profile';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Number of random bytes behind a PKCE `code_verifier`. 64 bytes → 86 base64url
|
|
30
|
+
* characters, comfortably inside RFC 7636 §4.1's required 43–128 range.
|
|
31
|
+
*/
|
|
32
|
+
const PKCE_VERIFIER_BYTES = 64;
|
|
33
|
+
|
|
34
|
+
/** Number of random bytes behind an OAuth `state` (CSRF) token. */
|
|
35
|
+
const OAUTH_STATE_BYTES = 32;
|
|
36
|
+
|
|
37
|
+
/** RFC 4648 §5 base64url alphabet (URL- and filename-safe, no padding). */
|
|
38
|
+
const BASE64URL_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
39
|
+
|
|
40
|
+
/** A generated PKCE verifier/challenge pair. */
|
|
41
|
+
export interface PkcePair {
|
|
42
|
+
/** The high-entropy secret replayed on the token exchange (kept client-side). */
|
|
43
|
+
codeVerifier: string;
|
|
44
|
+
/** `BASE64URL(SHA-256(codeVerifier))` — sent on the authorize redirect. */
|
|
45
|
+
codeChallenge: string;
|
|
46
|
+
/** The PKCE transformation method. Always `S256`. */
|
|
47
|
+
method: 'S256';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Parameters for {@link buildOAuthAuthorizeUrl}. */
|
|
51
|
+
export interface BuildOAuthAuthorizeUrlParams {
|
|
52
|
+
/** Authorize endpoint; defaults to {@link OXY_AUTHORIZE_URL}. */
|
|
53
|
+
authorizeBaseUrl?: string;
|
|
54
|
+
/** The registered `ApplicationCredential` public key (`oxy_dk_…`). */
|
|
55
|
+
clientId: string;
|
|
56
|
+
/** Exact registered redirect URI to return the authorization code to. */
|
|
57
|
+
redirectUri: string;
|
|
58
|
+
/** Requested scope; defaults to {@link DEFAULT_OAUTH_SCOPE}. */
|
|
59
|
+
scope?: string;
|
|
60
|
+
/** Opaque CSRF token from {@link generateOAuthState}. */
|
|
61
|
+
state: string;
|
|
62
|
+
/** The PKCE `codeChallenge` from {@link generatePkcePair}. */
|
|
63
|
+
codeChallenge: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Cryptographically-secure random bytes, cross-platform.
|
|
68
|
+
*
|
|
69
|
+
* Mirrors the platform gating the rest of core's crypto already uses:
|
|
70
|
+
* `expo-crypto` on React Native, Node's built-in `crypto` on the server, and
|
|
71
|
+
* the Web Crypto API in the browser (also the fallback if Node's `crypto`
|
|
72
|
+
* fails to load in an unusual bundled-Node environment).
|
|
73
|
+
*/
|
|
74
|
+
async function getSecureRandomBytes(byteLength: number): Promise<Uint8Array> {
|
|
75
|
+
if (isReactNative()) {
|
|
76
|
+
const crypto = await loadExpoCrypto();
|
|
77
|
+
return Uint8Array.from(await crypto.getRandomBytesAsync(byteLength));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (isNodeJS()) {
|
|
81
|
+
try {
|
|
82
|
+
const nodeCrypto = await loadNodeCrypto();
|
|
83
|
+
return Uint8Array.from(nodeCrypto.randomBytes(byteLength));
|
|
84
|
+
} catch (error) {
|
|
85
|
+
logger.warn(
|
|
86
|
+
'[oxy.oauth] Node crypto unavailable for PKCE random bytes, falling back to Web Crypto',
|
|
87
|
+
{ component: 'oauthPkce' },
|
|
88
|
+
error,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const bytes = new Uint8Array(byteLength);
|
|
94
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
95
|
+
return bytes;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Encode raw bytes as unpadded base64url (RFC 4648 §5). */
|
|
99
|
+
function bytesToBase64Url(bytes: Uint8Array): string {
|
|
100
|
+
let output = '';
|
|
101
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
102
|
+
const byte0 = bytes[i];
|
|
103
|
+
const hasByte1 = i + 1 < bytes.length;
|
|
104
|
+
const hasByte2 = i + 2 < bytes.length;
|
|
105
|
+
const byte1 = hasByte1 ? bytes[i + 1] : 0;
|
|
106
|
+
const byte2 = hasByte2 ? bytes[i + 2] : 0;
|
|
107
|
+
|
|
108
|
+
output += BASE64URL_ALPHABET[byte0 >> 2];
|
|
109
|
+
output += BASE64URL_ALPHABET[((byte0 & 0x03) << 4) | (byte1 >> 4)];
|
|
110
|
+
if (hasByte1) {
|
|
111
|
+
output += BASE64URL_ALPHABET[((byte1 & 0x0f) << 2) | (byte2 >> 6)];
|
|
112
|
+
}
|
|
113
|
+
if (hasByte2) {
|
|
114
|
+
output += BASE64URL_ALPHABET[byte2 & 0x3f];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return output;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Decode a lowercase-hex string into its raw bytes. */
|
|
121
|
+
function hexToBytes(hex: string): Uint8Array {
|
|
122
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
123
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
124
|
+
bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
125
|
+
}
|
|
126
|
+
return bytes;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Compute the PKCE S256 `code_challenge` for a given verifier:
|
|
131
|
+
* `BASE64URL(SHA-256(ASCII(codeVerifier)))` (RFC 7636 §4.2). The verifier is
|
|
132
|
+
* base64url (ASCII), so its UTF-8 and ASCII byte encodings are identical.
|
|
133
|
+
*
|
|
134
|
+
* Reuses `@oxyhq/protocol`'s cross-platform {@link sha256} (which returns
|
|
135
|
+
* lowercase hex); the digest bytes are recovered and re-encoded as base64url.
|
|
136
|
+
*/
|
|
137
|
+
export async function computeCodeChallenge(codeVerifier: string): Promise<string> {
|
|
138
|
+
const digestHex = await sha256(codeVerifier);
|
|
139
|
+
return bytesToBase64Url(hexToBytes(digestHex));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Generate a fresh PKCE verifier/challenge pair for an OAuth authorization-code
|
|
144
|
+
* flow. The verifier is 64 random bytes as base64url (86 chars, within RFC 7636
|
|
145
|
+
* §4.1's 43–128 range and drawn only from the unreserved set); the challenge is
|
|
146
|
+
* its S256 transform.
|
|
147
|
+
*/
|
|
148
|
+
export async function generatePkcePair(): Promise<PkcePair> {
|
|
149
|
+
const codeVerifier = bytesToBase64Url(await getSecureRandomBytes(PKCE_VERIFIER_BYTES));
|
|
150
|
+
const codeChallenge = await computeCodeChallenge(codeVerifier);
|
|
151
|
+
return { codeVerifier, codeChallenge, method: 'S256' };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Generate an opaque, single-use OAuth `state` token (32 random bytes as
|
|
156
|
+
* base64url) for CSRF protection across the authorize redirect.
|
|
157
|
+
*/
|
|
158
|
+
export async function generateOAuthState(): Promise<string> {
|
|
159
|
+
return bytesToBase64Url(await getSecureRandomBytes(OAUTH_STATE_BYTES));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Build the `auth.oxy.so/authorize` redirect URL for an OAuth authorization-code
|
|
164
|
+
* + PKCE (S256) flow. Built via the WHATWG `URL` API so a custom
|
|
165
|
+
* `authorizeBaseUrl` that already carries a query string keeps its existing
|
|
166
|
+
* params (the OAuth params are merged in, not clobbered by a naive `?` concat).
|
|
167
|
+
* All values are percent-encoded by `URL.searchParams`.
|
|
168
|
+
*/
|
|
169
|
+
export function buildOAuthAuthorizeUrl(params: BuildOAuthAuthorizeUrlParams): string {
|
|
170
|
+
const {
|
|
171
|
+
authorizeBaseUrl = OXY_AUTHORIZE_URL,
|
|
172
|
+
clientId,
|
|
173
|
+
redirectUri,
|
|
174
|
+
scope = DEFAULT_OAUTH_SCOPE,
|
|
175
|
+
state,
|
|
176
|
+
codeChallenge,
|
|
177
|
+
} = params;
|
|
178
|
+
|
|
179
|
+
const url = new URL(authorizeBaseUrl);
|
|
180
|
+
url.searchParams.set('client_id', clientId);
|
|
181
|
+
url.searchParams.set('redirect_uri', redirectUri);
|
|
182
|
+
url.searchParams.set('response_type', 'code');
|
|
183
|
+
url.searchParams.set('state', state);
|
|
184
|
+
url.searchParams.set('scope', scope);
|
|
185
|
+
url.searchParams.set('code_challenge', codeChallenge);
|
|
186
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
187
|
+
|
|
188
|
+
return url.toString();
|
|
189
|
+
}
|
package/src/utils/platform.ts
CHANGED
|
@@ -104,7 +104,7 @@ export function setPlatformOS(os: PlatformOS): void {
|
|
|
104
104
|
*
|
|
105
105
|
* Native defines a global `window` but no `document`, so the DOM probe — not a
|
|
106
106
|
* bare `window` check — is the reliable discriminator. This is the single
|
|
107
|
-
* source of truth consumed by `@oxyhq/services`
|
|
107
|
+
* source of truth consumed by `@oxyhq/services` (which dropped
|
|
108
108
|
* their local copies), so every consumer shares the exact same predicate.
|
|
109
109
|
*
|
|
110
110
|
* NOTE: this is a live runtime probe (not the cached `getPlatformOS()` verdict)
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* SSO callback path constant.
|
|
4
|
-
*
|
|
5
|
-
* The client SSO-bounce machinery (per-origin sessionStorage keys, the bounce
|
|
6
|
-
* URL builder, the `guardActive` / `allowSsoBounce` / `getSsoCallbackBootstrapScript`
|
|
7
|
-
* predicates, …) was removed in the device-first cutover — RP apps no longer
|
|
8
|
-
* bounce through `auth.oxy.so`. The one survivor is this path constant, still
|
|
9
|
-
* referenced by the api SSO controller and the IdP (both lista B, gated on the
|
|
10
|
-
* ecosystem bump). `@oxyhq/core/server` re-exports it for the api.
|
|
11
|
-
*
|
|
12
|
-
* LEGACY(old-sdk): `SSO_CALLBACK_PATH` survives ONLY for the lista-B api/IdP SSO
|
|
13
|
-
* surface. Deletable once Homiio/Allo/Alia/Syra are bumped off the old SDK AND
|
|
14
|
-
* CloudWatch `/oxy/ecs` shows the `/sso*` + `/fedcm/*` routes quiet — the
|
|
15
|
-
* F-final sweep should remove this file then.
|
|
16
|
-
*/
|
|
17
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.SSO_CALLBACK_PATH = void 0;
|
|
19
|
-
/**
|
|
20
|
-
* The RP callback path the central IdP redirects back to after a legacy SSO
|
|
21
|
-
* bounce. Kept as the single source of truth so the api/IdP never hardcode the
|
|
22
|
-
* literal.
|
|
23
|
-
*/
|
|
24
|
-
exports.SSO_CALLBACK_PATH = '/__oxy/sso-callback';
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SSO callback path constant.
|
|
3
|
-
*
|
|
4
|
-
* The client SSO-bounce machinery (per-origin sessionStorage keys, the bounce
|
|
5
|
-
* URL builder, the `guardActive` / `allowSsoBounce` / `getSsoCallbackBootstrapScript`
|
|
6
|
-
* predicates, …) was removed in the device-first cutover — RP apps no longer
|
|
7
|
-
* bounce through `auth.oxy.so`. The one survivor is this path constant, still
|
|
8
|
-
* referenced by the api SSO controller and the IdP (both lista B, gated on the
|
|
9
|
-
* ecosystem bump). `@oxyhq/core/server` re-exports it for the api.
|
|
10
|
-
*
|
|
11
|
-
* LEGACY(old-sdk): `SSO_CALLBACK_PATH` survives ONLY for the lista-B api/IdP SSO
|
|
12
|
-
* surface. Deletable once Homiio/Allo/Alia/Syra are bumped off the old SDK AND
|
|
13
|
-
* CloudWatch `/oxy/ecs` shows the `/sso*` + `/fedcm/*` routes quiet — the
|
|
14
|
-
* F-final sweep should remove this file then.
|
|
15
|
-
*/
|
|
16
|
-
/**
|
|
17
|
-
* The RP callback path the central IdP redirects back to after a legacy SSO
|
|
18
|
-
* bounce. Kept as the single source of truth so the api/IdP never hardcode the
|
|
19
|
-
* literal.
|
|
20
|
-
*/
|
|
21
|
-
export const SSO_CALLBACK_PATH = '/__oxy/sso-callback';
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SSO callback path constant.
|
|
3
|
-
*
|
|
4
|
-
* The client SSO-bounce machinery (per-origin sessionStorage keys, the bounce
|
|
5
|
-
* URL builder, the `guardActive` / `allowSsoBounce` / `getSsoCallbackBootstrapScript`
|
|
6
|
-
* predicates, …) was removed in the device-first cutover — RP apps no longer
|
|
7
|
-
* bounce through `auth.oxy.so`. The one survivor is this path constant, still
|
|
8
|
-
* referenced by the api SSO controller and the IdP (both lista B, gated on the
|
|
9
|
-
* ecosystem bump). `@oxyhq/core/server` re-exports it for the api.
|
|
10
|
-
*
|
|
11
|
-
* LEGACY(old-sdk): `SSO_CALLBACK_PATH` survives ONLY for the lista-B api/IdP SSO
|
|
12
|
-
* surface. Deletable once Homiio/Allo/Alia/Syra are bumped off the old SDK AND
|
|
13
|
-
* CloudWatch `/oxy/ecs` shows the `/sso*` + `/fedcm/*` routes quiet — the
|
|
14
|
-
* F-final sweep should remove this file then.
|
|
15
|
-
*/
|
|
16
|
-
/**
|
|
17
|
-
* The RP callback path the central IdP redirects back to after a legacy SSO
|
|
18
|
-
* bounce. Kept as the single source of truth so the api/IdP never hardcode the
|
|
19
|
-
* literal.
|
|
20
|
-
*/
|
|
21
|
-
export declare const SSO_CALLBACK_PATH = "/__oxy/sso-callback";
|
package/src/utils/ssoBounce.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SSO callback path constant.
|
|
3
|
-
*
|
|
4
|
-
* The client SSO-bounce machinery (per-origin sessionStorage keys, the bounce
|
|
5
|
-
* URL builder, the `guardActive` / `allowSsoBounce` / `getSsoCallbackBootstrapScript`
|
|
6
|
-
* predicates, …) was removed in the device-first cutover — RP apps no longer
|
|
7
|
-
* bounce through `auth.oxy.so`. The one survivor is this path constant, still
|
|
8
|
-
* referenced by the api SSO controller and the IdP (both lista B, gated on the
|
|
9
|
-
* ecosystem bump). `@oxyhq/core/server` re-exports it for the api.
|
|
10
|
-
*
|
|
11
|
-
* LEGACY(old-sdk): `SSO_CALLBACK_PATH` survives ONLY for the lista-B api/IdP SSO
|
|
12
|
-
* surface. Deletable once Homiio/Allo/Alia/Syra are bumped off the old SDK AND
|
|
13
|
-
* CloudWatch `/oxy/ecs` shows the `/sso*` + `/fedcm/*` routes quiet — the
|
|
14
|
-
* F-final sweep should remove this file then.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* The RP callback path the central IdP redirects back to after a legacy SSO
|
|
19
|
-
* bounce. Kept as the single source of truth so the api/IdP never hardcode the
|
|
20
|
-
* literal.
|
|
21
|
-
*/
|
|
22
|
-
export const SSO_CALLBACK_PATH = '/__oxy/sso-callback';
|