@oxyhq/core 12.1.1 → 12.2.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/i18n/locales/ar-SA.json +4 -1
- package/dist/cjs/i18n/locales/ca-ES.json +4 -1
- package/dist/cjs/i18n/locales/de-DE.json +4 -1
- package/dist/cjs/i18n/locales/en-US.json +4 -1
- package/dist/cjs/i18n/locales/es-ES.json +4 -1
- package/dist/cjs/i18n/locales/fr-FR.json +4 -1
- package/dist/cjs/i18n/locales/it-IT.json +4 -1
- package/dist/cjs/i18n/locales/ja-JP.json +4 -1
- package/dist/cjs/i18n/locales/ko-KR.json +4 -1
- package/dist/cjs/i18n/locales/locales/ar-SA.json +4 -1
- package/dist/cjs/i18n/locales/locales/ca-ES.json +4 -1
- package/dist/cjs/i18n/locales/locales/de-DE.json +4 -1
- package/dist/cjs/i18n/locales/locales/en-US.json +4 -1
- package/dist/cjs/i18n/locales/locales/es-ES.json +4 -1
- package/dist/cjs/i18n/locales/locales/fr-FR.json +4 -1
- package/dist/cjs/i18n/locales/locales/it-IT.json +4 -1
- package/dist/cjs/i18n/locales/locales/ja-JP.json +4 -1
- package/dist/cjs/i18n/locales/locales/ko-KR.json +4 -1
- package/dist/cjs/i18n/locales/locales/pt-PT.json +4 -1
- package/dist/cjs/i18n/locales/locales/zh-CN.json +4 -1
- package/dist/cjs/i18n/locales/pt-PT.json +4 -1
- package/dist/cjs/i18n/locales/zh-CN.json +4 -1
- package/dist/cjs/session/accountDialogController.js +112 -2
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/i18n/locales/ar-SA.json +4 -1
- package/dist/esm/i18n/locales/ca-ES.json +4 -1
- package/dist/esm/i18n/locales/de-DE.json +4 -1
- package/dist/esm/i18n/locales/en-US.json +4 -1
- package/dist/esm/i18n/locales/es-ES.json +4 -1
- package/dist/esm/i18n/locales/fr-FR.json +4 -1
- package/dist/esm/i18n/locales/it-IT.json +4 -1
- package/dist/esm/i18n/locales/ja-JP.json +4 -1
- package/dist/esm/i18n/locales/ko-KR.json +4 -1
- package/dist/esm/i18n/locales/locales/ar-SA.json +4 -1
- package/dist/esm/i18n/locales/locales/ca-ES.json +4 -1
- package/dist/esm/i18n/locales/locales/de-DE.json +4 -1
- package/dist/esm/i18n/locales/locales/en-US.json +4 -1
- package/dist/esm/i18n/locales/locales/es-ES.json +4 -1
- package/dist/esm/i18n/locales/locales/fr-FR.json +4 -1
- package/dist/esm/i18n/locales/locales/it-IT.json +4 -1
- package/dist/esm/i18n/locales/locales/ja-JP.json +4 -1
- package/dist/esm/i18n/locales/locales/ko-KR.json +4 -1
- package/dist/esm/i18n/locales/locales/pt-PT.json +4 -1
- package/dist/esm/i18n/locales/locales/zh-CN.json +4 -1
- package/dist/esm/i18n/locales/pt-PT.json +4 -1
- package/dist/esm/i18n/locales/zh-CN.json +4 -1
- package/dist/esm/session/accountDialogController.js +112 -2
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/session/accountDialogController.d.ts +81 -1
- package/package.json +4 -4
- package/src/i18n/locales/ar-SA.json +4 -1
- package/src/i18n/locales/ca-ES.json +4 -1
- package/src/i18n/locales/de-DE.json +4 -1
- package/src/i18n/locales/en-US.json +4 -1
- package/src/i18n/locales/es-ES.json +4 -1
- package/src/i18n/locales/fr-FR.json +4 -1
- package/src/i18n/locales/it-IT.json +4 -1
- package/src/i18n/locales/ja-JP.json +4 -1
- package/src/i18n/locales/ko-KR.json +4 -1
- package/src/i18n/locales/pt-PT.json +4 -1
- package/src/i18n/locales/zh-CN.json +4 -1
- package/src/index.ts +1 -0
- package/src/session/__tests__/accountDialogController.test.ts +133 -1
- package/src/session/accountDialogController.ts +145 -2
package/dist/types/index.d.ts
CHANGED
|
@@ -99,7 +99,7 @@ export { deviceStateToClientSessions, activeSessionIdOf, activeUserOf, accountId
|
|
|
99
99
|
export { projectSwitchableAccounts, switchableAccountIds, } from './session/accountProjection';
|
|
100
100
|
export type { SwitchableAccount, SwitchableAccountUser, ProjectSwitchableAccountsInput, } from './session/accountProjection';
|
|
101
101
|
export { AccountDialogController, createAccountDialogController, } from './session/accountDialogController';
|
|
102
|
-
export type { AccountDialogControllerOptions, AccountDialogSnapshot, AccountDialogView, CommonsAvailability, SignInFlowPhase, SignInFlowState, } from './session/accountDialogController';
|
|
102
|
+
export type { AccountDialogControllerOptions, AccountDialogSnapshot, AccountDialogView, CommonsAvailability, PopupWindowHandle, SignInFlowPhase, SignInFlowState, } from './session/accountDialogController';
|
|
103
103
|
export { createWebAuthStateStore, createNativeAuthStateStore, createMemoryAuthStateStore, AUTH_STATE_STORAGE_KEY, } from './session/authStateStore';
|
|
104
104
|
export type { PersistedAuthState, AuthStateStore, NativeKeyValueStorage, } from './session/authStateStore';
|
|
105
105
|
export { refreshPersistedSession, refreshDeviceSecretArm, createAuthRefreshHandler, installAuthRefreshHandler, startTokenRefreshScheduler, TOKEN_REFRESH_LEAD_MS, } from './session/refresh';
|
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
* - `commonsAvailability` — whether Commons is installed on this device
|
|
25
25
|
* (native only, via the injected `canOpenApp` probe), so the QR view can
|
|
26
26
|
* offer a "Get Commons" fallback instead of a same-device dead end.
|
|
27
|
+
* - `startPasskeyHubSignIn` — on a non-Oxy web origin (where a WebAuthn
|
|
28
|
+
* ceremony can't run locally, the RP-ID is bound to oxy.so), open a popup
|
|
29
|
+
* at the auth.oxy.so passkey hub instead, scoped to the SAME device-flow
|
|
30
|
+
* session `showQr` creates, and complete it via the SAME poll/socket/claim
|
|
31
|
+
* engine. Falls back to `showQr`'s plain QR rendering if the popup is
|
|
32
|
+
* blocked.
|
|
27
33
|
*
|
|
28
34
|
* Sign-in is passkey (WebAuthn) or the Commons QR / shared-keychain handoff —
|
|
29
35
|
* password, social login, and 2FA were removed ecosystem-wide. Account
|
|
@@ -51,6 +57,21 @@ export type AccountDialogView = 'accounts' | 'signin' | 'qr' | 'add' | 'signup';
|
|
|
51
57
|
export type CommonsAvailability = 'unknown' | 'checking' | 'available' | 'unavailable';
|
|
52
58
|
/** Lifecycle phase of the "Sign in with Oxy" device flow. */
|
|
53
59
|
export type SignInFlowPhase = 'idle' | 'starting' | 'waiting' | 'authorized' | 'error';
|
|
60
|
+
/**
|
|
61
|
+
* Minimal structural handle over a popup `Window` — just enough for the
|
|
62
|
+
* cross-origin passkey hub flow ({@link AccountDialogController.startPasskeyHubSignIn}):
|
|
63
|
+
* navigate it once the device-flow session's `authorizeCode` is known, detect
|
|
64
|
+
* the user closing it early (`closed`), and close it programmatically on
|
|
65
|
+
* completion/failure. Satisfied directly by `window.open()`'s return value on
|
|
66
|
+
* web; native has no popup concept and never injects an opener.
|
|
67
|
+
*/
|
|
68
|
+
export interface PopupWindowHandle {
|
|
69
|
+
readonly closed: boolean;
|
|
70
|
+
close(): void;
|
|
71
|
+
location: {
|
|
72
|
+
href: string;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
54
75
|
/** State of the "Sign in with Oxy" (shared-key / QR) device flow. */
|
|
55
76
|
export interface SignInFlowState {
|
|
56
77
|
phase: SignInFlowPhase;
|
|
@@ -166,6 +187,20 @@ export interface AccountDialogControllerOptions {
|
|
|
166
187
|
* `showQr` behaves exactly as before (render QR only).
|
|
167
188
|
*/
|
|
168
189
|
canOpenApp?: (url: string) => Promise<boolean>;
|
|
190
|
+
/**
|
|
191
|
+
* Open a new, empty popup window SYNCHRONOUSLY (before any `await`) so the
|
|
192
|
+
* browser attributes it to the click gesture that triggered it, returning a
|
|
193
|
+
* handle to navigate once the device-flow session is known — or `null` if
|
|
194
|
+
* the popup was blocked. Injected by the provider (web only:
|
|
195
|
+
* `window.open('', ...)`; absent on native, where the hub-popup flow does
|
|
196
|
+
* not apply). Used by {@link AccountDialogController.startPasskeyHubSignIn}.
|
|
197
|
+
*/
|
|
198
|
+
openPopup?: () => PopupWindowHandle | null;
|
|
199
|
+
/**
|
|
200
|
+
* Base origin of the auth.oxy.so passkey hub (defaults to
|
|
201
|
+
* `https://auth.${CENTRAL_IDP_APEX}`). Overridable for local/staging testing.
|
|
202
|
+
*/
|
|
203
|
+
hubBaseUrl?: string;
|
|
169
204
|
}
|
|
170
205
|
type SnapshotListener = (snapshot: AccountDialogSnapshot) => void;
|
|
171
206
|
export declare class AccountDialogController {
|
|
@@ -180,6 +215,8 @@ export declare class AccountDialogController {
|
|
|
180
215
|
private readonly openUrl?;
|
|
181
216
|
private readonly canOpenApp?;
|
|
182
217
|
private readonly socketFactory?;
|
|
218
|
+
private readonly openPopup?;
|
|
219
|
+
private readonly hubBaseUrl;
|
|
183
220
|
private readonly listeners;
|
|
184
221
|
private view;
|
|
185
222
|
private graph;
|
|
@@ -204,6 +241,10 @@ export declare class AccountDialogController {
|
|
|
204
241
|
* single-use token concurrently.
|
|
205
242
|
*/
|
|
206
243
|
private pollInFlight;
|
|
244
|
+
/** The popup opened by {@link startPasskeyHubSignIn}, or `null`. */
|
|
245
|
+
private activePopup;
|
|
246
|
+
/** Watches {@link activePopup}'s `closed` state; see {@link watchPopup}. */
|
|
247
|
+
private popupWatchTimer;
|
|
207
248
|
private unsubscribeSession;
|
|
208
249
|
private unsubscribeTokens;
|
|
209
250
|
/** Last-observed SDK auth readiness (a planted bearer). Drives the fetch edge. */
|
|
@@ -303,6 +344,45 @@ export declare class AccountDialogController {
|
|
|
303
344
|
* session committed. Requires `clientId`.
|
|
304
345
|
*/
|
|
305
346
|
showQr(): Promise<void>;
|
|
347
|
+
/**
|
|
348
|
+
* Web-only: "Sign in with a passkey" on a non-Oxy origin cannot run the
|
|
349
|
+
* WebAuthn ceremony locally — a credential minted with `WEBAUTHN_RP_ID=oxy.so`
|
|
350
|
+
* can only be asserted from `oxy.so`/a subdomain/loopback, a browser-enforced
|
|
351
|
+
* boundary `isOxyRpOrigin()` (consumer-side) already gates on. Instead, open
|
|
352
|
+
* a popup at the auth.oxy.so passkey hub, scoped to the SAME device-flow
|
|
353
|
+
* session {@link showQr} would create (same `authorizeCode`/`sessionToken`
|
|
354
|
+
* pair), and let the SAME poll/socket/claim engine complete it once the hub
|
|
355
|
+
* authorizes the session (`POST /auth/session/authorize-code/:authorizeCode`,
|
|
356
|
+
* bearer-authed, server-side — this method never sees or transmits the
|
|
357
|
+
* secret `sessionToken`).
|
|
358
|
+
*
|
|
359
|
+
* The popup MUST be opened synchronously, before any `await`, or the
|
|
360
|
+
* browser's popup blocker loses the click-gesture attribution and silently
|
|
361
|
+
* blocks it — so {@link openPopup} is invoked as the very first step, before
|
|
362
|
+
* the device-flow session (which needs an async call) even exists. If the
|
|
363
|
+
* popup is blocked (or no `openPopup` was injected), this falls back to
|
|
364
|
+
* `showQr`'s plain QR rendering — no dead end.
|
|
365
|
+
*/
|
|
366
|
+
startPasskeyHubSignIn(): Promise<void>;
|
|
367
|
+
/**
|
|
368
|
+
* Shared device-flow session creation for both {@link showQr} (renders the
|
|
369
|
+
* QR) and {@link startPasskeyHubSignIn} (also opens the hub popup) — the
|
|
370
|
+
* same `startCommonsSignIn` → poll/socket wiring either way. Returns the
|
|
371
|
+
* handle on success (already reflected in `signIn`), or `null` on failure
|
|
372
|
+
* (already set as `signIn.error`).
|
|
373
|
+
*/
|
|
374
|
+
private startDeviceFlowSession;
|
|
375
|
+
/**
|
|
376
|
+
* Poll {@link PopupWindowHandle.closed} so a user who dismisses the hub
|
|
377
|
+
* popup without completing sign-in gets prompt feedback (there is no DOM
|
|
378
|
+
* event for a cross-origin popup closing — polling is the only mechanism).
|
|
379
|
+
* Cleared by {@link closeActivePopup}, which every sign-in exit path already
|
|
380
|
+
* calls.
|
|
381
|
+
*/
|
|
382
|
+
private watchPopup;
|
|
383
|
+
private clearPopupWatchTimer;
|
|
384
|
+
/** Tear down the popup watch timer and close the popup, if any. Idempotent. */
|
|
385
|
+
private closeActivePopup;
|
|
306
386
|
/**
|
|
307
387
|
* Resolve whether Commons is installed on this device via the injected
|
|
308
388
|
* `canOpenApp` probe, updating {@link commonsAvailability} as durable,
|
|
@@ -323,7 +403,7 @@ export declare class AccountDialogController {
|
|
|
323
403
|
* fallback stays live regardless of the outcome here.
|
|
324
404
|
*/
|
|
325
405
|
private deepLinkIntoCommonsIfAvailable;
|
|
326
|
-
/** Tear down the active sign-in device flow (timers + socket + token) and reset to idle. */
|
|
406
|
+
/** Tear down the active sign-in device flow (timers + socket + popup + token) and reset to idle. */
|
|
327
407
|
cancelSignIn(): void;
|
|
328
408
|
private scheduleNextPoll;
|
|
329
409
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/core",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.2.1",
|
|
4
4
|
"description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
"invariant": "^2.2.4",
|
|
118
118
|
"jwt-decode": "^4.0.0",
|
|
119
119
|
"socket.io-client": "^4.8.1",
|
|
120
|
-
"tldts": "^7.4.
|
|
120
|
+
"tldts": "^7.4.8",
|
|
121
121
|
"zod": "^3.25.64"
|
|
122
122
|
},
|
|
123
123
|
"peerDependencies": {
|
|
@@ -147,10 +147,10 @@
|
|
|
147
147
|
"@types/elliptic": "^6.4.18",
|
|
148
148
|
"@types/express": "^4.17.21",
|
|
149
149
|
"@types/invariant": "^2.2.34",
|
|
150
|
-
"@types/node": "^20.19.
|
|
150
|
+
"@types/node": "^20.19.43",
|
|
151
151
|
"expo-crypto": "~56.0.3",
|
|
152
152
|
"expo-secure-store": "~56.0.4",
|
|
153
|
-
"express": "^4.
|
|
153
|
+
"express": "^4.22.2",
|
|
154
154
|
"express-rate-limit": "^7.5.0",
|
|
155
155
|
"release-it": "^19.0.6",
|
|
156
156
|
"typescript": "^5.9.2"
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "أنشئ حسابك",
|
|
54
|
+
"subtitle": "هوية واحدة لكل النظام البيئي.",
|
|
53
55
|
"commonsHeadline": "أنشئ هويتك في Commons",
|
|
54
56
|
"commonsExplainer": "Commons هي هوية Oxy الخاصة بك بحفظ ذاتي كامل — تطبيق واحد يعمل في كل مكان.",
|
|
55
57
|
"createInCommons": "أنشئ هويتك في Commons",
|
|
@@ -61,7 +63,8 @@
|
|
|
61
63
|
"usernameAvailable": "متاح",
|
|
62
64
|
"usernameTaken": "اسم المستخدم هذا مستخدم بالفعل",
|
|
63
65
|
"backToSignInLink": "لديك حساب بالفعل؟ سجّل الدخول",
|
|
64
|
-
"
|
|
66
|
+
"hubExplainer": "أنشئ هوية Oxy الخاصة بك في نافذة آمنة، ثم عد إلى هنا مباشرة.",
|
|
67
|
+
"continueInWindow": "المتابعة في نافذة جديدة",
|
|
65
68
|
"welcome": {
|
|
66
69
|
"title": "مرحبًا بك في Oxy!",
|
|
67
70
|
"subtitle": "أنشئ حسابك في خطوات قليلة",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "Crea el teu compte",
|
|
54
|
+
"subtitle": "Una identitat per a tot l'ecosistema.",
|
|
53
55
|
"commonsHeadline": "Crea la teva identitat a Commons",
|
|
54
56
|
"commonsExplainer": "Commons és la teva identitat d'Oxy amb autocustòdia completa: una app que funciona a tot arreu.",
|
|
55
57
|
"createInCommons": "Crea la teva identitat a Commons",
|
|
@@ -61,7 +63,8 @@
|
|
|
61
63
|
"usernameAvailable": "Disponible",
|
|
62
64
|
"usernameTaken": "Aquest nom d'usuari ja està en ús",
|
|
63
65
|
"backToSignInLink": "Ja tens compte? Inicia sessió",
|
|
64
|
-
"
|
|
66
|
+
"hubExplainer": "Crea la teva Oxy ID en una finestra segura i torna de seguida.",
|
|
67
|
+
"continueInWindow": "Continua en una finestra nova",
|
|
65
68
|
"welcome": {
|
|
66
69
|
"title": "Benvingut a Oxy!",
|
|
67
70
|
"subtitle": "Crea el teu compte en pocs passos",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "Erstellen Sie Ihr Konto",
|
|
54
|
+
"subtitle": "Eine Identität für das gesamte Ökosystem.",
|
|
53
55
|
"commonsHeadline": "Erstellen Sie Ihre Identität in Commons",
|
|
54
56
|
"commonsExplainer": "Commons ist Ihre vollständig selbstverwaltete Oxy-Identität – eine App, überall einsetzbar.",
|
|
55
57
|
"createInCommons": "Erstellen Sie Ihre Identität in Commons",
|
|
@@ -61,7 +63,8 @@
|
|
|
61
63
|
"usernameAvailable": "Verfügbar",
|
|
62
64
|
"usernameTaken": "Dieser Benutzername ist bereits vergeben",
|
|
63
65
|
"backToSignInLink": "Sie haben bereits ein Konto? Anmelden",
|
|
64
|
-
"
|
|
66
|
+
"hubExplainer": "Erstellen Sie Ihre Oxy-ID in einem sicheren Fenster und kehren Sie dann direkt zurück.",
|
|
67
|
+
"continueInWindow": "In einem neuen Fenster fortfahren",
|
|
65
68
|
"welcome": {
|
|
66
69
|
"title": "Willkommen bei Oxy!",
|
|
67
70
|
"subtitle": "Erstellen Sie Ihr Konto in nur wenigen Schritten",
|
|
@@ -57,6 +57,8 @@
|
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
"signup": {
|
|
60
|
+
"title": "Create your account",
|
|
61
|
+
"subtitle": "One identity for the whole ecosystem.",
|
|
60
62
|
"commonsHeadline": "Create your identity in Commons",
|
|
61
63
|
"commonsExplainer": "Commons is your full self-custody Oxy identity — one app, works everywhere.",
|
|
62
64
|
"createInCommons": "Create your identity in Commons",
|
|
@@ -68,7 +70,8 @@
|
|
|
68
70
|
"usernameAvailable": "Available",
|
|
69
71
|
"usernameTaken": "This username is taken",
|
|
70
72
|
"backToSignInLink": "Already have an account? Sign in",
|
|
71
|
-
"
|
|
73
|
+
"hubExplainer": "Create your Oxy ID in a secure window, then come right back.",
|
|
74
|
+
"continueInWindow": "Continue in a new window",
|
|
72
75
|
"welcome": {
|
|
73
76
|
"title": "Welcome to Oxy!",
|
|
74
77
|
"subtitle": "Let's create your account in just a few steps",
|
|
@@ -57,6 +57,8 @@
|
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
"signup": {
|
|
60
|
+
"title": "Crea tu cuenta",
|
|
61
|
+
"subtitle": "Una identidad para todo el ecosistema.",
|
|
60
62
|
"commonsHeadline": "Crea tu identidad en Commons",
|
|
61
63
|
"commonsExplainer": "Commons es tu identidad de Oxy con autocustodia completa: una app que funciona en todas partes.",
|
|
62
64
|
"createInCommons": "Crea tu identidad en Commons",
|
|
@@ -68,7 +70,8 @@
|
|
|
68
70
|
"usernameAvailable": "Disponible",
|
|
69
71
|
"usernameTaken": "Este usuario ya está en uso",
|
|
70
72
|
"backToSignInLink": "¿Ya tienes cuenta? Inicia sesión",
|
|
71
|
-
"
|
|
73
|
+
"hubExplainer": "Crea tu Oxy ID en una ventana segura y vuelve enseguida.",
|
|
74
|
+
"continueInWindow": "Continuar en una ventana nueva",
|
|
72
75
|
"welcome": {
|
|
73
76
|
"title": "¡Te damos la bienvenida a Oxy!",
|
|
74
77
|
"subtitle": "Crea tu cuenta en pocos pasos",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "Créez votre compte",
|
|
54
|
+
"subtitle": "Une seule identité pour tout l'écosystème.",
|
|
53
55
|
"commonsHeadline": "Créez votre identité dans Commons",
|
|
54
56
|
"commonsExplainer": "Commons est votre identité Oxy en autoconservation complète : une seule app, partout.",
|
|
55
57
|
"createInCommons": "Créez votre identité dans Commons",
|
|
@@ -61,7 +63,8 @@
|
|
|
61
63
|
"usernameAvailable": "Disponible",
|
|
62
64
|
"usernameTaken": "Ce nom d'utilisateur est déjà pris",
|
|
63
65
|
"backToSignInLink": "Vous avez déjà un compte ? Connectez-vous",
|
|
64
|
-
"
|
|
66
|
+
"hubExplainer": "Créez votre Oxy ID dans une fenêtre sécurisée, puis revenez directement.",
|
|
67
|
+
"continueInWindow": "Continuer dans une nouvelle fenêtre",
|
|
65
68
|
"welcome": {
|
|
66
69
|
"title": "Bienvenue sur Oxy !",
|
|
67
70
|
"subtitle": "Créez votre compte en quelques étapes",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "Crea il tuo account",
|
|
54
|
+
"subtitle": "Un'unica identità per tutto l'ecosistema.",
|
|
53
55
|
"commonsHeadline": "Crea la tua identità in Commons",
|
|
54
56
|
"commonsExplainer": "Commons è la tua identità Oxy in piena autocustodia: un'unica app, ovunque.",
|
|
55
57
|
"createInCommons": "Crea la tua identità in Commons",
|
|
@@ -61,7 +63,8 @@
|
|
|
61
63
|
"usernameAvailable": "Disponibile",
|
|
62
64
|
"usernameTaken": "Questo nome utente è già in uso",
|
|
63
65
|
"backToSignInLink": "Hai già un account? Accedi",
|
|
64
|
-
"
|
|
66
|
+
"hubExplainer": "Crea il tuo Oxy ID in una finestra sicura, poi torna subito qui.",
|
|
67
|
+
"continueInWindow": "Continua in una nuova finestra",
|
|
65
68
|
"welcome": {
|
|
66
69
|
"title": "Benvenuto su Oxy!",
|
|
67
70
|
"subtitle": "Crea il tuo account in pochi passaggi",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "アカウントを作成",
|
|
54
|
+
"subtitle": "エコシステム全体で使える1つのアイデンティティ。",
|
|
53
55
|
"commonsHeadline": "Commonsであなたのアイデンティティを作成",
|
|
54
56
|
"commonsExplainer": "Commonsは完全に自己管理できるあなたのOxy IDです。1つのアプリでどこでも使えます。",
|
|
55
57
|
"createInCommons": "Commonsであなたのアイデンティティを作成",
|
|
@@ -61,7 +63,8 @@
|
|
|
61
63
|
"usernameAvailable": "利用可能",
|
|
62
64
|
"usernameTaken": "このユーザー名は既に使用されています",
|
|
63
65
|
"backToSignInLink": "すでにアカウントをお持ちですか?サインイン",
|
|
64
|
-
"
|
|
66
|
+
"hubExplainer": "安全なウィンドウでOxy IDを作成し、すぐに戻ってきてください。",
|
|
67
|
+
"continueInWindow": "新しいウィンドウで続ける",
|
|
65
68
|
"welcome": {
|
|
66
69
|
"title": "Oxyへようこそ!",
|
|
67
70
|
"subtitle": "数ステップでアカウントを作成",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "계정 만들기",
|
|
54
|
+
"subtitle": "생태계 전체를 위한 하나의 아이덴티티.",
|
|
53
55
|
"commonsHeadline": "Commons에서 나의 아이덴티티 만들기",
|
|
54
56
|
"commonsExplainer": "Commons는 완전한 자기 보관 방식의 Oxy 아이덴티티입니다. 하나의 앱으로 어디서나 사용하세요.",
|
|
55
57
|
"createInCommons": "Commons에서 나의 아이덴티티 만들기",
|
|
@@ -61,7 +63,8 @@
|
|
|
61
63
|
"usernameAvailable": "사용 가능",
|
|
62
64
|
"usernameTaken": "이미 사용 중인 사용자 이름입니다",
|
|
63
65
|
"backToSignInLink": "이미 계정이 있으신가요? 로그인",
|
|
64
|
-
"
|
|
66
|
+
"hubExplainer": "안전한 창에서 Oxy ID를 만든 후 바로 돌아오세요.",
|
|
67
|
+
"continueInWindow": "새 창에서 계속하기",
|
|
65
68
|
"welcome": {
|
|
66
69
|
"title": "Oxy에 오신 것을 환영합니다!",
|
|
67
70
|
"subtitle": "몇 단계만으로 계정을 만드세요",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "Crie a sua conta",
|
|
54
|
+
"subtitle": "Uma identidade para todo o ecossistema.",
|
|
53
55
|
"commonsHeadline": "Crie a sua identidade no Commons",
|
|
54
56
|
"commonsExplainer": "O Commons é a sua identidade Oxy com autocustódia total: uma app que funciona em todo o lado.",
|
|
55
57
|
"createInCommons": "Crie a sua identidade no Commons",
|
|
@@ -61,7 +63,8 @@
|
|
|
61
63
|
"usernameAvailable": "Disponível",
|
|
62
64
|
"usernameTaken": "Este nome de utilizador já está em uso",
|
|
63
65
|
"backToSignInLink": "Já tem uma conta? Inicie sessão",
|
|
64
|
-
"
|
|
66
|
+
"hubExplainer": "Crie a sua Oxy ID numa janela segura e volte de seguida.",
|
|
67
|
+
"continueInWindow": "Continuar numa nova janela",
|
|
65
68
|
"welcome": {
|
|
66
69
|
"title": "Bem-vindo ao Oxy!",
|
|
67
70
|
"subtitle": "Crie a sua conta em poucos passos",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "创建您的账户",
|
|
54
|
+
"subtitle": "一个身份,畅行整个生态系统。",
|
|
53
55
|
"commonsHeadline": "在 Commons 中创建您的身份",
|
|
54
56
|
"commonsExplainer": "Commons 是您完全自主保管的 Oxy 身份——一个应用,随处可用。",
|
|
55
57
|
"createInCommons": "在 Commons 中创建您的身份",
|
|
@@ -61,7 +63,8 @@
|
|
|
61
63
|
"usernameAvailable": "可用",
|
|
62
64
|
"usernameTaken": "该用户名已被占用",
|
|
63
65
|
"backToSignInLink": "已有账户?登录",
|
|
64
|
-
"
|
|
66
|
+
"hubExplainer": "在安全窗口中创建您的 Oxy ID,然后立即返回。",
|
|
67
|
+
"continueInWindow": "在新窗口中继续",
|
|
65
68
|
"welcome": {
|
|
66
69
|
"title": "欢迎使用Oxy!",
|
|
67
70
|
"subtitle": "只需几步即可创建您的账户",
|
package/src/index.ts
CHANGED
|
@@ -126,7 +126,13 @@ interface Harness {
|
|
|
126
126
|
onSignedIn: jest.Mock;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
function makeHarness(
|
|
129
|
+
function makeHarness(
|
|
130
|
+
over: Partial<{
|
|
131
|
+
clientId: string | null;
|
|
132
|
+
openPopup: () => import('../accountDialogController').PopupWindowHandle | null;
|
|
133
|
+
hubBaseUrl: string;
|
|
134
|
+
}> = {},
|
|
135
|
+
): Harness {
|
|
130
136
|
const oxy = makeOxy();
|
|
131
137
|
const sc = new TestSessionClient(host());
|
|
132
138
|
const commitSession = jest.fn().mockResolvedValue(undefined);
|
|
@@ -138,6 +144,8 @@ function makeHarness(over: Partial<{ clientId: string | null }> = {}): Harness {
|
|
|
138
144
|
commitSession,
|
|
139
145
|
onSignedIn,
|
|
140
146
|
pollIntervalMs: 1000,
|
|
147
|
+
openPopup: over.openPopup,
|
|
148
|
+
hubBaseUrl: over.hubBaseUrl,
|
|
141
149
|
});
|
|
142
150
|
return { controller, oxy, sc, commitSession, onSignedIn };
|
|
143
151
|
}
|
|
@@ -641,6 +649,130 @@ describe('AccountDialogController — sign in with Oxy', () => {
|
|
|
641
649
|
});
|
|
642
650
|
});
|
|
643
651
|
|
|
652
|
+
/** A controllable fake `PopupWindowHandle` for `startPasskeyHubSignIn` tests. */
|
|
653
|
+
function fakePopup(): import('../accountDialogController').PopupWindowHandle & { setClosed: () => void } {
|
|
654
|
+
let closed = false;
|
|
655
|
+
return {
|
|
656
|
+
get closed() {
|
|
657
|
+
return closed;
|
|
658
|
+
},
|
|
659
|
+
close: jest.fn(() => {
|
|
660
|
+
closed = true;
|
|
661
|
+
}),
|
|
662
|
+
location: { href: '' },
|
|
663
|
+
setClosed: () => {
|
|
664
|
+
closed = true;
|
|
665
|
+
},
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
describe('AccountDialogController — startPasskeyHubSignIn (b2 passkey hub popup)', () => {
|
|
670
|
+
it('opens the popup synchronously, then navigates it to the hub URL with the authorizeCode once the session exists', async () => {
|
|
671
|
+
const popup = fakePopup();
|
|
672
|
+
const openPopup = jest.fn(() => popup);
|
|
673
|
+
const { controller, oxy } = makeHarness({ openPopup, hubBaseUrl: 'https://auth.oxy.so' });
|
|
674
|
+
oxy.startCommonsSignIn.mockResolvedValue({
|
|
675
|
+
sessionToken: 'secret-tok',
|
|
676
|
+
authorizeCode: 'AUTH-CODE',
|
|
677
|
+
qrPayload: 'oxycommons://approve?v=1&code=AUTH-CODE',
|
|
678
|
+
expiresAt: Date.now() + 300_000,
|
|
679
|
+
status: 'pending',
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
await controller.startPasskeyHubSignIn();
|
|
683
|
+
|
|
684
|
+
expect(openPopup).toHaveBeenCalledTimes(1);
|
|
685
|
+
expect(popup.location.href).toBe('https://auth.oxy.so/hub-passkey?code=AUTH-CODE');
|
|
686
|
+
// Same underlying device-flow session showQr would create — the QR view
|
|
687
|
+
// still renders as a fallback/alternative alongside the popup.
|
|
688
|
+
const snap = controller.getSnapshot();
|
|
689
|
+
expect(snap.view).toBe('qr');
|
|
690
|
+
expect(snap.signIn.phase).toBe('waiting');
|
|
691
|
+
expect(snap.signIn.authorizeCode).toBe('AUTH-CODE');
|
|
692
|
+
controller.cancelSignIn();
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it('falls back to the plain QR flow (no navigation) when the popup is blocked', async () => {
|
|
696
|
+
const openPopup = jest.fn(() => null);
|
|
697
|
+
const { controller, oxy } = makeHarness({ openPopup });
|
|
698
|
+
oxy.startCommonsSignIn.mockResolvedValue({
|
|
699
|
+
sessionToken: 'secret-tok',
|
|
700
|
+
authorizeCode: 'AUTH-CODE',
|
|
701
|
+
qrPayload: 'oxycommons://approve?v=1&code=AUTH-CODE',
|
|
702
|
+
expiresAt: Date.now() + 300_000,
|
|
703
|
+
status: 'pending',
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
await controller.startPasskeyHubSignIn();
|
|
707
|
+
|
|
708
|
+
expect(oxy.startCommonsSignIn).toHaveBeenCalledWith({ clientId: 'oxy_dk_test' });
|
|
709
|
+
const snap = controller.getSnapshot();
|
|
710
|
+
expect(snap.view).toBe('qr');
|
|
711
|
+
expect(snap.signIn.phase).toBe('waiting');
|
|
712
|
+
expect(snap.signIn.qrPayload).toBe('oxycommons://approve?v=1&code=AUTH-CODE');
|
|
713
|
+
controller.cancelSignIn();
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it('closes the popup without navigating it when device-flow session creation fails', async () => {
|
|
717
|
+
const popup = fakePopup();
|
|
718
|
+
const openPopup = jest.fn(() => popup);
|
|
719
|
+
const { controller, oxy } = makeHarness({ openPopup });
|
|
720
|
+
oxy.startCommonsSignIn.mockRejectedValue(new Error('network down'));
|
|
721
|
+
|
|
722
|
+
await controller.startPasskeyHubSignIn();
|
|
723
|
+
|
|
724
|
+
expect(popup.close).toHaveBeenCalledTimes(1);
|
|
725
|
+
expect(popup.location.href).toBe('');
|
|
726
|
+
expect(controller.getSnapshot().signIn.phase).toBe('error');
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it('surfaces "cancelled" and closes the popup watcher when the user closes the popup before authorizing', async () => {
|
|
730
|
+
jest.useFakeTimers();
|
|
731
|
+
try {
|
|
732
|
+
const popup = fakePopup();
|
|
733
|
+
const openPopup = jest.fn(() => popup);
|
|
734
|
+
const { controller, oxy } = makeHarness({ openPopup });
|
|
735
|
+
oxy.startCommonsSignIn.mockResolvedValue({
|
|
736
|
+
sessionToken: 'secret-tok',
|
|
737
|
+
authorizeCode: 'AUTH-CODE',
|
|
738
|
+
qrPayload: 'oxycommons://approve?v=1&code=AUTH-CODE',
|
|
739
|
+
expiresAt: Date.now() + 300_000,
|
|
740
|
+
status: 'pending',
|
|
741
|
+
});
|
|
742
|
+
oxy.pollCommonsSignIn.mockResolvedValue({ authorized: false, status: 'pending' });
|
|
743
|
+
|
|
744
|
+
await controller.startPasskeyHubSignIn();
|
|
745
|
+
expect(controller.getSnapshot().signIn.phase).toBe('waiting');
|
|
746
|
+
|
|
747
|
+
popup.setClosed();
|
|
748
|
+
await jest.advanceTimersByTimeAsync(1000); // the 1s popup-close watchdog tick
|
|
749
|
+
|
|
750
|
+
const snap = controller.getSnapshot();
|
|
751
|
+
expect(snap.signIn.phase).toBe('error');
|
|
752
|
+
expect(snap.signIn.error).toMatch(/cancelled/i);
|
|
753
|
+
} finally {
|
|
754
|
+
jest.useRealTimers();
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('closes the popup again (never navigates it) when clientId is missing', async () => {
|
|
759
|
+
const popup = fakePopup();
|
|
760
|
+
const openPopup = jest.fn(() => popup);
|
|
761
|
+
const { controller, oxy } = makeHarness({ clientId: null, openPopup });
|
|
762
|
+
|
|
763
|
+
await controller.startPasskeyHubSignIn();
|
|
764
|
+
|
|
765
|
+
// openPopup is invoked unconditionally (before the clientId check, since it
|
|
766
|
+
// must run synchronously) — but the popup must be closed again rather than
|
|
767
|
+
// navigated, since the flow can't proceed without a clientId.
|
|
768
|
+
expect(oxy.startCommonsSignIn).not.toHaveBeenCalled();
|
|
769
|
+
expect(popup.close).toHaveBeenCalledTimes(1);
|
|
770
|
+
expect(popup.location.href).toBe('');
|
|
771
|
+
expect(controller.getSnapshot().signIn.phase).toBe('error');
|
|
772
|
+
expect(controller.getSnapshot().signIn.error).toMatch(/clientId/);
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
|
|
644
776
|
describe('AccountDialogController — Commons availability (canOpenApp)', () => {
|
|
645
777
|
const START_HANDLE = {
|
|
646
778
|
sessionToken: 'secret-tok',
|