@oxyhq/core 7.1.1 → 8.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.
Files changed (93) hide show
  1. package/README.md +48 -24
  2. package/dist/cjs/.tsbuildinfo +1 -1
  3. package/dist/cjs/HttpService.js +6 -6
  4. package/dist/cjs/boot/coldBootV2.js +97 -2
  5. package/dist/cjs/boot/deviceBootReturn.js +15 -0
  6. package/dist/cjs/i18n/locales/en-US.json +44 -1
  7. package/dist/cjs/i18n/locales/es-ES.json +44 -1
  8. package/dist/cjs/i18n/locales/locales/en-US.json +45 -2
  9. package/dist/cjs/i18n/locales/locales/es-ES.json +45 -2
  10. package/dist/cjs/index.js +19 -16
  11. package/dist/cjs/mixins/OxyServices.deviceBoot.js +28 -0
  12. package/dist/cjs/server/index.js +1 -7
  13. package/dist/cjs/session/accountDialogController.js +1 -1
  14. package/dist/cjs/session/accountProjection.js +1 -1
  15. package/dist/cjs/session/authStateStore.js +6 -0
  16. package/dist/cjs/session/projectSessionState.js +1 -1
  17. package/dist/cjs/session/refresh.js +9 -0
  18. package/dist/cjs/session/sessionClientHost.js +1 -2
  19. package/dist/cjs/utils/accountUtils.js +1 -1
  20. package/dist/cjs/utils/oauthPkce.js +142 -0
  21. package/dist/cjs/utils/platform.js +1 -1
  22. package/dist/esm/.tsbuildinfo +1 -1
  23. package/dist/esm/HttpService.js +6 -6
  24. package/dist/esm/boot/coldBootV2.js +97 -2
  25. package/dist/esm/boot/deviceBootReturn.js +15 -0
  26. package/dist/esm/i18n/locales/en-US.json +44 -1
  27. package/dist/esm/i18n/locales/es-ES.json +44 -1
  28. package/dist/esm/i18n/locales/locales/en-US.json +45 -2
  29. package/dist/esm/i18n/locales/locales/es-ES.json +45 -2
  30. package/dist/esm/index.js +11 -13
  31. package/dist/esm/mixins/OxyServices.deviceBoot.js +29 -1
  32. package/dist/esm/server/index.js +0 -5
  33. package/dist/esm/session/accountDialogController.js +1 -1
  34. package/dist/esm/session/accountProjection.js +1 -1
  35. package/dist/esm/session/authStateStore.js +6 -0
  36. package/dist/esm/session/projectSessionState.js +1 -1
  37. package/dist/esm/session/refresh.js +9 -0
  38. package/dist/esm/session/sessionClientHost.js +1 -2
  39. package/dist/esm/utils/accountUtils.js +1 -1
  40. package/dist/esm/utils/oauthPkce.js +135 -0
  41. package/dist/esm/utils/platform.js +1 -1
  42. package/dist/types/.tsbuildinfo +1 -1
  43. package/dist/types/HttpService.d.ts +1 -1
  44. package/dist/types/index.d.ts +3 -2
  45. package/dist/types/mixins/OxyServices.accounts.d.ts +13 -3
  46. package/dist/types/mixins/OxyServices.connectedApps.d.ts +4 -0
  47. package/dist/types/mixins/OxyServices.deviceBoot.d.ts +17 -1
  48. package/dist/types/mixins/OxyServices.devices.d.ts +3 -2
  49. package/dist/types/models/interfaces.d.ts +4 -4
  50. package/dist/types/server/index.d.ts +0 -1
  51. package/dist/types/session/accountDialogController.d.ts +1 -1
  52. package/dist/types/session/accountProjection.d.ts +1 -1
  53. package/dist/types/session/authStateStore.d.ts +20 -0
  54. package/dist/types/session/projectSessionState.d.ts +1 -1
  55. package/dist/types/session/refresh.d.ts +4 -8
  56. package/dist/types/session/sessionClientHost.d.ts +1 -2
  57. package/dist/types/utils/accountUtils.d.ts +1 -1
  58. package/dist/types/utils/oauthPkce.d.ts +74 -0
  59. package/dist/types/utils/platform.d.ts +1 -1
  60. package/package.json +3 -3
  61. package/src/HttpService.ts +6 -6
  62. package/src/boot/__tests__/coldBootV2.test.ts +215 -1
  63. package/src/boot/__tests__/deviceBootReturn.test.ts +32 -0
  64. package/src/boot/coldBootV2.ts +117 -2
  65. package/src/boot/deviceBootReturn.ts +15 -0
  66. package/src/i18n/locales/en-US.json +45 -2
  67. package/src/i18n/locales/es-ES.json +45 -2
  68. package/src/index.ts +23 -16
  69. package/src/mixins/OxyServices.accounts.ts +12 -0
  70. package/src/mixins/OxyServices.connectedApps.ts +4 -0
  71. package/src/mixins/OxyServices.deviceBoot.ts +38 -0
  72. package/src/mixins/OxyServices.devices.ts +6 -5
  73. package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +44 -1
  74. package/src/mixins/__tests__/accounts.test.ts +1 -1
  75. package/src/models/interfaces.ts +7 -5
  76. package/src/server/index.ts +0 -6
  77. package/src/session/__tests__/authStateStore.test.ts +27 -0
  78. package/src/session/__tests__/refresh.test.ts +14 -0
  79. package/src/session/accountDialogController.ts +1 -1
  80. package/src/session/accountProjection.ts +1 -1
  81. package/src/session/authStateStore.ts +26 -0
  82. package/src/session/projectSessionState.ts +1 -1
  83. package/src/session/refresh.ts +13 -8
  84. package/src/session/sessionClientHost.ts +1 -2
  85. package/src/utils/__tests__/coldBoot.test.ts +55 -65
  86. package/src/utils/__tests__/oauthPkce.test.ts +154 -0
  87. package/src/utils/accountUtils.ts +1 -1
  88. package/src/utils/oauthPkce.ts +189 -0
  89. package/src/utils/platform.ts +1 -1
  90. package/dist/cjs/utils/ssoBounce.js +0 -24
  91. package/dist/esm/utils/ssoBounce.js +0 -21
  92. package/dist/types/utils/ssoBounce.d.ts +0 -21
  93. package/src/utils/ssoBounce.ts +0 -22
@@ -92,6 +92,15 @@ async function refreshPersistedSession(deps) {
92
92
  if (persisted.deviceToken) {
93
93
  next.deviceToken = persisted.deviceToken;
94
94
  }
95
+ // The refresh response carries no device credentials — carry the persisted
96
+ // deviceId/deviceSecret (phase 2c) forward so a rotation never drops the
97
+ // zero-cookie mint lane (mirrors the deviceToken preservation above).
98
+ if (persisted.deviceId) {
99
+ next.deviceId = persisted.deviceId;
100
+ }
101
+ if (persisted.deviceSecret) {
102
+ next.deviceSecret = persisted.deviceSecret;
103
+ }
95
104
  await store.save(next);
96
105
  return rotated.accessToken;
97
106
  }
@@ -7,8 +7,7 @@ exports.createSessionClientHost = createSessionClientHost;
7
7
  * `SessionClient` is host-agnostic: it only needs a REST + token surface.
8
8
  * `OxyServices` already exposes all of that except `getCurrentAccountId`,
9
9
  * which has no direct equivalent — the adapter holds a mutable ref set by
10
- * the caller (`OxyContext` in `@oxyhq/services`, `WebOxyProvider` in
11
- * `@oxyhq/auth`) via `setCurrentAccountId`.
10
+ * the caller (`OxyContext` in `@oxyhq/services`) via `setCurrentAccountId`.
12
11
  *
13
12
  * Shared here (rather than duplicated per consumer) because it is entirely
14
13
  * platform-agnostic: every method it calls exists identically on
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  /**
3
3
  * Shared account types and pure helper functions.
4
- * Used by both @oxyhq/services (React Native) and @oxyhq/auth (Web) account stores.
4
+ * Used by the @oxyhq/services account stores (Expo/RN and RN-Web).
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.getAccountColor = exports.createQuickAccount = exports.buildAccountsArray = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.formatPublicKeyHandle = void 0;
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ /**
3
+ * OAuth 2.0 Authorization Code + PKCE helpers for "Sign in with Oxy" third-party
4
+ * sign-in.
5
+ *
6
+ * Third-party Relying Parties (SPAs, static sites, and native apps that are NOT
7
+ * Oxy first-party) authenticate through the standard OAuth flow against
8
+ * `auth.oxy.so/authorize` — never FedCM, SSO bounces, or Oxy session cookies.
9
+ * Public clients (no secret) prove possession of the authorization code with
10
+ * PKCE (RFC 7636, S256): the RP generates a random `code_verifier`, sends its
11
+ * `code_challenge = BASE64URL(SHA-256(code_verifier))` on the authorize
12
+ * redirect, and later replays the raw verifier on the token exchange.
13
+ *
14
+ * All cross-platform crypto (random bytes, SHA-256) is delegated to the shared
15
+ * `@oxyhq/protocol` platform loaders — the exact primitives the rest of core's
16
+ * crypto already uses — so these helpers run identically on web, Node, and
17
+ * React Native. No `require()`, so the ESM build stays bundler-clean.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.DEFAULT_OAUTH_SCOPE = exports.OXY_AUTHORIZE_URL = void 0;
21
+ exports.computeCodeChallenge = computeCodeChallenge;
22
+ exports.generatePkcePair = generatePkcePair;
23
+ exports.generateOAuthState = generateOAuthState;
24
+ exports.buildOAuthAuthorizeUrl = buildOAuthAuthorizeUrl;
25
+ const protocol_1 = require("@oxyhq/protocol");
26
+ const loggerUtils_1 = require("./loggerUtils");
27
+ /** The central Oxy IdP authorization endpoint used by default. */
28
+ exports.OXY_AUTHORIZE_URL = 'https://auth.oxy.so/authorize';
29
+ /** Default OAuth scope requested for a "Sign in with Oxy" third-party flow. */
30
+ exports.DEFAULT_OAUTH_SCOPE = 'openid profile';
31
+ /**
32
+ * Number of random bytes behind a PKCE `code_verifier`. 64 bytes → 86 base64url
33
+ * characters, comfortably inside RFC 7636 §4.1's required 43–128 range.
34
+ */
35
+ const PKCE_VERIFIER_BYTES = 64;
36
+ /** Number of random bytes behind an OAuth `state` (CSRF) token. */
37
+ const OAUTH_STATE_BYTES = 32;
38
+ /** RFC 4648 §5 base64url alphabet (URL- and filename-safe, no padding). */
39
+ const BASE64URL_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
40
+ /**
41
+ * Cryptographically-secure random bytes, cross-platform.
42
+ *
43
+ * Mirrors the platform gating the rest of core's crypto already uses:
44
+ * `expo-crypto` on React Native, Node's built-in `crypto` on the server, and
45
+ * the Web Crypto API in the browser (also the fallback if Node's `crypto`
46
+ * fails to load in an unusual bundled-Node environment).
47
+ */
48
+ async function getSecureRandomBytes(byteLength) {
49
+ if ((0, protocol_1.isReactNative)()) {
50
+ const crypto = await (0, protocol_1.loadExpoCrypto)();
51
+ return Uint8Array.from(await crypto.getRandomBytesAsync(byteLength));
52
+ }
53
+ if ((0, protocol_1.isNodeJS)()) {
54
+ try {
55
+ const nodeCrypto = await (0, protocol_1.loadNodeCrypto)();
56
+ return Uint8Array.from(nodeCrypto.randomBytes(byteLength));
57
+ }
58
+ catch (error) {
59
+ loggerUtils_1.logger.warn('[oxy.oauth] Node crypto unavailable for PKCE random bytes, falling back to Web Crypto', { component: 'oauthPkce' }, error);
60
+ }
61
+ }
62
+ const bytes = new Uint8Array(byteLength);
63
+ globalThis.crypto.getRandomValues(bytes);
64
+ return bytes;
65
+ }
66
+ /** Encode raw bytes as unpadded base64url (RFC 4648 §5). */
67
+ function bytesToBase64Url(bytes) {
68
+ let output = '';
69
+ for (let i = 0; i < bytes.length; i += 3) {
70
+ const byte0 = bytes[i];
71
+ const hasByte1 = i + 1 < bytes.length;
72
+ const hasByte2 = i + 2 < bytes.length;
73
+ const byte1 = hasByte1 ? bytes[i + 1] : 0;
74
+ const byte2 = hasByte2 ? bytes[i + 2] : 0;
75
+ output += BASE64URL_ALPHABET[byte0 >> 2];
76
+ output += BASE64URL_ALPHABET[((byte0 & 0x03) << 4) | (byte1 >> 4)];
77
+ if (hasByte1) {
78
+ output += BASE64URL_ALPHABET[((byte1 & 0x0f) << 2) | (byte2 >> 6)];
79
+ }
80
+ if (hasByte2) {
81
+ output += BASE64URL_ALPHABET[byte2 & 0x3f];
82
+ }
83
+ }
84
+ return output;
85
+ }
86
+ /** Decode a lowercase-hex string into its raw bytes. */
87
+ function hexToBytes(hex) {
88
+ const bytes = new Uint8Array(hex.length / 2);
89
+ for (let i = 0; i < bytes.length; i += 1) {
90
+ bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
91
+ }
92
+ return bytes;
93
+ }
94
+ /**
95
+ * Compute the PKCE S256 `code_challenge` for a given verifier:
96
+ * `BASE64URL(SHA-256(ASCII(codeVerifier)))` (RFC 7636 §4.2). The verifier is
97
+ * base64url (ASCII), so its UTF-8 and ASCII byte encodings are identical.
98
+ *
99
+ * Reuses `@oxyhq/protocol`'s cross-platform {@link sha256} (which returns
100
+ * lowercase hex); the digest bytes are recovered and re-encoded as base64url.
101
+ */
102
+ async function computeCodeChallenge(codeVerifier) {
103
+ const digestHex = await (0, protocol_1.sha256)(codeVerifier);
104
+ return bytesToBase64Url(hexToBytes(digestHex));
105
+ }
106
+ /**
107
+ * Generate a fresh PKCE verifier/challenge pair for an OAuth authorization-code
108
+ * flow. The verifier is 64 random bytes as base64url (86 chars, within RFC 7636
109
+ * §4.1's 43–128 range and drawn only from the unreserved set); the challenge is
110
+ * its S256 transform.
111
+ */
112
+ async function generatePkcePair() {
113
+ const codeVerifier = bytesToBase64Url(await getSecureRandomBytes(PKCE_VERIFIER_BYTES));
114
+ const codeChallenge = await computeCodeChallenge(codeVerifier);
115
+ return { codeVerifier, codeChallenge, method: 'S256' };
116
+ }
117
+ /**
118
+ * Generate an opaque, single-use OAuth `state` token (32 random bytes as
119
+ * base64url) for CSRF protection across the authorize redirect.
120
+ */
121
+ async function generateOAuthState() {
122
+ return bytesToBase64Url(await getSecureRandomBytes(OAUTH_STATE_BYTES));
123
+ }
124
+ /**
125
+ * Build the `auth.oxy.so/authorize` redirect URL for an OAuth authorization-code
126
+ * + PKCE (S256) flow. Built via the WHATWG `URL` API so a custom
127
+ * `authorizeBaseUrl` that already carries a query string keeps its existing
128
+ * params (the OAuth params are merged in, not clobbered by a naive `?` concat).
129
+ * All values are percent-encoded by `URL.searchParams`.
130
+ */
131
+ function buildOAuthAuthorizeUrl(params) {
132
+ const { authorizeBaseUrl = exports.OXY_AUTHORIZE_URL, clientId, redirectUri, scope = exports.DEFAULT_OAUTH_SCOPE, state, codeChallenge, } = params;
133
+ const url = new URL(authorizeBaseUrl);
134
+ url.searchParams.set('client_id', clientId);
135
+ url.searchParams.set('redirect_uri', redirectUri);
136
+ url.searchParams.set('response_type', 'code');
137
+ url.searchParams.set('state', state);
138
+ url.searchParams.set('scope', scope);
139
+ url.searchParams.set('code_challenge', codeChallenge);
140
+ url.searchParams.set('code_challenge_method', 'S256');
141
+ return url.toString();
142
+ }
@@ -98,7 +98,7 @@ function setPlatformOS(os) {
98
98
  *
99
99
  * Native defines a global `window` but no `document`, so the DOM probe — not a
100
100
  * bare `window` check — is the reliable discriminator. This is the single
101
- * source of truth consumed by `@oxyhq/services` and `@oxyhq/auth` (both dropped
101
+ * source of truth consumed by `@oxyhq/services` (which dropped
102
102
  * their local copies), so every consumer shares the exact same predicate.
103
103
  *
104
104
  * NOTE: this is a live runtime probe (not the cached `getPlatformOS()` verdict)