@estiva-app/identity 0.2.2 → 0.2.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @estiva-app/identity
2
2
 
3
+ ## 0.2.3 — 2026-10-13
4
+
5
+ **Additive.** A caller that passes `storage` behaves exactly as before, apart
6
+ from renewal now being locked across tabs.
7
+
8
+ - **Renewal is locked across tabs (PEE-3).** A session in `localStorage` is read
9
+ by every tab, and every tab's renewal timer fires for the same expiry; the
10
+ per-page guard from 0.2.0 does not see the other tabs. Estiva ID revokes the
11
+ whole refresh chain when one refresh token is presented twice, so two open
12
+ tabs could sign the person out of everything. Ship and Leaf have shared their
13
+ session since they adopted the package and were exposed to this.
14
+
15
+ `refreshAccessToken` now runs under `lock` (new, optional; the Web Locks API
16
+ by default, unguarded where there is none). A tab that waited for the lock and
17
+ finds a different refresh token stored takes that token instead of calling
18
+ `/token`. A `scheduleRenewal` timer that fires after another tab renewed
19
+ re-arms from the shared token's expiry rather than renewing again.
20
+
21
+ - **`storage` is optional, and defaults to `localStorage`; `pendingStore` then
22
+ defaults to `sessionStorage`.** Every app keeps its session the same way:
23
+ shared by its tabs, the in-flight PKCE values per tab. Peek moves to it with
24
+ PEE-3. Passing `storage` alone still uses it for both.
25
+
3
26
  ## 0.2.2 — 2026-09-08
4
27
 
5
28
  **Additive.** `token` gains a function form; a string behaves exactly as before,
package/README.md CHANGED
@@ -39,15 +39,21 @@ package come out Peek-shaped, which is the whole risk SHA-4 names.
39
39
  | --- | --- | --- |
40
40
  | `clientId` | `estiva-peek` | `estiva-ship` |
41
41
  | `redirectUri` | a fixed `/auth/callback` | its current pathname — Ship routes on `location.hash` |
42
- | `storage` — the session | `sessionStorage`, dies with the tab | `localStorage`, outlives a tab |
43
- | `pendingStore` — in-flight PKCE credentials | defaults to `storage` | `sessionStorage`, because the flow starts and ends in one tab |
44
42
  | `keyPrefix` | `peek.estivaId` | `ship.estiva-id` |
45
43
  | `navigate` | the one genuinely untestable act, so it is observable | |
46
44
  | `setTimer` / `clearTimer` | the timers `scheduleRenewal` runs on, injected for the same reason | |
47
45
 
48
- **Do not unify the two stores.** NIP-RS read-state slots (CRO-4) must survive a
49
- restart and belong in `localStorage`; an access token does not. Different
50
- lifetimes, different homes.
46
+ **Where the session lives is the same for every app (PEE-3).** `storage` defaults
47
+ to `localStorage` — one session shared by every tab, surviving a tab close — and
48
+ `pendingStore` to `sessionStorage`, because a sign-in starts and ends in one tab.
49
+ Peek kept a per-tab session until PEE-3, and every new tab paid a ~0.5 s silent
50
+ `/authorize` bounce for it. Both stay injectable (a test, a script); a caller
51
+ that passes only `storage` gets it for both, as before.
52
+
53
+ **A shared session needs a cross-tab lock on renewal.** Every tab's timer fires
54
+ for the same expiry, and Estiva ID revokes the whole chain when a refresh token
55
+ is presented twice. `lock` defaults to the Web Locks API; a tab that waited for
56
+ it takes the token another tab just renewed instead of spending the old one.
51
57
 
52
58
  `pendingStore` exists because wiring the second consumer found the package
53
59
  assuming one store — it was extracted from Peek, which uses one. That is the "a
@@ -62,7 +68,6 @@ const client = createEstivaId({
62
68
  base: ESTIVA_ID_ORIGIN,
63
69
  clientId: 'estiva-peek',
64
70
  redirectUri: () => `${window.location.origin}/auth/callback`,
65
- storage: () => (typeof window === 'undefined' ? null : window.sessionStorage),
66
71
  keyPrefix: 'peek.estivaId',
67
72
  navigate: (url) => window.location.assign(url),
68
73
  })
package/dist/client.d.ts CHANGED
@@ -38,14 +38,14 @@
38
38
  * `app_credentials` with separate `redirectUris` and separate kind ceilings.
39
39
  * - **`redirectUri`** — Peek registers a fixed `/auth/callback`; Ship routes on
40
40
  * `location.hash` and registers its current pathname. Neither is more correct.
41
- * - **`storage`** and **`pendingStore`** — two stores, because Ship needs two.
42
- * Peek keeps everything in `sessionStorage`, which dies with the tab and keeps
43
- * a bearer token off a shared machine's disk. Ship keeps its *session* in
44
- * `localStorage` so it outlives a tab, and its *in-flight PKCE credentials* in
45
- * `sessionStorage` because the flow starts and ends in one tab. The package
46
- * assumed one store until the second consumer was wired — see `pendingStore`.
47
- * **Do not unify them**: NIP-RS slots (CRO-4) must survive a restart and belong
48
- * in `localStorage`, while an access token does not.
41
+ * - **`storage`** and **`pendingStore`** — two stores, because the flow needs
42
+ * two. The *session* lives in `localStorage` so every tab shares it and it
43
+ * outlives a tab; the *in-flight PKCE credentials* live in `sessionStorage`
44
+ * because the flow starts and ends in one tab. That is the default since
45
+ * PEE-3, when Peek gave up its per-tab session (a new tab paid a ~0.5 s
46
+ * `/authorize` bounce) and all three apps came to choose the same. Both stay
47
+ * injectable — a test, a script or a future "public computer" mode may differ.
48
+ * A shared session is why renewal takes a cross-tab {@link EstivaIdConfig.lock}.
49
49
  * - **`keyPrefix`** — so two apps on one origin cannot read each other's keys.
50
50
  * - **`navigate`** — the one genuinely untestable act. Injected so a test can
51
51
  * observe where the flow *would* go.
@@ -74,21 +74,22 @@ export interface EstivaIdConfig {
74
74
  /**
75
75
  * Where the **session** lives — the token and nothing else.
76
76
  *
77
- * Peek uses `sessionStorage`: it dies with the tab, which keeps a bearer token
78
- * off a shared machine's disk. Ship uses `localStorage` so a session survives
79
- * a tab close. Returns null outside a browser.
77
+ * Defaults to `localStorage`: every tab shares one session and it survives a
78
+ * tab close (PEE-3). Returns null outside a browser.
80
79
  */
81
- storage: () => KeyValueStore | null;
80
+ storage?: () => KeyValueStore | null;
82
81
  /**
83
82
  * Where an **in-flight sign-in's** single-use credentials live — the verifier,
84
83
  * the state, the returnTo, the shell reason and the silent-attempt guard.
85
84
  *
86
- * Defaults to {@link storage}, which is what Peek wants: one store for both.
85
+ * Defaults to `sessionStorage` when {@link storage} is defaulted too, and to
86
+ * {@link storage} when it is given — so a caller passing one store keeps
87
+ * exactly the behaviour it had before the defaults existed.
87
88
  *
88
- * **Ship needs them separated, and that is why this exists.** Its session
89
- * belongs in `localStorage` so it outlives a tab, while the PKCE credentials
90
- * are per-tab by nature — the flow starts and ends in one tab, and the guard's
91
- * whole point is that a genuinely new tab is entitled to a fresh attempt.
89
+ * **The two are separate because the flow needs it.** The session belongs in
90
+ * `localStorage` so it outlives a tab, while the PKCE credentials are per-tab
91
+ * by nature — the flow starts and ends in one tab, and the guard's whole point
92
+ * is that a genuinely new tab is entitled to a fresh attempt.
92
93
  *
93
94
  * This parameter was added by wiring the second consumer. The package had
94
95
  * assumed one store because it was extracted from Peek, which uses one: the
@@ -110,6 +111,18 @@ export interface EstivaIdConfig {
110
111
  */
111
112
  setTimer?: (run: () => void, ms: number) => unknown;
112
113
  clearTimer?: (handle: unknown) => void;
114
+ /**
115
+ * Runs `run` while holding the lock `name` against every other tab of this
116
+ * origin (PEE-3). Renewal holds it, because a shared session means every
117
+ * tab's timer fires at the same moment, and Estiva ID revokes the whole chain
118
+ * when one refresh token is presented twice — two tabs renewing together
119
+ * would sign the person out of all of them.
120
+ *
121
+ * Defaults to the Web Locks API (`navigator.locks.request`), and to running
122
+ * `run` unguarded where there is none — a single-page runtime has no other
123
+ * tab to race.
124
+ */
125
+ lock?: <T>(name: string, run: () => Promise<T>) => Promise<T>;
113
126
  }
114
127
  export interface StoredToken {
115
128
  accessToken: string;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js CHANGED
@@ -38,30 +38,50 @@
38
38
  * `app_credentials` with separate `redirectUris` and separate kind ceilings.
39
39
  * - **`redirectUri`** — Peek registers a fixed `/auth/callback`; Ship routes on
40
40
  * `location.hash` and registers its current pathname. Neither is more correct.
41
- * - **`storage`** and **`pendingStore`** — two stores, because Ship needs two.
42
- * Peek keeps everything in `sessionStorage`, which dies with the tab and keeps
43
- * a bearer token off a shared machine's disk. Ship keeps its *session* in
44
- * `localStorage` so it outlives a tab, and its *in-flight PKCE credentials* in
45
- * `sessionStorage` because the flow starts and ends in one tab. The package
46
- * assumed one store until the second consumer was wired — see `pendingStore`.
47
- * **Do not unify them**: NIP-RS slots (CRO-4) must survive a restart and belong
48
- * in `localStorage`, while an access token does not.
41
+ * - **`storage`** and **`pendingStore`** — two stores, because the flow needs
42
+ * two. The *session* lives in `localStorage` so every tab shares it and it
43
+ * outlives a tab; the *in-flight PKCE credentials* live in `sessionStorage`
44
+ * because the flow starts and ends in one tab. That is the default since
45
+ * PEE-3, when Peek gave up its per-tab session (a new tab paid a ~0.5 s
46
+ * `/authorize` bounce) and all three apps came to choose the same. Both stay
47
+ * injectable — a test, a script or a future "public computer" mode may differ.
48
+ * A shared session is why renewal takes a cross-tab {@link EstivaIdConfig.lock}.
49
49
  * - **`keyPrefix`** — so two apps on one origin cannot read each other's keys.
50
50
  * - **`navigate`** — the one genuinely untestable act. Injected so a test can
51
51
  * observe where the flow *would* go.
52
52
  */
53
53
  import { clearGuard, markGuard } from './shell.js';
54
+ /** A browser store, or null where there is none or reading it throws (a sandboxed frame). */
55
+ function webStore(which) {
56
+ try {
57
+ if (which === 'local')
58
+ return typeof localStorage === 'undefined' ? null : (localStorage ?? null);
59
+ return typeof sessionStorage === 'undefined' ? null : (sessionStorage ?? null);
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ function webLock(name, run) {
66
+ const locks = typeof navigator === 'undefined' ? undefined : navigator?.locks;
67
+ return locks ? locks.request(name, run) : run();
68
+ }
54
69
  /**
55
70
  * `setTimeout`'s ceiling. Above 2^31-1 ms the delay overflows to a negative
56
71
  * int32 and the timer fires immediately.
57
72
  */
58
73
  const MAX_TIMER_MS = 2_147_483_647;
74
+ /** How long renewal keeps the cross-tab lock after writing, for the write to reach the other tabs (PEE-3). */
75
+ const RENEWAL_SETTLE_MS = 250;
59
76
  export class SignInError extends Error {
60
77
  }
61
78
  export function createEstivaId(config) {
62
79
  const now = config.now ?? (() => Date.now());
63
80
  /** In-flight credentials, which may live somewhere shorter than the session. */
64
- const pending = config.pendingStore ?? config.storage;
81
+ /** The session, shared by every tab unless the app says otherwise (PEE-3). */
82
+ const session = config.storage ?? (() => webStore('local'));
83
+ const pending = config.pendingStore ?? config.storage ?? (() => webStore('session'));
84
+ const lock = config.lock ?? webLock;
65
85
  const send = () => config.fetch ?? fetch;
66
86
  const go = (url) => {
67
87
  if (!config.navigate)
@@ -149,7 +169,7 @@ export function createEstivaId(config) {
149
169
  };
150
170
  };
151
171
  const storedToken = () => {
152
- const raw = config.storage()?.getItem(TOKEN_KEY);
172
+ const raw = session()?.getItem(TOKEN_KEY);
153
173
  if (!raw)
154
174
  return null;
155
175
  try {
@@ -186,7 +206,7 @@ export function createEstivaId(config) {
186
206
  * Anything that wants both says so, and only `beginSignOut` does.
187
207
  */
188
208
  const clearSession = () => {
189
- config.storage()?.removeItem(TOKEN_KEY);
209
+ session()?.removeItem(TOKEN_KEY);
190
210
  // The reason lives with the in-flight credentials, which may be a different
191
211
  // store — see `pendingStore`. Still cleared here: a reason outliving the
192
212
  // session it described is a lie, and that was true before the split.
@@ -246,7 +266,7 @@ export function createEstivaId(config) {
246
266
  * never presented twice — a replay is what Estiva ID treats as theft.
247
267
  */
248
268
  const performRefresh = async () => {
249
- const s = config.storage();
269
+ const s = session();
250
270
  const current = storedToken();
251
271
  if (!s || !current?.refreshToken)
252
272
  return null;
@@ -268,11 +288,22 @@ export function createEstivaId(config) {
268
288
  // not sign somebody out.
269
289
  return null;
270
290
  }
291
+ /*
292
+ The session is shared by every tab (PEE-3), and signing out takes no lock:
293
+ while `/token` was answering, another tab may have signed out — or in.
294
+ Then this answer belongs to a session that is gone, and writing it would
295
+ put a live token back after the sign-out, for its whole lifetime. Only
296
+ the session this renewal spent is replaced or cleared.
297
+ */
298
+ const stillOurs = () => storedToken()?.refreshToken === current.refreshToken;
271
299
  if (!response.ok) {
272
- clearSession();
300
+ if (stillOurs())
301
+ clearSession();
273
302
  return null;
274
303
  }
275
304
  const body = (await response.json());
305
+ if (!stillOurs())
306
+ return validToken();
276
307
  if (!body.access_token) {
277
308
  clearSession();
278
309
  return null;
@@ -299,12 +330,35 @@ export function createEstivaId(config) {
299
330
  *
300
331
  * Coalescing rather than caching: the promise is dropped the moment it
301
332
  * settles, so the *next* expiry renews again.
333
+ *
334
+ * **And never across tabs either (PEE-3).** The promise guards one page; a
335
+ * session in `localStorage` is shared by every tab, and each tab's timer
336
+ * fires at the same moment, for the same expiry. So the renewal runs under
337
+ * {@link EstivaIdConfig.lock}, and a tab that waited for it compares the
338
+ * stored refresh token with the one it set out to spend: if another tab
339
+ * renewed (or signed out, or in) meanwhile, that tab's answer is taken and
340
+ * `/token` is not called — presenting the spent token would be the replay.
302
341
  */
303
342
  let refreshing = null;
304
343
  const refreshAccessToken = () => {
305
344
  if (refreshing)
306
345
  return refreshing;
307
- refreshing = performRefresh().finally(() => {
346
+ const spending = storedToken()?.refreshToken;
347
+ refreshing = new Promise((answer, fail) => {
348
+ lock(`${config.keyPrefix}.renewal`, async () => {
349
+ if (storedToken()?.refreshToken !== spending)
350
+ return answer(validToken());
351
+ answer(await performRefresh());
352
+ /*
353
+ Held a little longer than the renewal, though the caller already has
354
+ its answer. A browser does not order one tab's `localStorage` write
355
+ with another tab's lock grant — the write reaches the other process's
356
+ copy shortly after — so a tab granted the lock at once could still
357
+ read the spent token and present it again.
358
+ */
359
+ await new Promise((settle) => setTimeout(settle, RENEWAL_SETTLE_MS));
360
+ }).catch(fail);
361
+ }).finally(() => {
308
362
  refreshing = null;
309
363
  });
310
364
  return refreshing;
@@ -406,7 +460,7 @@ export function createEstivaId(config) {
406
460
  */
407
461
  async completeSignIn(search) {
408
462
  const s = pending();
409
- const sessionStore = config.storage();
463
+ const sessionStore = session();
410
464
  if (!s || !sessionStore)
411
465
  throw new SignInError('sign-in requires a browser');
412
466
  const params = new URLSearchParams(search);
@@ -516,6 +570,14 @@ export function createEstivaId(config) {
516
570
  const delay = Math.min(Math.max(0, token.expiresAt - now()), MAX_TIMER_MS);
517
571
  handle = setTimer(() => {
518
572
  handle = null;
573
+ // Another tab sharing the session renewed first (PEE-3): follow its
574
+ // token rather than spend a grant this tab no longer needs.
575
+ const shared = storedToken();
576
+ if (shared && shared.expiresAt > token.expiresAt) {
577
+ handlers.onRenewed?.(shared);
578
+ arm();
579
+ return;
580
+ }
519
581
  void refreshAccessToken().then((next) => {
520
582
  if (cancelled)
521
583
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -24,10 +24,12 @@
24
24
  *
25
25
  * ## Every difference between the two consumers is a parameter
26
26
  *
27
- * `clientId`, `redirectUri`, `storage`, `keyPrefix` and `navigate` are all
28
- * injected, because Peek and Ship genuinely differ on every one of them and
29
- * hardcoding any would have made the package come out Peek-shaped. See
30
- * `client.ts` for what each difference actually is.
27
+ * `clientId`, `redirectUri`, `keyPrefix` and `navigate` are all injected,
28
+ * because Peek and Ship genuinely differ on every one of them and hardcoding
29
+ * any would have made the package come out Peek-shaped. `storage` and
30
+ * `pendingStore` are injectable too, but since PEE-3 every app takes the same
31
+ * defaults (`localStorage` for the session, `sessionStorage` for a sign-in in
32
+ * flight). See `client.ts` for what each difference actually is.
31
33
  *
32
34
  * ## The globals prediction this package falsified
33
35
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,eAAO,MAAM,gBAAgB,UAAU,CAAA;AAEvC,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,aAAa,EACb,UAAU,EACV,cAAc,EACd,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,UAAU,EACV,sBAAsB,EACtB,SAAS,EACT,SAAS,EACT,UAAU,EACV,aAAa,GACd,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,WAAW,EACX,cAAc,GACf,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EAClB,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,cAAc,GACf,MAAM,aAAa,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,eAAO,MAAM,gBAAgB,UAAU,CAAA;AAEvC,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,aAAa,EACb,UAAU,EACV,cAAc,EACd,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,UAAU,EACV,sBAAsB,EACtB,SAAS,EACT,SAAS,EACT,UAAU,EACV,aAAa,GACd,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,WAAW,EACX,cAAc,GACf,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EAClB,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,cAAc,GACf,MAAM,aAAa,CAAA"}
package/dist/index.js CHANGED
@@ -24,10 +24,12 @@
24
24
  *
25
25
  * ## Every difference between the two consumers is a parameter
26
26
  *
27
- * `clientId`, `redirectUri`, `storage`, `keyPrefix` and `navigate` are all
28
- * injected, because Peek and Ship genuinely differ on every one of them and
29
- * hardcoding any would have made the package come out Peek-shaped. See
30
- * `client.ts` for what each difference actually is.
27
+ * `clientId`, `redirectUri`, `keyPrefix` and `navigate` are all injected,
28
+ * because Peek and Ship genuinely differ on every one of them and hardcoding
29
+ * any would have made the package come out Peek-shaped. `storage` and
30
+ * `pendingStore` are injectable too, but since PEE-3 every app takes the same
31
+ * defaults (`localStorage` for the session, `sessionStorage` for a sign-in in
32
+ * flight). See `client.ts` for what each difference actually is.
31
33
  *
32
34
  * ## The globals prediction this package falsified
33
35
  *
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAA;AAEvC,OAAO,EAQL,aAAa,EACb,UAAU,EACV,cAAc,EACd,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,UAAU,EACV,sBAAsB,EACtB,SAAS,EACT,SAAS,EACT,UAAU,EACV,aAAa,GACd,MAAM,YAAY,CAAA;AAEnB,OAAO,EAOL,WAAW,EACX,cAAc,GACf,MAAM,aAAa,CAAA;AAEpB,OAAO,EAIL,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,cAAc,GACf,MAAM,aAAa,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAA;AAEvC,OAAO,EAQL,aAAa,EACb,UAAU,EACV,cAAc,EACd,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,UAAU,EACV,sBAAsB,EACtB,SAAS,EACT,SAAS,EACT,UAAU,EACV,aAAa,GACd,MAAM,YAAY,CAAA;AAEnB,OAAO,EAOL,WAAW,EACX,cAAc,GACf,MAAM,aAAa,CAAA;AAEpB,OAAO,EAIL,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,cAAc,GACf,MAAM,aAAa,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estiva-app/identity",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Estiva ID sign-in: authorization code + PKCE, token storage, silent renewal, sign-out, signing via POST /sign, and the auth-shell state machine. Knows nothing about any app's data layer.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/client.ts CHANGED
@@ -38,14 +38,14 @@
38
38
  * `app_credentials` with separate `redirectUris` and separate kind ceilings.
39
39
  * - **`redirectUri`** — Peek registers a fixed `/auth/callback`; Ship routes on
40
40
  * `location.hash` and registers its current pathname. Neither is more correct.
41
- * - **`storage`** and **`pendingStore`** — two stores, because Ship needs two.
42
- * Peek keeps everything in `sessionStorage`, which dies with the tab and keeps
43
- * a bearer token off a shared machine's disk. Ship keeps its *session* in
44
- * `localStorage` so it outlives a tab, and its *in-flight PKCE credentials* in
45
- * `sessionStorage` because the flow starts and ends in one tab. The package
46
- * assumed one store until the second consumer was wired — see `pendingStore`.
47
- * **Do not unify them**: NIP-RS slots (CRO-4) must survive a restart and belong
48
- * in `localStorage`, while an access token does not.
41
+ * - **`storage`** and **`pendingStore`** — two stores, because the flow needs
42
+ * two. The *session* lives in `localStorage` so every tab shares it and it
43
+ * outlives a tab; the *in-flight PKCE credentials* live in `sessionStorage`
44
+ * because the flow starts and ends in one tab. That is the default since
45
+ * PEE-3, when Peek gave up its per-tab session (a new tab paid a ~0.5 s
46
+ * `/authorize` bounce) and all three apps came to choose the same. Both stay
47
+ * injectable — a test, a script or a future "public computer" mode may differ.
48
+ * A shared session is why renewal takes a cross-tab {@link EstivaIdConfig.lock}.
49
49
  * - **`keyPrefix`** — so two apps on one origin cannot read each other's keys.
50
50
  * - **`navigate`** — the one genuinely untestable act. Injected so a test can
51
51
  * observe where the flow *would* go.
@@ -96,6 +96,32 @@ export type FetchLike = (
96
96
 
97
97
  declare const fetch: FetchLike
98
98
 
99
+ /**
100
+ * The defaults for `storage`, `pendingStore` and `lock`. Read through `typeof`
101
+ * inside functions, so a runtime without them — Node, a worker — gets null or
102
+ * the unguarded path rather than a ReferenceError.
103
+ */
104
+ declare const localStorage: KeyValueStore | undefined
105
+ declare const sessionStorage: KeyValueStore | undefined
106
+ declare const navigator:
107
+ | { locks?: { request<T>(name: string, run: () => Promise<T>): Promise<T> } }
108
+ | undefined
109
+
110
+ /** A browser store, or null where there is none or reading it throws (a sandboxed frame). */
111
+ function webStore(which: 'local' | 'session'): KeyValueStore | null {
112
+ try {
113
+ if (which === 'local') return typeof localStorage === 'undefined' ? null : (localStorage ?? null)
114
+ return typeof sessionStorage === 'undefined' ? null : (sessionStorage ?? null)
115
+ } catch {
116
+ return null
117
+ }
118
+ }
119
+
120
+ function webLock<T>(name: string, run: () => Promise<T>): Promise<T> {
121
+ const locks = typeof navigator === 'undefined' ? undefined : navigator?.locks
122
+ return locks ? locks.request(name, run) : run()
123
+ }
124
+
99
125
  export interface EstivaIdConfig {
100
126
  /** Origin of the identity service, e.g. `https://id.estiva.app`. */
101
127
  base: string
@@ -109,21 +135,22 @@ export interface EstivaIdConfig {
109
135
  /**
110
136
  * Where the **session** lives — the token and nothing else.
111
137
  *
112
- * Peek uses `sessionStorage`: it dies with the tab, which keeps a bearer token
113
- * off a shared machine's disk. Ship uses `localStorage` so a session survives
114
- * a tab close. Returns null outside a browser.
138
+ * Defaults to `localStorage`: every tab shares one session and it survives a
139
+ * tab close (PEE-3). Returns null outside a browser.
115
140
  */
116
- storage: () => KeyValueStore | null
141
+ storage?: () => KeyValueStore | null
117
142
  /**
118
143
  * Where an **in-flight sign-in's** single-use credentials live — the verifier,
119
144
  * the state, the returnTo, the shell reason and the silent-attempt guard.
120
145
  *
121
- * Defaults to {@link storage}, which is what Peek wants: one store for both.
146
+ * Defaults to `sessionStorage` when {@link storage} is defaulted too, and to
147
+ * {@link storage} when it is given — so a caller passing one store keeps
148
+ * exactly the behaviour it had before the defaults existed.
122
149
  *
123
- * **Ship needs them separated, and that is why this exists.** Its session
124
- * belongs in `localStorage` so it outlives a tab, while the PKCE credentials
125
- * are per-tab by nature — the flow starts and ends in one tab, and the guard's
126
- * whole point is that a genuinely new tab is entitled to a fresh attempt.
150
+ * **The two are separate because the flow needs it.** The session belongs in
151
+ * `localStorage` so it outlives a tab, while the PKCE credentials are per-tab
152
+ * by nature — the flow starts and ends in one tab, and the guard's whole point
153
+ * is that a genuinely new tab is entitled to a fresh attempt.
127
154
  *
128
155
  * This parameter was added by wiring the second consumer. The package had
129
156
  * assumed one store because it was extracted from Peek, which uses one: the
@@ -145,6 +172,18 @@ export interface EstivaIdConfig {
145
172
  */
146
173
  setTimer?: (run: () => void, ms: number) => unknown
147
174
  clearTimer?: (handle: unknown) => void
175
+ /**
176
+ * Runs `run` while holding the lock `name` against every other tab of this
177
+ * origin (PEE-3). Renewal holds it, because a shared session means every
178
+ * tab's timer fires at the same moment, and Estiva ID revokes the whole chain
179
+ * when one refresh token is presented twice — two tabs renewing together
180
+ * would sign the person out of all of them.
181
+ *
182
+ * Defaults to the Web Locks API (`navigator.locks.request`), and to running
183
+ * `run` unguarded where there is none — a single-page runtime has no other
184
+ * tab to race.
185
+ */
186
+ lock?: <T>(name: string, run: () => Promise<T>) => Promise<T>
148
187
  }
149
188
 
150
189
  export interface StoredToken {
@@ -172,6 +211,9 @@ export interface StoredToken {
172
211
  */
173
212
  const MAX_TIMER_MS = 2_147_483_647
174
213
 
214
+ /** How long renewal keeps the cross-tab lock after writing, for the write to reach the other tabs (PEE-3). */
215
+ const RENEWAL_SETTLE_MS = 250
216
+
175
217
  export class SignInError extends Error {}
176
218
 
177
219
  /** What a scheduled renewal reports back. Both optional; neither is required to be useful. */
@@ -225,7 +267,10 @@ export interface EstivaIdClient {
225
267
  export function createEstivaId(config: EstivaIdConfig): EstivaIdClient {
226
268
  const now = config.now ?? (() => Date.now())
227
269
  /** In-flight credentials, which may live somewhere shorter than the session. */
228
- const pending = config.pendingStore ?? config.storage
270
+ /** The session, shared by every tab unless the app says otherwise (PEE-3). */
271
+ const session = config.storage ?? (() => webStore('local'))
272
+ const pending = config.pendingStore ?? config.storage ?? (() => webStore('session'))
273
+ const lock = config.lock ?? webLock
229
274
  const send = () => config.fetch ?? fetch
230
275
  const go = (url: string) => {
231
276
  if (!config.navigate) throw new Error('createEstivaId: navigate is required to leave for Estiva ID')
@@ -316,7 +361,7 @@ export function createEstivaId(config: EstivaIdConfig): EstivaIdClient {
316
361
  }
317
362
 
318
363
  const storedToken = (): StoredToken | null => {
319
- const raw = config.storage()?.getItem(TOKEN_KEY)
364
+ const raw = session()?.getItem(TOKEN_KEY)
320
365
  if (!raw) return null
321
366
  try {
322
367
  const parsed = JSON.parse(raw) as StoredToken
@@ -353,7 +398,7 @@ export function createEstivaId(config: EstivaIdConfig): EstivaIdClient {
353
398
  * Anything that wants both says so, and only `beginSignOut` does.
354
399
  */
355
400
  const clearSession = (): void => {
356
- config.storage()?.removeItem(TOKEN_KEY)
401
+ session()?.removeItem(TOKEN_KEY)
357
402
  // The reason lives with the in-flight credentials, which may be a different
358
403
  // store — see `pendingStore`. Still cleared here: a reason outliving the
359
404
  // session it described is a lie, and that was true before the split.
@@ -414,7 +459,7 @@ export function createEstivaId(config: EstivaIdConfig): EstivaIdClient {
414
459
  * never presented twice — a replay is what Estiva ID treats as theft.
415
460
  */
416
461
  const performRefresh = async (): Promise<StoredToken | null> => {
417
- const s = config.storage()
462
+ const s = session()
418
463
  const current = storedToken()
419
464
  if (!s || !current?.refreshToken) return null
420
465
 
@@ -436,12 +481,22 @@ export function createEstivaId(config: EstivaIdConfig): EstivaIdClient {
436
481
  return null
437
482
  }
438
483
 
484
+ /*
485
+ The session is shared by every tab (PEE-3), and signing out takes no lock:
486
+ while `/token` was answering, another tab may have signed out — or in.
487
+ Then this answer belongs to a session that is gone, and writing it would
488
+ put a live token back after the sign-out, for its whole lifetime. Only
489
+ the session this renewal spent is replaced or cleared.
490
+ */
491
+ const stillOurs = () => storedToken()?.refreshToken === current.refreshToken
492
+
439
493
  if (!response.ok) {
440
- clearSession()
494
+ if (stillOurs()) clearSession()
441
495
  return null
442
496
  }
443
497
 
444
498
  const body = (await response.json()) as TokenResponse
499
+ if (!stillOurs()) return validToken()
445
500
  if (!body.access_token) {
446
501
  clearSession()
447
502
  return null
@@ -470,12 +525,34 @@ export function createEstivaId(config: EstivaIdConfig): EstivaIdClient {
470
525
  *
471
526
  * Coalescing rather than caching: the promise is dropped the moment it
472
527
  * settles, so the *next* expiry renews again.
528
+ *
529
+ * **And never across tabs either (PEE-3).** The promise guards one page; a
530
+ * session in `localStorage` is shared by every tab, and each tab's timer
531
+ * fires at the same moment, for the same expiry. So the renewal runs under
532
+ * {@link EstivaIdConfig.lock}, and a tab that waited for it compares the
533
+ * stored refresh token with the one it set out to spend: if another tab
534
+ * renewed (or signed out, or in) meanwhile, that tab's answer is taken and
535
+ * `/token` is not called — presenting the spent token would be the replay.
473
536
  */
474
537
  let refreshing: Promise<StoredToken | null> | null = null
475
538
 
476
539
  const refreshAccessToken = (): Promise<StoredToken | null> => {
477
540
  if (refreshing) return refreshing
478
- refreshing = performRefresh().finally(() => {
541
+ const spending = storedToken()?.refreshToken
542
+ refreshing = new Promise<StoredToken | null>((answer, fail) => {
543
+ lock(`${config.keyPrefix}.renewal`, async () => {
544
+ if (storedToken()?.refreshToken !== spending) return answer(validToken())
545
+ answer(await performRefresh())
546
+ /*
547
+ Held a little longer than the renewal, though the caller already has
548
+ its answer. A browser does not order one tab's `localStorage` write
549
+ with another tab's lock grant — the write reaches the other process's
550
+ copy shortly after — so a tab granted the lock at once could still
551
+ read the spent token and present it again.
552
+ */
553
+ await new Promise<void>((settle) => setTimeout(settle, RENEWAL_SETTLE_MS))
554
+ }).catch(fail)
555
+ }).finally(() => {
479
556
  refreshing = null
480
557
  })
481
558
  return refreshing
@@ -586,7 +663,7 @@ export function createEstivaId(config: EstivaIdConfig): EstivaIdClient {
586
663
  */
587
664
  async completeSignIn(search: string): Promise<{ token: StoredToken; returnTo: string }> {
588
665
  const s = pending()
589
- const sessionStore = config.storage()
666
+ const sessionStore = session()
590
667
  if (!s || !sessionStore) throw new SignInError('sign-in requires a browser')
591
668
 
592
669
  const params = new URLSearchParams(search)
@@ -707,6 +784,14 @@ export function createEstivaId(config: EstivaIdConfig): EstivaIdClient {
707
784
 
708
785
  handle = setTimer(() => {
709
786
  handle = null
787
+ // Another tab sharing the session renewed first (PEE-3): follow its
788
+ // token rather than spend a grant this tab no longer needs.
789
+ const shared = storedToken()
790
+ if (shared && shared.expiresAt > token.expiresAt) {
791
+ handlers.onRenewed?.(shared)
792
+ arm()
793
+ return
794
+ }
710
795
  void refreshAccessToken().then((next) => {
711
796
  if (cancelled) return
712
797
  if (!next) {
package/src/index.ts CHANGED
@@ -24,10 +24,12 @@
24
24
  *
25
25
  * ## Every difference between the two consumers is a parameter
26
26
  *
27
- * `clientId`, `redirectUri`, `storage`, `keyPrefix` and `navigate` are all
28
- * injected, because Peek and Ship genuinely differ on every one of them and
29
- * hardcoding any would have made the package come out Peek-shaped. See
30
- * `client.ts` for what each difference actually is.
27
+ * `clientId`, `redirectUri`, `keyPrefix` and `navigate` are all injected,
28
+ * because Peek and Ship genuinely differ on every one of them and hardcoding
29
+ * any would have made the package come out Peek-shaped. `storage` and
30
+ * `pendingStore` are injectable too, but since PEE-3 every app takes the same
31
+ * defaults (`localStorage` for the session, `sessionStorage` for a sign-in in
32
+ * flight). See `client.ts` for what each difference actually is.
31
33
  *
32
34
  * ## The globals prediction this package falsified
33
35
  *