@oxyhq/core 12.10.5 → 12.11.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 (130) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/boot/sessionColdBoot.js +142 -37
  3. package/dist/cjs/i18n/locales/ar-SA.json +13 -1
  4. package/dist/cjs/i18n/locales/ca-ES.json +13 -1
  5. package/dist/cjs/i18n/locales/de-DE.json +13 -1
  6. package/dist/cjs/i18n/locales/en-US.json +13 -4
  7. package/dist/cjs/i18n/locales/es-ES.json +13 -4
  8. package/dist/cjs/i18n/locales/fr-FR.json +13 -1
  9. package/dist/cjs/i18n/locales/it-IT.json +13 -1
  10. package/dist/cjs/i18n/locales/ja-JP.json +13 -1
  11. package/dist/cjs/i18n/locales/ko-KR.json +13 -1
  12. package/dist/cjs/i18n/locales/locales/ar-SA.json +13 -1
  13. package/dist/cjs/i18n/locales/locales/ca-ES.json +13 -1
  14. package/dist/cjs/i18n/locales/locales/de-DE.json +13 -1
  15. package/dist/cjs/i18n/locales/locales/en-US.json +13 -4
  16. package/dist/cjs/i18n/locales/locales/es-ES.json +13 -4
  17. package/dist/cjs/i18n/locales/locales/fr-FR.json +13 -1
  18. package/dist/cjs/i18n/locales/locales/it-IT.json +13 -1
  19. package/dist/cjs/i18n/locales/locales/ja-JP.json +13 -1
  20. package/dist/cjs/i18n/locales/locales/ko-KR.json +13 -1
  21. package/dist/cjs/i18n/locales/locales/pt-PT.json +13 -1
  22. package/dist/cjs/i18n/locales/locales/zh-CN.json +13 -1
  23. package/dist/cjs/i18n/locales/pt-PT.json +13 -1
  24. package/dist/cjs/i18n/locales/zh-CN.json +13 -1
  25. package/dist/cjs/index.js +31 -5
  26. package/dist/cjs/mixins/OxyServices.auth.js +257 -17
  27. package/dist/cjs/mixins/OxyServices.deviceBoot.js +54 -3
  28. package/dist/cjs/mixins/OxyServices.notifications.js +63 -0
  29. package/dist/cjs/mixins/index.js +5 -0
  30. package/dist/cjs/session/SessionClient.js +40 -4
  31. package/dist/cjs/session/accountDialogController.js +288 -41
  32. package/dist/cjs/session/hubSync.js +1 -1
  33. package/dist/cjs/session/identityPin.js +256 -0
  34. package/dist/cjs/session/identitySession.js +135 -0
  35. package/dist/cjs/session/projectSessionState.js +54 -15
  36. package/dist/cjs/session/refresh.js +73 -8
  37. package/dist/cjs/utils/commonsDelivery.js +59 -0
  38. package/dist/cjs/utils/oauthPkce.js +33 -3
  39. package/dist/esm/.tsbuildinfo +1 -1
  40. package/dist/esm/boot/sessionColdBoot.js +142 -37
  41. package/dist/esm/i18n/locales/ar-SA.json +13 -1
  42. package/dist/esm/i18n/locales/ca-ES.json +13 -1
  43. package/dist/esm/i18n/locales/de-DE.json +13 -1
  44. package/dist/esm/i18n/locales/en-US.json +13 -4
  45. package/dist/esm/i18n/locales/es-ES.json +13 -4
  46. package/dist/esm/i18n/locales/fr-FR.json +13 -1
  47. package/dist/esm/i18n/locales/it-IT.json +13 -1
  48. package/dist/esm/i18n/locales/ja-JP.json +13 -1
  49. package/dist/esm/i18n/locales/ko-KR.json +13 -1
  50. package/dist/esm/i18n/locales/locales/ar-SA.json +13 -1
  51. package/dist/esm/i18n/locales/locales/ca-ES.json +13 -1
  52. package/dist/esm/i18n/locales/locales/de-DE.json +13 -1
  53. package/dist/esm/i18n/locales/locales/en-US.json +13 -4
  54. package/dist/esm/i18n/locales/locales/es-ES.json +13 -4
  55. package/dist/esm/i18n/locales/locales/fr-FR.json +13 -1
  56. package/dist/esm/i18n/locales/locales/it-IT.json +13 -1
  57. package/dist/esm/i18n/locales/locales/ja-JP.json +13 -1
  58. package/dist/esm/i18n/locales/locales/ko-KR.json +13 -1
  59. package/dist/esm/i18n/locales/locales/pt-PT.json +13 -1
  60. package/dist/esm/i18n/locales/locales/zh-CN.json +13 -1
  61. package/dist/esm/i18n/locales/pt-PT.json +13 -1
  62. package/dist/esm/i18n/locales/zh-CN.json +13 -1
  63. package/dist/esm/index.js +14 -1
  64. package/dist/esm/mixins/OxyServices.auth.js +257 -17
  65. package/dist/esm/mixins/OxyServices.deviceBoot.js +52 -3
  66. package/dist/esm/mixins/OxyServices.notifications.js +60 -0
  67. package/dist/esm/mixins/index.js +5 -0
  68. package/dist/esm/session/SessionClient.js +40 -4
  69. package/dist/esm/session/accountDialogController.js +288 -41
  70. package/dist/esm/session/hubSync.js +1 -1
  71. package/dist/esm/session/identityPin.js +249 -0
  72. package/dist/esm/session/identitySession.js +131 -0
  73. package/dist/esm/session/projectSessionState.js +54 -15
  74. package/dist/esm/session/refresh.js +73 -8
  75. package/dist/esm/utils/commonsDelivery.js +54 -0
  76. package/dist/esm/utils/oauthPkce.js +31 -2
  77. package/dist/types/.tsbuildinfo +1 -1
  78. package/dist/types/boot/sessionColdBoot.d.ts +38 -1
  79. package/dist/types/index.d.ts +12 -4
  80. package/dist/types/mixins/OxyServices.auth.d.ts +262 -11
  81. package/dist/types/mixins/OxyServices.deviceBoot.d.ts +31 -1
  82. package/dist/types/mixins/OxyServices.notifications.d.ts +143 -0
  83. package/dist/types/mixins/index.d.ts +2 -1
  84. package/dist/types/session/SessionClient.d.ts +28 -0
  85. package/dist/types/session/accountDialogController.d.ts +187 -9
  86. package/dist/types/session/identityPin.d.ts +87 -0
  87. package/dist/types/session/identitySession.d.ts +105 -0
  88. package/dist/types/session/projectSessionState.d.ts +21 -19
  89. package/dist/types/session/refresh.d.ts +32 -1
  90. package/dist/types/utils/commonsDelivery.d.ts +105 -0
  91. package/dist/types/utils/oauthPkce.d.ts +21 -1
  92. package/package.json +2 -2
  93. package/src/boot/__tests__/sessionColdBoot.identity.test.ts +393 -0
  94. package/src/boot/sessionColdBoot.ts +183 -37
  95. package/src/i18n/locales/ar-SA.json +13 -1
  96. package/src/i18n/locales/ca-ES.json +13 -1
  97. package/src/i18n/locales/de-DE.json +13 -1
  98. package/src/i18n/locales/en-US.json +13 -4
  99. package/src/i18n/locales/es-ES.json +13 -4
  100. package/src/i18n/locales/fr-FR.json +13 -1
  101. package/src/i18n/locales/it-IT.json +13 -1
  102. package/src/i18n/locales/ja-JP.json +13 -1
  103. package/src/i18n/locales/ko-KR.json +13 -1
  104. package/src/i18n/locales/pt-PT.json +13 -1
  105. package/src/i18n/locales/zh-CN.json +13 -1
  106. package/src/index.ts +49 -0
  107. package/src/mixins/OxyServices.auth.ts +464 -24
  108. package/src/mixins/OxyServices.deviceBoot.ts +54 -2
  109. package/src/mixins/OxyServices.notifications.ts +142 -0
  110. package/src/mixins/__tests__/commonsSignIn.test.ts +754 -2
  111. package/src/mixins/__tests__/deviceTokenMintPinned.test.ts +113 -0
  112. package/src/mixins/__tests__/preSessionSkipAuth.test.ts +18 -0
  113. package/src/mixins/index.ts +6 -0
  114. package/src/session/SessionClient.ts +57 -4
  115. package/src/session/__tests__/SessionClient.identityPin.test.ts +175 -0
  116. package/src/session/__tests__/accountDialogController.test.ts +812 -1
  117. package/src/session/__tests__/hubSync.test.ts +51 -0
  118. package/src/session/__tests__/identityPin.test.ts +131 -0
  119. package/src/session/__tests__/identitySession.test.ts +194 -0
  120. package/src/session/__tests__/projectSessionState.test.ts +88 -0
  121. package/src/session/__tests__/refresh.identityPin.test.ts +306 -0
  122. package/src/session/accountDialogController.ts +435 -44
  123. package/src/session/hubSync.ts +1 -1
  124. package/src/session/identityPin.ts +310 -0
  125. package/src/session/identitySession.ts +206 -0
  126. package/src/session/projectSessionState.ts +61 -13
  127. package/src/session/refresh.ts +103 -9
  128. package/src/utils/__tests__/oauthPkce.test.ts +51 -0
  129. package/src/utils/commonsDelivery.ts +132 -0
  130. package/src/utils/oauthPkce.ts +56 -3
@@ -15,18 +15,45 @@
15
15
  * origin persisted a `deviceId` + `deviceSecret`, mint a short access token
16
16
  * with a single bearer-less POST to `/session/device/token` (no cookie, no
17
17
  * navigation) and rotate the secret in-use.
18
- * 3. `shared-key-signin` (native) — re-mint from the shared-keychain identity.
18
+ * 3. `shared-key-signin` (native, ACCOUNT mode) — re-mint from the
19
+ * shared-keychain identity — OR `identity-key-signin` (IDENTITY mode) —
20
+ * re-mint from THIS device's primary identity key.
19
21
  * 4. Signed out.
20
22
  *
23
+ * Two session modes (see {@link RunSessionColdBootOptions.sessionMode}):
24
+ * - `account` (default) — the device's ACTIVE account owns the session. Every
25
+ * Oxy app but the identity vault boots this way; behaviour is unchanged.
26
+ * - `identity` — the owner of the local PRIMARY identity key owns the session,
27
+ * permanently, regardless of which account the device is switched to. Each
28
+ * step above is bound to the persisted identity pin, and the shared-keychain
29
+ * lane is replaced by the primary-key one (the shared slot is a CROSS-APP
30
+ * slot that may hold a different identity).
31
+ *
21
32
  * ESM-safe (no `require()`); no react/react-native/expo imports.
22
33
  */
23
34
  import { runColdBoot, type ColdBootOutcome, type ColdBootStep } from '../utils/coldBoot';
24
35
  import { isNative as detectNative } from '../utils/platform';
25
36
  import { logger } from '../logger';
37
+ import { computeIdentityTag } from '../utils/cacheKey';
26
38
  import { TOKEN_REFRESH_LEAD_MS, refreshDeviceSecretArm } from '../session/refresh';
39
+ import {
40
+ establishIdentitySession,
41
+ resolveIdentityPin,
42
+ type IdentityBinding,
43
+ } from '../session/identitySession';
44
+ import type { IdentityPin } from '../session/identityPin';
27
45
  import type { OxyServices } from '../OxyServices';
28
46
  import type { AuthStateStore } from '../session/authStateStore';
29
47
 
48
+ /**
49
+ * Who owns the session this boot resolves.
50
+ *
51
+ * - `account` — the device's active account (every ordinary Oxy app).
52
+ * - `identity` — the owner of the device's primary identity key (the identity
53
+ * vault). Requires {@link RunSessionColdBootOptions.identity}.
54
+ */
55
+ export type SessionMode = 'account' | 'identity';
56
+
30
57
  /** The winning session shape a cold-boot step reports. */
31
58
  export interface DeviceBootSession {
32
59
  sessionId: string;
@@ -72,6 +99,23 @@ export interface RunSessionColdBootOptions {
72
99
  * so a flaky probe can never falsely skip a real sign-in.
73
100
  */
74
101
  isOffline?: () => boolean;
102
+ /**
103
+ * Who owns the resolved session. Defaults to `'account'` — the device's active
104
+ * account — which is the behaviour every ordinary Oxy app has today and which
105
+ * this option leaves byte-for-byte unchanged.
106
+ *
107
+ * `'identity'` binds the boot to the owner of this device's PRIMARY identity
108
+ * key and REQUIRES {@link identity}. When it is missing the boot refuses to
109
+ * run any lane (an identity-bound client silently falling back to the device's
110
+ * active account is precisely the bug this mode exists to prevent) and
111
+ * resolves signed out.
112
+ */
113
+ sessionMode?: SessionMode;
114
+ /**
115
+ * The identity binding (pin store + key/signature access) used by
116
+ * `sessionMode: 'identity'`. Ignored in `'account'` mode.
117
+ */
118
+ identity?: IdentityBinding;
75
119
  }
76
120
 
77
121
  /**
@@ -85,6 +129,19 @@ export async function runSessionColdBoot(
85
129
  const { oxy, store } = opts;
86
130
  const isNative = opts.platform?.isNative ?? detectNative();
87
131
 
132
+ // Non-null ONLY in identity mode; every `!== null` test below therefore reads
133
+ // as "is this boot identity-bound?" while also narrowing the binding.
134
+ const identityBinding = opts.sessionMode === 'identity' ? opts.identity ?? null : null;
135
+ if (opts.sessionMode === 'identity' && identityBinding === null) {
136
+ logger.error(
137
+ 'runSessionColdBoot: sessionMode "identity" requires an `identity` binding — refusing to run any lane (an identity-bound client must never adopt the device active account)',
138
+ undefined,
139
+ { component: 'sessionColdBoot', method: 'runSessionColdBoot' },
140
+ );
141
+ await opts.onSignedOut?.('error');
142
+ return { kind: 'unauthenticated' };
143
+ }
144
+
88
145
  // Best-effort connectivity gate for the NETWORK steps only. A missing hint or
89
146
  // any non-`true` verdict means "assume online" — never falsely skip a real
90
147
  // sign-in on an ambiguous probe.
@@ -94,6 +151,23 @@ export async function runSessionColdBoot(
94
151
  // bundler re-evaluation.
95
152
  let signedOutReason: SignedOutReason = 'no_session';
96
153
 
154
+ // Boot-local memo for the identity pin. Resolved lazily INSIDE a step so the
155
+ // (local, but storage-backed) read is covered by `overallDeadlineMs`, and
156
+ // resolved at most once per boot so two steps cannot disagree. Always `null`
157
+ // in account mode.
158
+ let pinResolved = false;
159
+ let cachedPin: IdentityPin | null = null;
160
+ const getIdentityPin = async (): Promise<IdentityPin | null> => {
161
+ if (identityBinding === null) {
162
+ return null;
163
+ }
164
+ if (!pinResolved) {
165
+ cachedPin = await resolveIdentityPin(identityBinding);
166
+ pinResolved = true;
167
+ }
168
+ return cachedPin;
169
+ };
170
+
97
171
  const steps: Array<ColdBootStep<DeviceBootSession>> = [];
98
172
 
99
173
  // 1. warm-token-plant (web + native) — the fastest path. When the persisted
@@ -122,6 +196,21 @@ export async function runSessionColdBoot(
122
196
  if (!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now() + TOKEN_REFRESH_LEAD_MS) {
123
197
  return { kind: 'skip' };
124
198
  }
199
+ if (identityBinding !== null) {
200
+ // Identity-bound: a persisted token is only acceptable when it belongs to
201
+ // the PINNED account. Both the token's own claim (`computeIdentityTag`,
202
+ // the same derivation `SessionClient.applyState` uses) and the owning
203
+ // session identity must match — a token planted for another account by an
204
+ // earlier account-mode write is exactly the durable drift this gate stops.
205
+ const pin = await getIdentityPin();
206
+ if (
207
+ pin === null ||
208
+ persisted.userId !== pin.accountId ||
209
+ computeIdentityTag(persisted.accessToken) !== pin.accountId
210
+ ) {
211
+ return { kind: 'skip' };
212
+ }
213
+ }
125
214
  oxy.setTokens(persisted.accessToken);
126
215
  return {
127
216
  kind: 'session',
@@ -148,7 +237,16 @@ export async function runSessionColdBoot(
148
237
  // a doomed mint cannot burn the overall deadline before routing settles.
149
238
  enabled: () => !isOffline(),
150
239
  run: async () => {
151
- const result = await refreshDeviceSecretArm({ oxy, store });
240
+ const pin = await getIdentityPin();
241
+ if (identityBinding !== null && pin === null) {
242
+ // Identity-bound with no verified pin (never written, or cleared because
243
+ // the local key changed/vanished). An UNPINNED mint would adopt whichever
244
+ // account the device is currently switched to — the exact drift this mode
245
+ // exists to prevent. Fall through to `identity-key-signin`, which
246
+ // re-derives the account from the local key and rewrites the pin.
247
+ return { kind: 'skip' };
248
+ }
249
+ const result = await refreshDeviceSecretArm({ oxy, store, pin });
152
250
  switch (result.status) {
153
251
  case 'ok':
154
252
  // The arm persisted the rotated secret and planted the token.
@@ -186,6 +284,15 @@ export async function runSessionColdBoot(
186
284
  { component: 'sessionColdBoot', method: 'device-secret-mint' },
187
285
  );
188
286
  return { kind: 'skip' };
287
+ case 'account-not-on-device':
288
+ // Pinned mint rejected: the pinned account is no longer a live account
289
+ // of this device session. The secret is healthy — keep it untouched and
290
+ // let `identity-key-signin` re-establish from the local key.
291
+ logger.debug(
292
+ 'pinned device-secret mint rejected (account_not_on_device) — keeping secret, re-establishing from the identity key',
293
+ { component: 'sessionColdBoot', method: 'device-secret-mint' },
294
+ );
295
+ return { kind: 'skip' };
189
296
  case 'transient':
190
297
  // Network / 5xx: keep the secret; a later attempt can succeed.
191
298
  logger.debug(
@@ -199,42 +306,81 @@ export async function runSessionColdBoot(
199
306
  },
200
307
  });
201
308
 
202
- // 3. shared-key-signin (native) — re-mint from the shared identity. Native
203
- // AND online: it is a network step (challenge + verify round-trips), so it
204
- // is gated by the same offline hint as the mint lane. `{ retry: false }`
205
- // keeps the two round-trips as single attempts the refresh scheduler /
206
- // 401 lane own later retries so this step cannot multiply boot latency.
207
- steps.push({
208
- id: 'shared-key-signin',
209
- enabled: () => isNative && !isOffline(),
210
- run: async () => {
211
- const session = await oxy.signInWithSharedIdentity({ requestOptions: { retry: false } });
212
- if (!session?.accessToken) {
213
- return { kind: 'skip' };
214
- }
215
- // `verifyChallenge` mints a rotating deviceSecret; persist it so the next
216
- // boot can use the faster device-secret-mint lane (sockets + tab-focus
217
- // re-mint depend on the credential being in the store).
218
- if (session.deviceId && session.deviceSecret) {
219
- await store.save({
220
- sessionId: session.sessionId,
221
- userId: session.user.id,
222
- deviceId: session.deviceId,
223
- deviceSecret: session.deviceSecret,
224
- accessToken: session.accessToken,
225
- expiresAt: session.expiresAt,
309
+ if (identityBinding !== null) {
310
+ // 3-identity. identity-key-signin re-mint from THIS device's PRIMARY
311
+ // identity key (`getPublicKey` challenge sign verify). It REPLACES
312
+ // `shared-key-signin`, which reads the CROSS-APP shared keychain slot and
313
+ // may therefore hold a different identity than the device's primary the
314
+ // one thing an identity-bound client must never adopt. The server resolves
315
+ // the account from the verified signer, so the winning session is
316
+ // identity-authoritative and the pin is (re)written from it.
317
+ //
318
+ // Online-gated like every network step; `{ retry: false }` keeps the two
319
+ // round-trips single attempts (the refresh scheduler / 401 lane own later
320
+ // retries) so this step cannot multiply boot latency. On web
321
+ // `KeyManager.getPublicKey()` resolves to `null`, so it skips.
322
+ steps.push({
323
+ id: 'identity-key-signin',
324
+ enabled: () => !isOffline(),
325
+ run: async () => {
326
+ const established = await establishIdentitySession({
327
+ oxy,
328
+ store,
329
+ binding: identityBinding,
330
+ requestOptions: { retry: false },
226
331
  });
227
- }
228
- return {
229
- kind: 'session',
230
- session: {
231
- sessionId: session.sessionId,
232
- userId: session.user.id,
233
- accessToken: session.accessToken,
234
- },
235
- };
236
- },
237
- });
332
+ const accessToken = established?.session.accessToken;
333
+ if (!established || !accessToken) {
334
+ return { kind: 'skip' };
335
+ }
336
+ return {
337
+ kind: 'session',
338
+ session: {
339
+ sessionId: established.session.sessionId,
340
+ userId: established.session.user.id,
341
+ accessToken,
342
+ },
343
+ };
344
+ },
345
+ });
346
+ } else {
347
+ // 3. shared-key-signin (native) — re-mint from the shared identity. Native
348
+ // AND online: it is a network step (challenge + verify round-trips), so it
349
+ // is gated by the same offline hint as the mint lane. `{ retry: false }`
350
+ // keeps the two round-trips as single attempts — the refresh scheduler /
351
+ // 401 lane own later retries — so this step cannot multiply boot latency.
352
+ steps.push({
353
+ id: 'shared-key-signin',
354
+ enabled: () => isNative && !isOffline(),
355
+ run: async () => {
356
+ const session = await oxy.signInWithSharedIdentity({ requestOptions: { retry: false } });
357
+ if (!session?.accessToken) {
358
+ return { kind: 'skip' };
359
+ }
360
+ // `verifyChallenge` mints a rotating deviceSecret; persist it so the next
361
+ // boot can use the faster device-secret-mint lane (sockets + tab-focus
362
+ // re-mint depend on the credential being in the store).
363
+ if (session.deviceId && session.deviceSecret) {
364
+ await store.save({
365
+ sessionId: session.sessionId,
366
+ userId: session.user.id,
367
+ deviceId: session.deviceId,
368
+ deviceSecret: session.deviceSecret,
369
+ accessToken: session.accessToken,
370
+ expiresAt: session.expiresAt,
371
+ });
372
+ }
373
+ return {
374
+ kind: 'session',
375
+ session: {
376
+ sessionId: session.sessionId,
377
+ userId: session.user.id,
378
+ accessToken: session.accessToken,
379
+ },
380
+ };
381
+ },
382
+ });
383
+ }
238
384
 
239
385
  const outcome = await runColdBoot<DeviceBootSession>({
240
386
  steps,
@@ -124,7 +124,19 @@
124
124
  "commonsNotInstalled": "لا تملك Commons؟ احصل على التطبيق لتسجيل الدخول بمعرّف Oxy الخاص بك.",
125
125
  "showQrAnyway": "لدي Commons على جهاز آخر",
126
126
  "switchWhileSignedInAs": "تبديل الحساب، مسجّل الدخول باسم {{name}}",
127
- "manageOnDevice": "إدارة الحسابات على هذا الجهاز"
127
+ "manageOnDevice": "إدارة الحسابات على هذا الجهاز",
128
+ "continueWithOxy": "المتابعة باستخدام Oxy",
129
+ "havingTrouble": "هل تواجه مشكلة؟",
130
+ "progress": {
131
+ "preparing": "جارٍ تحضير الطلب",
132
+ "awaitingApproval": "في انتظار الموافقة",
133
+ "scanWithCommons": "امسح الرمز باستخدام Commons على هاتفك",
134
+ "continueInCommons": "تابع في Commons",
135
+ "checkCommons": "تحقّق من Commons على هاتفك",
136
+ "openedInCommons": "تم الفتح في Commons",
137
+ "confirming": "جارٍ تأكيد الهوية",
138
+ "confirmed": "تم تأكيد الهوية"
139
+ }
128
140
  },
129
141
  "common": {
130
142
  "actions": {
@@ -124,7 +124,19 @@
124
124
  "commonsNotInstalled": "No tens Commons? Descarrega l'app per iniciar sessió amb el teu Oxy ID.",
125
125
  "showQrAnyway": "Tinc Commons en un altre dispositiu",
126
126
  "switchWhileSignedInAs": "Canvia de compte, amb la sessió iniciada com a {{name}}",
127
- "manageOnDevice": "Gestionar comptes en aquest dispositiu"
127
+ "manageOnDevice": "Gestionar comptes en aquest dispositiu",
128
+ "continueWithOxy": "Continua amb Oxy",
129
+ "havingTrouble": "Algun problema?",
130
+ "progress": {
131
+ "preparing": "Preparant la sol·licitud",
132
+ "awaitingApproval": "Esperant l'aprovació",
133
+ "scanWithCommons": "Escaneja amb Commons al teu mòbil",
134
+ "continueInCommons": "Continua a Commons",
135
+ "checkCommons": "Revisa Commons al teu mòbil",
136
+ "openedInCommons": "Obert a Commons",
137
+ "confirming": "Confirmant la identitat",
138
+ "confirmed": "Identitat confirmada"
139
+ }
128
140
  },
129
141
  "common": {
130
142
  "actions": {
@@ -124,7 +124,19 @@
124
124
  "commonsNotInstalled": "Kein Commons? Holen Sie sich die App, um sich mit Ihrer Oxy-ID anzumelden.",
125
125
  "showQrAnyway": "Ich habe Commons auf einem anderen Gerät",
126
126
  "switchWhileSignedInAs": "Konto wechseln, angemeldet als {{name}}",
127
- "manageOnDevice": "Konten auf diesem Gerät verwalten"
127
+ "manageOnDevice": "Konten auf diesem Gerät verwalten",
128
+ "continueWithOxy": "Mit Oxy fortfahren",
129
+ "havingTrouble": "Gibt es ein Problem?",
130
+ "progress": {
131
+ "preparing": "Anfrage wird vorbereitet",
132
+ "awaitingApproval": "Warten auf Freigabe",
133
+ "scanWithCommons": "Scannen Sie mit Commons auf Ihrem Telefon",
134
+ "continueInCommons": "In Commons fortfahren",
135
+ "checkCommons": "Prüfen Sie Commons auf Ihrem Telefon",
136
+ "openedInCommons": "In Commons geöffnet",
137
+ "confirming": "Identität wird bestätigt",
138
+ "confirmed": "Identität bestätigt"
139
+ }
128
140
  },
129
141
  "common": {
130
142
  "actions": {
@@ -412,9 +412,6 @@
412
412
  },
413
413
  "label": "Account switcher",
414
414
  "searchPlaceholder": "Search accounts",
415
- "scanTitle": "Scan to sign in",
416
- "scanSubtitle": "Open the Oxy app on your phone and scan this code",
417
- "scanWithOxy": "Scan with the Oxy app to continue",
418
415
  "scanQr": "Scan QR code",
419
416
  "qrHeadline": "Sign in with your Oxy identity",
420
417
  "signInWithOxy": "Sign in with Oxy",
@@ -424,7 +421,19 @@
424
421
  "commonsNotInstalled": "Don't have Commons? Get the app to sign in with your Oxy ID.",
425
422
  "showQrAnyway": "I have Commons on another device",
426
423
  "switchWhileSignedInAs": "Switch account, signed in as {{name}}",
427
- "manageOnDevice": "Manage accounts on this device"
424
+ "manageOnDevice": "Manage accounts on this device",
425
+ "continueWithOxy": "Continue with Oxy",
426
+ "havingTrouble": "Having trouble?",
427
+ "progress": {
428
+ "preparing": "Preparing request",
429
+ "awaitingApproval": "Waiting for approval",
430
+ "scanWithCommons": "Scan with Commons on your phone",
431
+ "continueInCommons": "Continue in Commons",
432
+ "checkCommons": "Check Commons on your phone",
433
+ "openedInCommons": "Opened in Commons",
434
+ "confirming": "Confirming identity",
435
+ "confirmed": "Identity confirmed"
436
+ }
428
437
  },
429
438
  "reputation": {
430
439
  "faq": {
@@ -652,9 +652,6 @@
652
652
  },
653
653
  "label": "Selector de cuentas",
654
654
  "searchPlaceholder": "Buscar cuentas",
655
- "scanTitle": "Escanea para iniciar sesión",
656
- "scanSubtitle": "Abre la app de Oxy en tu móvil y escanea este código",
657
- "scanWithOxy": "Escanea con la app de Oxy para continuar",
658
655
  "scanQr": "Escanear código QR",
659
656
  "qrHeadline": "Inicia sesión con tu identidad de Oxy",
660
657
  "signInWithOxy": "Inicia sesión con Oxy",
@@ -664,7 +661,19 @@
664
661
  "commonsNotInstalled": "¿No tienes Commons? Descarga la app para iniciar sesión con tu Oxy ID.",
665
662
  "showQrAnyway": "Tengo Commons en otro dispositivo",
666
663
  "switchWhileSignedInAs": "Cambiar de cuenta, con la sesión iniciada como {{name}}",
667
- "manageOnDevice": "Gestionar cuentas en este dispositivo"
664
+ "manageOnDevice": "Gestionar cuentas en este dispositivo",
665
+ "continueWithOxy": "Continuar con Oxy",
666
+ "havingTrouble": "¿Algún problema?",
667
+ "progress": {
668
+ "preparing": "Preparando la solicitud",
669
+ "awaitingApproval": "Esperando la aprobación",
670
+ "scanWithCommons": "Escanea con Commons en tu móvil",
671
+ "continueInCommons": "Continúa en Commons",
672
+ "checkCommons": "Revisa Commons en tu móvil",
673
+ "openedInCommons": "Abierto en Commons",
674
+ "confirming": "Confirmando identidad",
675
+ "confirmed": "Identidad confirmada"
676
+ }
668
677
  },
669
678
  "feedback": {
670
679
  "type": {
@@ -124,7 +124,19 @@
124
124
  "commonsNotInstalled": "Pas de Commons ? Téléchargez l'app pour vous connecter avec votre Oxy ID.",
125
125
  "showQrAnyway": "J'ai Commons sur un autre appareil",
126
126
  "switchWhileSignedInAs": "Changer de compte, connecté en tant que {{name}}",
127
- "manageOnDevice": "Gérer les comptes sur cet appareil"
127
+ "manageOnDevice": "Gérer les comptes sur cet appareil",
128
+ "continueWithOxy": "Continuer avec Oxy",
129
+ "havingTrouble": "Un problème ?",
130
+ "progress": {
131
+ "preparing": "Préparation de la demande",
132
+ "awaitingApproval": "En attente d'approbation",
133
+ "scanWithCommons": "Scannez avec Commons sur votre téléphone",
134
+ "continueInCommons": "Continuez dans Commons",
135
+ "checkCommons": "Consultez Commons sur votre téléphone",
136
+ "openedInCommons": "Ouvert dans Commons",
137
+ "confirming": "Confirmation de l'identité",
138
+ "confirmed": "Identité confirmée"
139
+ }
128
140
  },
129
141
  "common": {
130
142
  "actions": {
@@ -124,7 +124,19 @@
124
124
  "commonsNotInstalled": "Non hai Commons? Scarica l'app per accedere con il tuo Oxy ID.",
125
125
  "showQrAnyway": "Ho Commons su un altro dispositivo",
126
126
  "switchWhileSignedInAs": "Cambia account, connesso come {{name}}",
127
- "manageOnDevice": "Gestisci account su questo dispositivo"
127
+ "manageOnDevice": "Gestisci account su questo dispositivo",
128
+ "continueWithOxy": "Continua con Oxy",
129
+ "havingTrouble": "Problemi?",
130
+ "progress": {
131
+ "preparing": "Preparazione della richiesta",
132
+ "awaitingApproval": "In attesa di approvazione",
133
+ "scanWithCommons": "Scansiona con Commons sul tuo telefono",
134
+ "continueInCommons": "Continua in Commons",
135
+ "checkCommons": "Controlla Commons sul tuo telefono",
136
+ "openedInCommons": "Aperto in Commons",
137
+ "confirming": "Conferma dell'identità in corso",
138
+ "confirmed": "Identità confermata"
139
+ }
128
140
  },
129
141
  "common": {
130
142
  "actions": {
@@ -124,7 +124,19 @@
124
124
  "commonsNotInstalled": "Commonsがありませんか?アプリを入手してOxy IDでサインインしましょう。",
125
125
  "showQrAnyway": "別のデバイスにCommonsがあります",
126
126
  "switchWhileSignedInAs": "アカウントを切り替え({{name}} としてログイン中)",
127
- "manageOnDevice": "このデバイスのアカウントを管理"
127
+ "manageOnDevice": "このデバイスのアカウントを管理",
128
+ "continueWithOxy": "Oxyで続ける",
129
+ "havingTrouble": "うまくいきませんか?",
130
+ "progress": {
131
+ "preparing": "リクエストを準備しています",
132
+ "awaitingApproval": "承認を待っています",
133
+ "scanWithCommons": "スマートフォンのCommonsでスキャンしてください",
134
+ "continueInCommons": "Commonsで続けてください",
135
+ "checkCommons": "スマートフォンのCommonsを確認してください",
136
+ "openedInCommons": "Commonsで開きました",
137
+ "confirming": "本人確認をしています",
138
+ "confirmed": "本人確認が完了しました"
139
+ }
128
140
  },
129
141
  "common": {
130
142
  "actions": {
@@ -124,7 +124,19 @@
124
124
  "commonsNotInstalled": "Commons가 없으신가요? 앱을 다운로드하여 Oxy ID로 로그인하세요.",
125
125
  "showQrAnyway": "다른 기기에 Commons가 있습니다",
126
126
  "switchWhileSignedInAs": "계정 전환, {{name}}(으)로 로그인됨",
127
- "manageOnDevice": "이 기기의 계정 관리"
127
+ "manageOnDevice": "이 기기의 계정 관리",
128
+ "continueWithOxy": "Oxy로 계속하기",
129
+ "havingTrouble": "문제가 있으신가요?",
130
+ "progress": {
131
+ "preparing": "요청 준비 중",
132
+ "awaitingApproval": "승인 대기 중",
133
+ "scanWithCommons": "휴대폰의 Commons로 스캔하세요",
134
+ "continueInCommons": "Commons에서 계속하세요",
135
+ "checkCommons": "휴대폰에서 Commons를 확인하세요",
136
+ "openedInCommons": "Commons에서 열림",
137
+ "confirming": "신원 확인 중",
138
+ "confirmed": "신원 확인 완료"
139
+ }
128
140
  },
129
141
  "common": {
130
142
  "actions": {
@@ -124,7 +124,19 @@
124
124
  "commonsNotInstalled": "Não tem o Commons? Obtenha a app para iniciar sessão com o seu Oxy ID.",
125
125
  "showQrAnyway": "Tenho o Commons noutro dispositivo",
126
126
  "switchWhileSignedInAs": "Mudar de conta, com sessão iniciada como {{name}}",
127
- "manageOnDevice": "Gerir contas neste dispositivo"
127
+ "manageOnDevice": "Gerir contas neste dispositivo",
128
+ "continueWithOxy": "Continuar com Oxy",
129
+ "havingTrouble": "Algum problema?",
130
+ "progress": {
131
+ "preparing": "A preparar o pedido",
132
+ "awaitingApproval": "A aguardar aprovação",
133
+ "scanWithCommons": "Digitalize com o Commons no seu telemóvel",
134
+ "continueInCommons": "Continue no Commons",
135
+ "checkCommons": "Verifique o Commons no seu telemóvel",
136
+ "openedInCommons": "Aberto no Commons",
137
+ "confirming": "A confirmar a identidade",
138
+ "confirmed": "Identidade confirmada"
139
+ }
128
140
  },
129
141
  "common": {
130
142
  "actions": {
@@ -124,7 +124,19 @@
124
124
  "commonsNotInstalled": "没有 Commons?获取应用以使用您的 Oxy ID 登录。",
125
125
  "showQrAnyway": "我在另一台设备上有 Commons",
126
126
  "switchWhileSignedInAs": "切换账号,当前登录为 {{name}}",
127
- "manageOnDevice": "管理此设备上的账号"
127
+ "manageOnDevice": "管理此设备上的账号",
128
+ "continueWithOxy": "使用 Oxy 继续",
129
+ "havingTrouble": "遇到问题?",
130
+ "progress": {
131
+ "preparing": "正在准备请求",
132
+ "awaitingApproval": "正在等待批准",
133
+ "scanWithCommons": "请用手机上的 Commons 扫描",
134
+ "continueInCommons": "请在 Commons 中继续",
135
+ "checkCommons": "请查看手机上的 Commons",
136
+ "openedInCommons": "已在 Commons 中打开",
137
+ "confirming": "正在确认身份",
138
+ "confirmed": "身份已确认"
139
+ }
128
140
  },
129
141
  "common": {
130
142
  "actions": {
package/src/index.ts CHANGED
@@ -41,14 +41,33 @@ export {
41
41
  getCommonsApprovalBlockingReason,
42
42
  parseCommonsApprovalExpiresAt,
43
43
  } from './utils/commonsApproval';
44
+ // Automatic "Sign in with Oxy" delivery selection — ONE pure decision that maps
45
+ // the caller's facts onto exactly one primary route (open Commons / await push / QR).
46
+ export { selectCommonsDelivery, pushTargetsFromDelivery, commonsDeliveryPlatform } from './utils/commonsDelivery';
47
+ export type {
48
+ CommonsDeliveryFacts,
49
+ CommonsDeliveryPlatform,
50
+ CommonsDeliveryRoute,
51
+ } from './utils/commonsDelivery';
44
52
  export type { ServiceTokenResponse } from './mixins/OxyServices.auth';
45
53
  // "Sign in with Oxy" — handoff (Workstream C)
46
54
  export type {
47
55
  CommonsSignInHandle,
48
56
  CommonsSignInStatus,
57
+ CommonsSignInPurpose,
58
+ CommonsOAuthContext,
49
59
  CommonsApprovalInfo,
60
+ CommonsApprovalSubjectAccount,
50
61
  CommonsSignInActionResult,
62
+ CommonsDenyReason,
63
+ CommonsOAuthFinalizeResult,
64
+ CommonsDeliveryResult,
51
65
  } from './mixins/OxyServices.auth';
66
+ // Push-token registration (Expo push tokens — never raw APNs/FCM device tokens).
67
+ export type {
68
+ PushTokenPlatform,
69
+ RegisterPushTokenInput,
70
+ } from './mixins/OxyServices.notifications';
52
71
  export type { ServiceApp, ServiceActingAsVerification } from './mixins/OxyServices.utility';
53
72
  export type {
54
73
  ContactDiscoveryMatch,
@@ -581,10 +600,12 @@ export {
581
600
  OXY_AUTHORIZE_URL,
582
601
  OXY_OAUTH_STATE_STORAGE_KEY,
583
602
  OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY,
603
+ OXY_OAUTH_REDIRECT_URI_STORAGE_KEY,
584
604
  OXY_SILENT_OAUTH_ATTEMPTED_KEY,
585
605
  OXY_CROSS_ORIGIN_RESTORE_ATTEMPTED_KEY,
586
606
  OXY_OAUTH_RETURN_PATH_STORAGE_KEY,
587
607
  normalizeOAuthRedirectUri,
608
+ canonicalizeOAuthRedirectUri,
588
609
  persistOAuthHandshake,
589
610
  readOAuthHandshake,
590
611
  clearOAuthHandshake,
@@ -666,6 +687,7 @@ export type {
666
687
  PopupWindowHandle,
667
688
  SignInFlowPhase,
668
689
  SignInFlowState,
690
+ SignInProgress,
669
691
  } from './session/accountDialogController';
670
692
 
671
693
  // ---------------------------------------------------------------------------
@@ -687,6 +709,32 @@ export type {
687
709
  NativeKeyValueStorage,
688
710
  } from './session/authStateStore';
689
711
 
712
+ // Identity-bound sessions (the identity vault). The pin is the durable
713
+ // `{publicKey, accountId}` binding between this device's PRIMARY identity key
714
+ // and the account it authenticates as; it is what keeps such a client from
715
+ // following the device's mutable `activeAccountId`.
716
+ export {
717
+ createWebIdentityPinStore,
718
+ createNativeIdentityPinStore,
719
+ createMemoryIdentityPinStore,
720
+ identityPinMatches,
721
+ IDENTITY_PIN_STORAGE_KEY,
722
+ } from './session/identityPin';
723
+ export type { IdentityPin, IdentityPinStore } from './session/identityPin';
724
+ export {
725
+ resolveIdentityPin,
726
+ establishIdentitySession,
727
+ } from './session/identitySession';
728
+ export type {
729
+ IdentityBinding,
730
+ IdentityRequestOptions,
731
+ EstablishedIdentitySession,
732
+ } from './session/identitySession';
733
+ // The typed `401 account_not_on_device` from a PINNED device-secret mint: the
734
+ // pinned account left this device's session set, so the caller re-establishes
735
+ // the identity session instead of dropping a healthy device credential.
736
+ export { AccountNotOnDeviceError } from './mixins/OxyServices.deviceBoot';
737
+
690
738
  export {
691
739
  refreshPersistedSession,
692
740
  refreshDeviceSecretArm,
@@ -700,6 +748,7 @@ export type { RefreshDeps, TokenRefreshSchedulerHandle, DeviceSecretMintOutcome
700
748
  export { runSessionColdBoot } from './boot/sessionColdBoot';
701
749
  export type {
702
750
  RunSessionColdBootOptions,
751
+ SessionMode,
703
752
  SignedOutReason,
704
753
  DeviceBootSession,
705
754
  } from './boot/sessionColdBoot';