@oxy.so/core 1.8.0 → 1.9.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.
@@ -13,8 +13,23 @@
13
13
  * 2. Node — backed by the built-in `node:crypto` module (`webcrypto`, else
14
14
  * `randomFillSync`). This is what a Node runtime WITHOUT a global WebCrypto
15
15
  * (Node 18 script entrypoints, some embedded hosts) falls back to.
16
- * 3. React Native — `expo-crypto.getRandomBytes` (statically imported via the
17
- * per-platform `platform/crypto` module in `@oxy.so/protocol`).
16
+ * 3. React Native — `expo-crypto.getRandomBytes`, through the dependency-free
17
+ * `@oxy.so/protocol/random` entry.
18
+ *
19
+ * # Evaluation order — this module must import NOTHING that can capture the global
20
+ *
21
+ * `@noble/hashes` 1.x reads `globalThis.crypto` exactly once, when its
22
+ * `crypto.js` is evaluated, and `randomBytes` uses that captured binding for
23
+ * the lifetime of the app. ES imports are evaluated before the importing
24
+ * module's body, so every module this file imports runs BEFORE the shim below
25
+ * is installed. This file used to import `@oxy.so/protocol`'s root entry,
26
+ * which reaches `@noble/curves` → `@noble/hashes` through the envelope signer:
27
+ * on Hermes (no `globalThis.crypto` at startup) noble captured `undefined`
28
+ * before the shim existed, and every identity creation on Android failed with
29
+ * `crypto.getRandomValues must be defined`. Its imports are therefore limited
30
+ * to `buffer` and `@oxy.so/protocol/random`, both free of crypto libraries,
31
+ * and `index.ts` imports this file first. `__tests__/polyfillOrder.test.ts`
32
+ * guards both halves.
18
33
  *
19
34
  * Historically step (2) delegated to `@oxy.so/protocol`'s RN-only
20
35
  * `getRandomBytesRN`, which THROWS on Node — so any Node host lacking a global
@@ -23,7 +38,7 @@
23
38
  */
24
39
  import _cjs_buffer from 'buffer';
25
40
  const { Buffer } = _cjs_buffer;
26
- import { getRandomBytesRN, isNodeJS } from '@oxy.so/protocol';
41
+ import { getRandomBytesRN, isNodeJS } from '@oxy.so/protocol/random';
27
42
  const getGlobalObject = () => {
28
43
  if (typeof globalThis !== 'undefined')
29
44
  return globalThis;
@@ -112,7 +127,7 @@ const cryptoPolyfill = {
112
127
  return array;
113
128
  }
114
129
  // React Native (and any non-Node host without WebCrypto): synchronous
115
- // expo-crypto via @oxy.so/protocol's RN `platform/crypto` variant.
130
+ // expo-crypto via @oxy.so/protocol's RN `platform/random` variant.
116
131
  const bytes = getRandomBytesRN(array.byteLength);
117
132
  const uint8View = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
118
133
  uint8View.set(bytes);
@@ -1850,6 +1850,7 @@
1850
1850
  "switch": {
1851
1851
  "title": "Switch account",
1852
1852
  "count": "{{count}} accounts",
1853
+ "countOne": "{{count}} account",
1853
1854
  "empty": "Accounts you own or share"
1854
1855
  }
1855
1856
  },
@@ -1970,7 +1971,8 @@
1970
1971
  "loading": "Loading sessions…",
1971
1972
  "empty": "No active sessions",
1972
1973
  "signOutAllOnThisDevice": "Sign out of all other devices",
1973
- "signOutAllSubtitle": "End {{count}} other device session(s)"
1974
+ "signOutAllSubtitle": "End {{count}} other device session(s)",
1975
+ "otherSession": "Another session"
1974
1976
  },
1975
1977
  "confirms": {
1976
1978
  "removeDeviceTitle": "Remove device",
@@ -1850,6 +1850,7 @@
1850
1850
  "switch": {
1851
1851
  "title": "Cambiar de cuenta",
1852
1852
  "count": "{{count}} cuentas",
1853
+ "countOne": "{{count}} cuenta",
1853
1854
  "empty": "Cuentas que posees o compartes"
1854
1855
  }
1855
1856
  },
@@ -1970,7 +1971,8 @@
1970
1971
  "loading": "Cargando sesiones…",
1971
1972
  "empty": "No hay sesiones activas",
1972
1973
  "signOutAllOnThisDevice": "Cerrar sesión en los demás dispositivos",
1973
- "signOutAllSubtitle": "Finalizar {{count}} sesión(es) de otros dispositivos"
1974
+ "signOutAllSubtitle": "Finalizar {{count}} sesión(es) de otros dispositivos",
1975
+ "otherSession": "Otra sesión"
1974
1976
  },
1975
1977
  "confirms": {
1976
1978
  "removeDeviceTitle": "Eliminar dispositivo",
@@ -1850,6 +1850,7 @@
1850
1850
  "switch": {
1851
1851
  "title": "Switch account",
1852
1852
  "count": "{{count}} accounts",
1853
+ "countOne": "{{count}} account",
1853
1854
  "empty": "Accounts you own or share"
1854
1855
  }
1855
1856
  },
@@ -1970,7 +1971,8 @@
1970
1971
  "loading": "Loading sessions…",
1971
1972
  "empty": "No active sessions",
1972
1973
  "signOutAllOnThisDevice": "Sign out of all other devices",
1973
- "signOutAllSubtitle": "End {{count}} other device session(s)"
1974
+ "signOutAllSubtitle": "End {{count}} other device session(s)",
1975
+ "otherSession": "Another session"
1974
1976
  },
1975
1977
  "confirms": {
1976
1978
  "removeDeviceTitle": "Remove device",
@@ -1850,6 +1850,7 @@
1850
1850
  "switch": {
1851
1851
  "title": "Cambiar de cuenta",
1852
1852
  "count": "{{count}} cuentas",
1853
+ "countOne": "{{count}} cuenta",
1853
1854
  "empty": "Cuentas que posees o compartes"
1854
1855
  }
1855
1856
  },
@@ -1970,7 +1971,8 @@
1970
1971
  "loading": "Cargando sesiones…",
1971
1972
  "empty": "No hay sesiones activas",
1972
1973
  "signOutAllOnThisDevice": "Cerrar sesión en los demás dispositivos",
1973
- "signOutAllSubtitle": "Finalizar {{count}} sesión(es) de otros dispositivos"
1974
+ "signOutAllSubtitle": "Finalizar {{count}} sesión(es) de otros dispositivos",
1975
+ "otherSession": "Otra sesión"
1974
1976
  },
1975
1977
  "confirms": {
1976
1978
  "removeDeviceTitle": "Eliminar dispositivo",
package/dist/esm/index.js CHANGED
@@ -16,7 +16,10 @@
16
16
  * Every export below is NOMINAL — no `export *`, no barrels, no compat shims.
17
17
  * If a symbol does not appear here, it is NOT part of the public API.
18
18
  */
19
- // Ensure crypto polyfills are loaded before anything else
19
+ // Ensure crypto polyfills are loaded before anything else. This MUST stay the
20
+ // first import: `@noble/hashes` captures `globalThis.crypto` when it is
21
+ // evaluated, so the shim has to exist before any module below reaches it (see
22
+ // the evaluation-order note in `./crypto/polyfill`).
20
23
  import './crypto/polyfill.js';
21
24
  // ---------------------------------------------------------------------------
22
25
  // API client
@@ -329,16 +329,25 @@ export class AccountDialogController {
329
329
  }
330
330
  // Signed out. The directory is bearer-read and `SessionClient` drops the one
331
331
  // it holds when the device empties, so there is nothing to clear here — only
332
- // the in-flight bookkeeping, which no longer describes anything.
332
+ // the in-flight bookkeeping, which no longer describes anything. The account
333
+ // menu described the account that just left, so it gives way to the entry.
333
334
  this.error = null;
334
335
  this.loading = false;
336
+ this.view = this.resolveView(this.view);
335
337
  this.emit();
336
338
  }
337
339
  // =========================================================================
338
340
  // View actions
339
341
  // =========================================================================
340
- /** Set the dialog view directly. */
341
- setView(view) {
342
+ /**
343
+ * Set the dialog view directly.
344
+ *
345
+ * `'accounts'` is only honoured with a session. Signed out it lands on the
346
+ * sign-in entry instead — the menu it would open holds "Switch account",
347
+ * storage and "Sign out" for an account that does not exist.
348
+ */
349
+ setView(requested) {
350
+ const view = this.resolveView(requested);
342
351
  if (this.view === view)
343
352
  return;
344
353
  this.view = view;
@@ -351,6 +360,37 @@ export class AccountDialogController {
351
360
  }
352
361
  this.emit();
353
362
  }
363
+ /**
364
+ * Go back one step: `signup` and `qr` return to the sign-in entry, and the
365
+ * entry returns to the account menu when somebody is signed in.
366
+ *
367
+ * Returns `false` — changing nothing — when the current view is the dialog's
368
+ * first one, so the host closes the dialog instead. Leaving an active
369
+ * request withdraws it; otherwise its authorize code stays approvable until
370
+ * it expires.
371
+ */
372
+ back() {
373
+ const target = backViewOf(this.view, this.hasSession());
374
+ if (target === null)
375
+ return false;
376
+ if (this.signIn.phase === 'starting' || this.signIn.phase === 'waiting') {
377
+ this.cancelSignIn();
378
+ }
379
+ this.setView(target);
380
+ return true;
381
+ }
382
+ /** `'accounts'` needs a session; without one the sign-in entry stands in for it. */
383
+ resolveView(view) {
384
+ return view === 'accounts' && !this.hasSession() ? 'signin' : view;
385
+ }
386
+ /**
387
+ * Whether an account is signed in here — the precondition of the account
388
+ * menu. The bearer, not the directory: the directory is bearer-read, and the
389
+ * menu's content (hero, storage, sign-out) is all about the bearer's account.
390
+ */
391
+ hasSession() {
392
+ return this.isAuthenticated();
393
+ }
354
394
  /** Switch to the "add account" view (the sign-in entry chooser). */
355
395
  add() {
356
396
  this.setView('add');
@@ -1285,8 +1325,13 @@ export class AccountDialogController {
1285
1325
  }
1286
1326
  computeSnapshot() {
1287
1327
  const directory = this.sessionClient.getDirectory();
1328
+ const hasSession = this.hasSession();
1329
+ // Resolved here as well as in `setView`: the controller is constructed on
1330
+ // `'accounts'` before anyone knows whether a bearer exists.
1331
+ const view = this.resolveView(this.view);
1288
1332
  return {
1289
- view: this.view,
1333
+ view,
1334
+ backView: backViewOf(view, hasSession),
1290
1335
  directory,
1291
1336
  activeContext: resolveActiveContext(directory),
1292
1337
  loading: this.loading,
@@ -1311,6 +1356,29 @@ export class AccountDialogController {
1311
1356
  }
1312
1357
  }
1313
1358
  }
1359
+ /**
1360
+ * The view one step back from `view`, or `null` when `view` is the dialog's
1361
+ * first view.
1362
+ *
1363
+ * The sign-in entry is `'add'` ("add another account") when somebody is signed
1364
+ * in and `'signin'` when nobody is; `signup` and `qr` are reached FROM that
1365
+ * entry, so they return to it. The entry itself returns to the account menu —
1366
+ * which exists only with a session. This used to be a host-side
1367
+ * "back = accounts" that assumed a signed-in origin, so a signed-out Back from
1368
+ * "Create your account" opened a menu holding "Sign out" for nobody.
1369
+ */
1370
+ export function backViewOf(view, hasSession) {
1371
+ switch (view) {
1372
+ case 'signup':
1373
+ case 'qr':
1374
+ return hasSession ? 'add' : 'signin';
1375
+ case 'add':
1376
+ return hasSession ? 'accounts' : null;
1377
+ case 'signin':
1378
+ case 'accounts':
1379
+ return null;
1380
+ }
1381
+ }
1314
1382
  /** Factory mirroring `createSessionClient`, for ergonomic wiring by consumers. */
1315
1383
  export function createAccountDialogController(options) {
1316
1384
  return new AccountDialogController(options);