@estiva-app/identity 0.1.1 → 0.1.2

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,55 @@
1
1
  # @estiva-app/identity
2
2
 
3
+ ## 0.1.2 — 2026-09-07
4
+
5
+ **No wire behaviour, no break.** `scheduleRenewal` is new and the guard on
6
+ `refreshAccessToken` only prevents a call that should never have been made, so a
7
+ consumer on `^0.1.0` needs no change — ADR 0002 §4b puts the break on MINOR
8
+ within `0.x`.
9
+
10
+ Two of SHA-4's five "hard-won behaviour that must survive" items did not, in
11
+ fact, survive — and both for the same reason. They lived in
12
+ `peek-app/src/auth/useEstivaIdAuth.ts`, the `ConvexProviderWithAuth` adapter,
13
+ which is deliberately the one file kept *out* of this package. So Peek kept them
14
+ and Ship, the second consumer whose entire purpose was to catch exactly this,
15
+ silently did not get them. Wiring a second consumer proves a package is not
16
+ shaped like the first app; it does not prove the first app handed everything
17
+ over.
18
+
19
+ - **`refreshAccessToken` now coalesces concurrent callers.** A refresh token is
20
+ single-use and Estiva ID reads a replay as theft, revoking the whole chain — so
21
+ two renewals in the air at once do not waste a round trip, they sign somebody
22
+ out of everything. Two callers is not hypothetical: Ship signs the NIP-98 auth
23
+ event and the content event as separate `POST /sign` calls, and a token that
24
+ expires between them answers `401` to both, each of which renews.
25
+
26
+ Peek has had this since PEEK-108 as a `refreshing` ref in its hook. Ship has
27
+ never had it. It belongs here, where every caller of every consumer gets it
28
+ without having thought about it. Coalescing, not caching: the promise is
29
+ dropped the moment it settles, and a test asserts the *next* expiry renews
30
+ again — a guard that held the promise forever would pass the first test and be
31
+ a worse bug than the one it fixed.
32
+
33
+ - **`scheduleRenewal(handlers)` renews before expiry rather than reacting to
34
+ it.** `tokenFrom` has always stored an expiry 30 seconds early; nothing in the
35
+ package acted on it. Reacting to expiry means somebody's next action is what
36
+ discovers the session ended. Renewing early means they notice nothing, which
37
+ is the whole point of PEEK-108.
38
+
39
+ The timer is injected (`setTimer`/`clearTimer`) for the same reason `fetch` and
40
+ `navigate` are: "we scheduled it" and "we did nothing" are otherwise the same
41
+ observation. The handle type never reaches the published `.d.ts` — it is
42
+ `number` in a browser and an object in Node, and naming either fails
43
+ `check-identity.yml`, correctly.
44
+
45
+ **Known limitation, documented here rather than rediscovered per app:** a
46
+ `setTimeout` is throttled in a background tab — to once a minute in Chrome and
47
+ Safari, and frozen outright after five minutes hidden — so a backgrounded tab
48
+ can miss its window. It degrades safely, because the refresh token long
49
+ outlives the access token: a late renewal is still a renewal, and anything that
50
+ beats the timer takes the existing `401`-and-retry path. The guard above is
51
+ what stops the two racing.
52
+
3
53
  ## 0.1.1 — 2026-08-28
4
54
 
5
55
  **No wire behaviour**, and no break: `pendingStore` is optional and defaults to
package/README.md CHANGED
@@ -42,6 +42,7 @@ package come out Peek-shaped, which is the whole risk SHA-4 names.
42
42
  | `pendingStore` — in-flight PKCE credentials | defaults to `storage` | `sessionStorage`, because the flow starts and ends in one tab |
43
43
  | `keyPrefix` | `peek.estivaId` | `ship.estiva-id` |
44
44
  | `navigate` | the one genuinely untestable act, so it is observable | |
45
+ | `setTimer` / `clearTimer` | the timers `scheduleRenewal` runs on, injected for the same reason | |
45
46
 
46
47
  **Do not unify the two stores.** NIP-RS read-state slots (CRO-4) must survive a
47
48
  restart and belong in `localStorage`; an access token does not. Different
@@ -67,6 +68,44 @@ const client = createEstivaId({
67
68
  export const { validToken, beginSignIn, completeSignIn, beginSignOut } = client
68
69
  ```
69
70
 
71
+ ## Renewal is scheduled, not reactive
72
+
73
+ ```ts
74
+ const schedule = client.scheduleRenewal({
75
+ onRenewed: (token) => setSignedIn(true),
76
+ onEnded: () => setSignedIn(false), // drop to the shell
77
+ })
78
+ // later, on sign-out or unmount
79
+ schedule.cancel()
80
+ ```
81
+
82
+ `tokenFrom` stores an expiry 30 seconds early on purpose, and `scheduleRenewal`
83
+ is what acts on it. Reacting to expiry instead means somebody's *next action* is
84
+ what discovers the session is over — a failed publish, a blank list, at best a
85
+ retry they can feel. Renewing early means they notice nothing, which is the whole
86
+ point of PEEK-108.
87
+
88
+ **Two renewals must never be in the air at once.** A refresh token is single-use
89
+ and Estiva ID reads a replay as evidence of theft, revoking the whole chain — so
90
+ a duplicate renewal does not waste a round trip, it signs somebody out of
91
+ everything. `refreshAccessToken` coalesces concurrent callers into one request,
92
+ and it does so for every caller, not only the scheduler: Ship signs the NIP-98
93
+ auth event and the content event as two separate `POST /sign` calls, and a token
94
+ that expires between them answers `401` to both.
95
+
96
+ **Known limitation: `setTimeout` is throttled in a background tab.** Chrome and
97
+ Safari clamp a hidden tab's timers to roughly once a minute, and may freeze them
98
+ outright after five minutes hidden, so a backgrounded tab can miss its renewal
99
+ window. This is recorded here rather than rediscovered once per app.
100
+
101
+ It degrades safely. The refresh token outlives the access token by a long way, so
102
+ a late renewal is still a renewal, and anything that beats the late timer takes
103
+ the existing `401`-and-retry path — which is exactly why the coalescing guard
104
+ above is not an optimisation. A `visibilitychange` listener that re-armed on
105
+ foreground would tighten it and is deliberately absent: `document` is a global
106
+ this package does not touch (ADR 0002 §4a). An app that wants it can `cancel()`
107
+ and call `scheduleRenewal` again.
108
+
70
109
  ## The prediction this package falsified
71
110
 
72
111
  Before starting, the expectation on the ticket was that identity **could not**
package/dist/client.d.ts CHANGED
@@ -102,6 +102,14 @@ export interface EstivaIdConfig {
102
102
  navigate?: (url: string) => void;
103
103
  now?: () => number;
104
104
  fetch?: FetchLike;
105
+ /**
106
+ * The timers `scheduleRenewal` runs on, injected for exactly the reason
107
+ * `fetch` and `navigate` are: a test cannot wait ten minutes to find out
108
+ * whether the renewal was scheduled, and "we scheduled it" and "we did
109
+ * nothing" are otherwise the same observation.
110
+ */
111
+ setTimer?: (run: () => void, ms: number) => unknown;
112
+ clearTimer?: (handle: unknown) => void;
105
113
  }
106
114
  export interface StoredToken {
107
115
  accessToken: string;
@@ -123,6 +131,25 @@ export interface StoredToken {
123
131
  }
124
132
  export declare class SignInError extends Error {
125
133
  }
134
+ /** What a scheduled renewal reports back. Both optional; neither is required to be useful. */
135
+ export interface RenewalHandlers {
136
+ /** A renewal succeeded. The session continues, on a later expiry. */
137
+ onRenewed?: (token: StoredToken) => void;
138
+ /**
139
+ * A renewal failed, and the session is over.
140
+ *
141
+ * The only honest thing an app can do here is drop to the shell. Note this
142
+ * fires on refusal *and* on an unreachable network — `refreshAccessToken`
143
+ * distinguishes them for the purpose of clearing the stored token, but from a
144
+ * scheduler's point of view a renewal that did not happen is a renewal that
145
+ * did not happen, and the shell's own probe is what sorts out which it was.
146
+ */
147
+ onEnded?: () => void;
148
+ }
149
+ export interface RenewalSchedule {
150
+ /** Stop renewing. Idempotent, and safe to call after the session has ended. */
151
+ cancel: () => void;
152
+ }
126
153
  export interface EstivaIdClient {
127
154
  redirectUri: () => string;
128
155
  codeChallenge: (verifier: string) => Promise<string>;
@@ -134,6 +161,7 @@ export interface EstivaIdClient {
134
161
  returnTo: string;
135
162
  }>;
136
163
  refreshAccessToken: () => Promise<StoredToken | null>;
164
+ scheduleRenewal: (handlers?: RenewalHandlers) => RenewalSchedule;
137
165
  storedToken: () => StoredToken | null;
138
166
  validToken: (now?: number) => StoredToken | null;
139
167
  hasSession: () => boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,WAAW,EAAyB,MAAM,YAAY,CAAA;AAuBxF,gFAAgF;AAChF,MAAM,MAAM,SAAS,GAAG,CACtB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,KACpE,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,CAAC,CAAA;AAIvE,MAAM,WAAW,cAAc;IAC7B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAA;IACZ,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,WAAW,EAAE,MAAM,MAAM,CAAA;IACzB;;;;;;OAMG;IACH,OAAO,EAAE,MAAM,aAAa,GAAG,IAAI,CAAA;IACnC;;;;;;;;;;;;;;;OAeG;IACH,YAAY,CAAC,EAAE,MAAM,aAAa,GAAG,IAAI,CAAA;IACzC,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAA;IACjB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,SAAS,CAAA;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd;;;;;;;;;;OAUG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,qBAAa,WAAY,SAAQ,KAAK;CAAG;AASzC,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,MAAM,CAAA;IACzB,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IACpD,WAAW,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAChF,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,KAAK,EAAE,WAAW,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACrF,kBAAkB,EAAE,MAAM,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IACrD,WAAW,EAAE,MAAM,WAAW,GAAG,IAAI,CAAA;IACrC,UAAU,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,KAAK,WAAW,GAAG,IAAI,CAAA;IAChD,UAAU,EAAE,MAAM,OAAO,CAAA;IACzB,kBAAkB,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;IAC5C,YAAY,EAAE,MAAM,IAAI,CAAA;IACxB,YAAY,EAAE,MAAM,IAAI,CAAA;IACxB,gBAAgB,EAAE,MAAM,OAAO,CAAA;IAC/B,kBAAkB,EAAE,MAAM,IAAI,CAAA;IAC9B,YAAY,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IACjC,gBAAgB,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAA;IAC/C,eAAe,EAAE,MAAM,WAAW,GAAG,IAAI,CAAA;CAC1C;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CAiXrE"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js CHANGED
@@ -51,6 +51,11 @@
51
51
  * observe where the flow *would* go.
52
52
  */
53
53
  import { clearGuard, markGuard } from './shell.js';
54
+ /**
55
+ * `setTimeout`'s ceiling. Above 2^31-1 ms the delay overflows to a negative
56
+ * int32 and the timer fires immediately.
57
+ */
58
+ const MAX_TIMER_MS = 2_147_483_647;
54
59
  export class SignInError extends Error {
55
60
  }
56
61
  export function createEstivaId(config) {
@@ -177,6 +182,83 @@ export function createEstivaId(config) {
177
182
  s?.removeItem(RETURN_KEY);
178
183
  return raw ?? null;
179
184
  };
185
+ /**
186
+ * Renew the access token without leaving the page (PEEK-108).
187
+ *
188
+ * This is the whole reason the grant exists. Before it, an expired token
189
+ * could only be replaced by a top-level navigation through `/authorize`,
190
+ * which costs whatever the person had on screen.
191
+ *
192
+ * Returns `null` for every failure, and the failures are not worth
193
+ * distinguishing: Estiva ID answers `invalid_grant` for all of them by
194
+ * design, and the caller's only move either way is to fall back to the
195
+ * shell. The stored token is cleared on refusal so a spent refresh token is
196
+ * never presented twice — a replay is what Estiva ID treats as theft.
197
+ */
198
+ const performRefresh = async () => {
199
+ const s = config.storage();
200
+ const current = storedToken();
201
+ if (!s || !current?.refreshToken)
202
+ return null;
203
+ let response;
204
+ try {
205
+ response = await send()(new URL('/token', config.base).toString(), {
206
+ method: 'POST',
207
+ headers: { 'content-type': 'application/json' },
208
+ body: JSON.stringify({
209
+ grantType: 'refresh_token',
210
+ refreshToken: current.refreshToken,
211
+ clientId: config.clientId,
212
+ }),
213
+ });
214
+ }
215
+ catch {
216
+ // Unreachable rather than refused. The token in hand may still be fine,
217
+ // so this deliberately does not clear the session — a flaky network must
218
+ // not sign somebody out.
219
+ return null;
220
+ }
221
+ if (!response.ok) {
222
+ clearSession();
223
+ return null;
224
+ }
225
+ const body = (await response.json());
226
+ if (!body.access_token) {
227
+ clearSession();
228
+ return null;
229
+ }
230
+ const next = tokenFrom(body, body.pubkey ?? current.pubkey);
231
+ s.setItem(TOKEN_KEY, JSON.stringify(next));
232
+ return next;
233
+ };
234
+ /**
235
+ * The renewal in flight, if there is one.
236
+ *
237
+ * **A refresh token is single-use and a replay is read as theft**, which
238
+ * revokes the whole chain — so two renewals must never be in the air at once.
239
+ * Two easily can be: Ship signs the NIP-98 auth event and the content event as
240
+ * separate `POST /sign` calls, and a token that expires between them answers
241
+ * `401` to both, each of which renews.
242
+ *
243
+ * Peek has guarded this since PEEK-108 with a `refreshing` ref in its Convex
244
+ * adapter, and SHA-4 lists the guard as one of the behaviours that must
245
+ * survive the extraction — but the guard is the one part that did not travel,
246
+ * because it lived in the hook rather than the client. So Peek kept it and
247
+ * Ship never had it. It belongs here, where every caller of every consumer
248
+ * gets it whether or not they thought about it.
249
+ *
250
+ * Coalescing rather than caching: the promise is dropped the moment it
251
+ * settles, so the *next* expiry renews again.
252
+ */
253
+ let refreshing = null;
254
+ const refreshAccessToken = () => {
255
+ if (refreshing)
256
+ return refreshing;
257
+ refreshing = performRefresh().finally(() => {
258
+ refreshing = null;
259
+ });
260
+ return refreshing;
261
+ };
180
262
  return {
181
263
  redirectUri: config.redirectUri,
182
264
  codeChallenge,
@@ -327,54 +409,87 @@ export function createEstivaId(config) {
327
409
  clearGuard(s);
328
410
  return { token, returnTo };
329
411
  },
412
+ refreshAccessToken,
330
413
  /**
331
- * Renew the access token without leaving the page (PEEK-108).
414
+ * Renew shortly **before** the token expires, rather than reacting to it
415
+ * having expired.
332
416
  *
333
- * This is the whole reason the grant exists. Before it, an expired token
334
- * could only be replaced by a top-level navigation through `/authorize`,
335
- * which costs whatever the person had on screen.
417
+ * SHA-4 lists this as one of the behaviours that must survive the
418
+ * extraction, and it is the one that reads as an optimisation and is not.
419
+ * Reacting to expiry means somebody's next action is the thing that
420
+ * discovers the session is over — a failed publish, a blank list, or at best
421
+ * a retry they can feel. Renewing 30 seconds early (`tokenFrom` already
422
+ * subtracts them) means they notice nothing at all.
336
423
  *
337
- * Returns `null` for every failure, and the failures are not worth
338
- * distinguishing: Estiva ID answers `invalid_grant` for all of them by
339
- * design, and the caller's only move either way is to fall back to the
340
- * shell. The stored token is cleared on refusal so a spent refresh token is
341
- * never presented twice — a replay is what Estiva ID treats as theft.
424
+ * It did not travel with the rest of SHA-4: the timer lived in Peek's Convex
425
+ * adapter, so Peek kept it and Ship, whose whole point was to be the second
426
+ * consumer, silently did not get it. Ship renews only when `POST /sign`
427
+ * answers `401`, which is precisely "on failure".
428
+ *
429
+ * ## The timer is a `setTimeout`, and browsers throttle those
430
+ *
431
+ * A background tab gets its timers clamped — to once a minute in Chrome and
432
+ * Safari, and after five minutes of being hidden they may be frozen outright.
433
+ * So a tab left in the background can miss its renewal window entirely.
434
+ *
435
+ * This is a known limitation rather than a bug, and it is recorded here so
436
+ * it is not rediscovered once per app. It degrades safely: a late renewal is
437
+ * still a valid renewal, because the refresh token outlives the access token
438
+ * by a long way, and a call that beats the late timer gets the existing
439
+ * `401`-and-retry path. What it must not do is renew *twice* in the scramble,
440
+ * which is what the guard on `refreshAccessToken` is for.
441
+ *
442
+ * A `visibilitychange` listener that re-arms on foreground would tighten
443
+ * this, and is deliberately not here: `document` is a global this package
444
+ * does not touch (ADR 0002 §4a), and an app that wants it can `cancel()` and
445
+ * call this again.
342
446
  */
343
- async refreshAccessToken() {
344
- const s = config.storage();
345
- const current = storedToken();
346
- if (!s || !current?.refreshToken)
347
- return null;
348
- let response;
349
- try {
350
- response = await send()(new URL('/token', config.base).toString(), {
351
- method: 'POST',
352
- headers: { 'content-type': 'application/json' },
353
- body: JSON.stringify({
354
- grantType: 'refresh_token',
355
- refreshToken: current.refreshToken,
356
- clientId: config.clientId,
357
- }),
358
- });
359
- }
360
- catch {
361
- // Unreachable rather than refused. The token in hand may still be fine,
362
- // so this deliberately does not clear the session — a flaky network must
363
- // not sign somebody out.
364
- return null;
365
- }
366
- if (!response.ok) {
367
- clearSession();
368
- return null;
369
- }
370
- const body = (await response.json());
371
- if (!body.access_token) {
372
- clearSession();
373
- return null;
374
- }
375
- const next = tokenFrom(body, body.pubkey ?? current.pubkey);
376
- s.setItem(TOKEN_KEY, JSON.stringify(next));
377
- return next;
447
+ scheduleRenewal(handlers = {}) {
448
+ const setTimer = config.setTimer ?? ((run, ms) => setTimeout(run, ms));
449
+ const clearTimer = config.clearTimer ?? ((handle) => clearTimeout(handle));
450
+ let handle = null;
451
+ let cancelled = false;
452
+ const arm = () => {
453
+ const token = storedToken();
454
+ // Nothing to renew. Not an ending — the caller is simply signed out, and
455
+ // an app that signs in later starts a new schedule.
456
+ if (!token)
457
+ return;
458
+ /*
459
+ Clamped at both ends. A negative delay is a token that already expired
460
+ — renew now rather than never, since the refresh token almost certainly
461
+ has not. The ceiling is `setTimeout`'s: a delay above 2^31-1 ms
462
+ overflows to a negative int32 and fires immediately, so a nonsensical
463
+ `expires_in` would otherwise become a hot loop against `/token` instead
464
+ of one very distant timer.
465
+ */
466
+ const delay = Math.min(Math.max(0, token.expiresAt - now()), MAX_TIMER_MS);
467
+ handle = setTimer(() => {
468
+ handle = null;
469
+ void refreshAccessToken().then((next) => {
470
+ if (cancelled)
471
+ return;
472
+ if (!next) {
473
+ handlers.onEnded?.();
474
+ return;
475
+ }
476
+ handlers.onRenewed?.(next);
477
+ // Re-armed from the *new* expiry rather than a fixed interval, so
478
+ // the schedule follows whatever lifetime Estiva ID actually issued.
479
+ arm();
480
+ });
481
+ }, delay);
482
+ };
483
+ arm();
484
+ return {
485
+ cancel() {
486
+ cancelled = true;
487
+ if (handle !== null) {
488
+ clearTimer(handle);
489
+ handle = null;
490
+ }
491
+ },
492
+ };
378
493
  },
379
494
  /**
380
495
  * Leave for Estiva ID to end the session *there* (PEEK-122).
@@ -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
@@ -46,7 +46,7 @@
46
46
  * The thing that made it possible was not cleverness — it was that a browser API
47
47
  * an app must inject is also a browser API a test can observe.
48
48
  */
49
- export declare const IDENTITY_VERSION = "0.1.1";
49
+ export declare const IDENTITY_VERSION = "0.1.2";
50
50
  export { type KeyValueStore, type ShellPhase, type ShellReason, type ShellState, type BootObservation, type BootAction, type ShellEvent, isRecoverable, SHELL_COPY, CONTINUE_LABEL, RETRY_LABEL, PROBE_TIMEOUT_MS, GUARD_TTL_MS, GUARD_KEY, decideBoot, reasonForCallbackError, readGuard, markGuard, clearGuard, logShellEvent, } from './shell.js';
51
- export { type EstivaIdConfig, type EstivaIdClient, type StoredToken, type FetchLike, SignInError, createEstivaId, } from './client.js';
51
+ export { type EstivaIdConfig, type EstivaIdClient, type StoredToken, type FetchLike, type RenewalHandlers, type RenewalSchedule, SignInError, createEstivaId, } from './client.js';
52
52
  //# sourceMappingURL=index.d.ts.map
@@ -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,WAAW,EACX,cAAc,GACf,MAAM,aAAa,CAAA"}
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"}
package/dist/index.js CHANGED
@@ -46,7 +46,7 @@
46
46
  * The thing that made it possible was not cleverness — it was that a browser API
47
47
  * an app must inject is also a browser API a test can observe.
48
48
  */
49
- export const IDENTITY_VERSION = '0.1.1';
49
+ export const IDENTITY_VERSION = '0.1.2';
50
50
  export { isRecoverable, SHELL_COPY, CONTINUE_LABEL, RETRY_LABEL, PROBE_TIMEOUT_MS, GUARD_TTL_MS, GUARD_KEY, decideBoot, reasonForCallbackError, readGuard, markGuard, clearGuard, logShellEvent, } from './shell.js';
51
51
  export { SignInError, createEstivaId, } from './client.js';
52
52
  //# sourceMappingURL=index.js.map
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,EAKL,WAAW,EACX,cAAc,GACf,MAAM,aAAa,CAAA"}
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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estiva-app/identity",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Estiva ID sign-in: authorization code + PKCE, token storage, silent renewal, sign-out, 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
@@ -73,6 +73,19 @@ declare const URL: {
73
73
  }
74
74
  declare const URLSearchParams: { new (init: string): { get(name: string): string | null } }
75
75
 
76
+ /**
77
+ * The timer pair, for {@link EstivaIdClient.scheduleRenewal}.
78
+ *
79
+ * Declared here for the same reason as everything above, and returning
80
+ * `unknown` for one more: the handle's real type is `number` in a browser and an
81
+ * object in Node, and naming either would put an ambient type in the published
82
+ * `.d.ts` — which `check-identity.yml` fails the build over, correctly. Nothing
83
+ * outside this module ever sees a handle; `scheduleRenewal` hands back a
84
+ * `cancel` function instead.
85
+ */
86
+ declare const setTimeout: (run: () => void, ms: number) => unknown
87
+ declare const clearTimeout: (handle: unknown) => void
88
+
76
89
  /** Just enough of `fetch` to redeem a code. A parameter as well as a global. */
77
90
  export type FetchLike = (
78
91
  url: string,
@@ -122,6 +135,14 @@ export interface EstivaIdConfig {
122
135
  navigate?: (url: string) => void
123
136
  now?: () => number
124
137
  fetch?: FetchLike
138
+ /**
139
+ * The timers `scheduleRenewal` runs on, injected for exactly the reason
140
+ * `fetch` and `navigate` are: a test cannot wait ten minutes to find out
141
+ * whether the renewal was scheduled, and "we scheduled it" and "we did
142
+ * nothing" are otherwise the same observation.
143
+ */
144
+ setTimer?: (run: () => void, ms: number) => unknown
145
+ clearTimer?: (handle: unknown) => void
125
146
  }
126
147
 
127
148
  export interface StoredToken {
@@ -143,8 +164,35 @@ export interface StoredToken {
143
164
  refreshToken?: string
144
165
  }
145
166
 
167
+ /**
168
+ * `setTimeout`'s ceiling. Above 2^31-1 ms the delay overflows to a negative
169
+ * int32 and the timer fires immediately.
170
+ */
171
+ const MAX_TIMER_MS = 2_147_483_647
172
+
146
173
  export class SignInError extends Error {}
147
174
 
175
+ /** What a scheduled renewal reports back. Both optional; neither is required to be useful. */
176
+ export interface RenewalHandlers {
177
+ /** A renewal succeeded. The session continues, on a later expiry. */
178
+ onRenewed?: (token: StoredToken) => void
179
+ /**
180
+ * A renewal failed, and the session is over.
181
+ *
182
+ * The only honest thing an app can do here is drop to the shell. Note this
183
+ * fires on refusal *and* on an unreachable network — `refreshAccessToken`
184
+ * distinguishes them for the purpose of clearing the stored token, but from a
185
+ * scheduler's point of view a renewal that did not happen is a renewal that
186
+ * did not happen, and the shell's own probe is what sorts out which it was.
187
+ */
188
+ onEnded?: () => void
189
+ }
190
+
191
+ export interface RenewalSchedule {
192
+ /** Stop renewing. Idempotent, and safe to call after the session has ended. */
193
+ cancel: () => void
194
+ }
195
+
148
196
  interface TokenResponse {
149
197
  access_token?: string
150
198
  expires_in?: number
@@ -158,6 +206,7 @@ export interface EstivaIdClient {
158
206
  beginSignIn: (returnTo: string, options?: { silent?: boolean }) => Promise<void>
159
207
  completeSignIn: (search: string) => Promise<{ token: StoredToken; returnTo: string }>
160
208
  refreshAccessToken: () => Promise<StoredToken | null>
209
+ scheduleRenewal: (handlers?: RenewalHandlers) => RenewalSchedule
161
210
  storedToken: () => StoredToken | null
162
211
  validToken: (now?: number) => StoredToken | null
163
212
  hasSession: () => boolean
@@ -300,6 +349,88 @@ export function createEstivaId(config: EstivaIdConfig): EstivaIdClient {
300
349
  return raw ?? null
301
350
  }
302
351
 
352
+ /**
353
+ * Renew the access token without leaving the page (PEEK-108).
354
+ *
355
+ * This is the whole reason the grant exists. Before it, an expired token
356
+ * could only be replaced by a top-level navigation through `/authorize`,
357
+ * which costs whatever the person had on screen.
358
+ *
359
+ * Returns `null` for every failure, and the failures are not worth
360
+ * distinguishing: Estiva ID answers `invalid_grant` for all of them by
361
+ * design, and the caller's only move either way is to fall back to the
362
+ * shell. The stored token is cleared on refusal so a spent refresh token is
363
+ * never presented twice — a replay is what Estiva ID treats as theft.
364
+ */
365
+ const performRefresh = async (): Promise<StoredToken | null> => {
366
+ const s = config.storage()
367
+ const current = storedToken()
368
+ if (!s || !current?.refreshToken) return null
369
+
370
+ let response: Awaited<ReturnType<FetchLike>>
371
+ try {
372
+ response = await send()(new URL('/token', config.base).toString(), {
373
+ method: 'POST',
374
+ headers: { 'content-type': 'application/json' },
375
+ body: JSON.stringify({
376
+ grantType: 'refresh_token',
377
+ refreshToken: current.refreshToken,
378
+ clientId: config.clientId,
379
+ }),
380
+ })
381
+ } catch {
382
+ // Unreachable rather than refused. The token in hand may still be fine,
383
+ // so this deliberately does not clear the session — a flaky network must
384
+ // not sign somebody out.
385
+ return null
386
+ }
387
+
388
+ if (!response.ok) {
389
+ clearSession()
390
+ return null
391
+ }
392
+
393
+ const body = (await response.json()) as TokenResponse
394
+ if (!body.access_token) {
395
+ clearSession()
396
+ return null
397
+ }
398
+
399
+ const next = tokenFrom(body, body.pubkey ?? current.pubkey)
400
+ s.setItem(TOKEN_KEY, JSON.stringify(next))
401
+ return next
402
+ }
403
+
404
+ /**
405
+ * The renewal in flight, if there is one.
406
+ *
407
+ * **A refresh token is single-use and a replay is read as theft**, which
408
+ * revokes the whole chain — so two renewals must never be in the air at once.
409
+ * Two easily can be: Ship signs the NIP-98 auth event and the content event as
410
+ * separate `POST /sign` calls, and a token that expires between them answers
411
+ * `401` to both, each of which renews.
412
+ *
413
+ * Peek has guarded this since PEEK-108 with a `refreshing` ref in its Convex
414
+ * adapter, and SHA-4 lists the guard as one of the behaviours that must
415
+ * survive the extraction — but the guard is the one part that did not travel,
416
+ * because it lived in the hook rather than the client. So Peek kept it and
417
+ * Ship never had it. It belongs here, where every caller of every consumer
418
+ * gets it whether or not they thought about it.
419
+ *
420
+ * Coalescing rather than caching: the promise is dropped the moment it
421
+ * settles, so the *next* expiry renews again.
422
+ */
423
+ let refreshing: Promise<StoredToken | null> | null = null
424
+
425
+ const refreshAccessToken = (): Promise<StoredToken | null> => {
426
+ if (refreshing) return refreshing
427
+ refreshing = performRefresh().finally(() => {
428
+ refreshing = null
429
+ })
430
+ return refreshing
431
+ }
432
+
433
+
303
434
  return {
304
435
  redirectUri: config.redirectUri,
305
436
  codeChallenge,
@@ -464,58 +595,95 @@ export function createEstivaId(config: EstivaIdConfig): EstivaIdClient {
464
595
  return { token, returnTo }
465
596
  },
466
597
 
598
+ refreshAccessToken,
599
+
467
600
  /**
468
- * Renew the access token without leaving the page (PEEK-108).
601
+ * Renew shortly **before** the token expires, rather than reacting to it
602
+ * having expired.
603
+ *
604
+ * SHA-4 lists this as one of the behaviours that must survive the
605
+ * extraction, and it is the one that reads as an optimisation and is not.
606
+ * Reacting to expiry means somebody's next action is the thing that
607
+ * discovers the session is over — a failed publish, a blank list, or at best
608
+ * a retry they can feel. Renewing 30 seconds early (`tokenFrom` already
609
+ * subtracts them) means they notice nothing at all.
610
+ *
611
+ * It did not travel with the rest of SHA-4: the timer lived in Peek's Convex
612
+ * adapter, so Peek kept it and Ship, whose whole point was to be the second
613
+ * consumer, silently did not get it. Ship renews only when `POST /sign`
614
+ * answers `401`, which is precisely "on failure".
469
615
  *
470
- * This is the whole reason the grant exists. Before it, an expired token
471
- * could only be replaced by a top-level navigation through `/authorize`,
472
- * which costs whatever the person had on screen.
616
+ * ## The timer is a `setTimeout`, and browsers throttle those
473
617
  *
474
- * Returns `null` for every failure, and the failures are not worth
475
- * distinguishing: Estiva ID answers `invalid_grant` for all of them by
476
- * design, and the caller's only move either way is to fall back to the
477
- * shell. The stored token is cleared on refusal so a spent refresh token is
478
- * never presented twice — a replay is what Estiva ID treats as theft.
618
+ * A background tab gets its timers clamped — to once a minute in Chrome and
619
+ * Safari, and after five minutes of being hidden they may be frozen outright.
620
+ * So a tab left in the background can miss its renewal window entirely.
621
+ *
622
+ * This is a known limitation rather than a bug, and it is recorded here so
623
+ * it is not rediscovered once per app. It degrades safely: a late renewal is
624
+ * still a valid renewal, because the refresh token outlives the access token
625
+ * by a long way, and a call that beats the late timer gets the existing
626
+ * `401`-and-retry path. What it must not do is renew *twice* in the scramble,
627
+ * which is what the guard on `refreshAccessToken` is for.
628
+ *
629
+ * A `visibilitychange` listener that re-arms on foreground would tighten
630
+ * this, and is deliberately not here: `document` is a global this package
631
+ * does not touch (ADR 0002 §4a), and an app that wants it can `cancel()` and
632
+ * call this again.
479
633
  */
480
- async refreshAccessToken(): Promise<StoredToken | null> {
481
- const s = config.storage()
482
- const current = storedToken()
483
- if (!s || !current?.refreshToken) return null
484
-
485
- let response: Awaited<ReturnType<FetchLike>>
486
- try {
487
- response = await send()(new URL('/token', config.base).toString(), {
488
- method: 'POST',
489
- headers: { 'content-type': 'application/json' },
490
- body: JSON.stringify({
491
- grantType: 'refresh_token',
492
- refreshToken: current.refreshToken,
493
- clientId: config.clientId,
494
- }),
495
- })
496
- } catch {
497
- // Unreachable rather than refused. The token in hand may still be fine,
498
- // so this deliberately does not clear the session — a flaky network must
499
- // not sign somebody out.
500
- return null
634
+ scheduleRenewal(handlers: RenewalHandlers = {}): RenewalSchedule {
635
+ const setTimer = config.setTimer ?? ((run, ms) => setTimeout(run, ms))
636
+ const clearTimer = config.clearTimer ?? ((handle) => clearTimeout(handle))
637
+
638
+ let handle: unknown = null
639
+ let cancelled = false
640
+
641
+ const arm = (): void => {
642
+ const token = storedToken()
643
+ // Nothing to renew. Not an ending — the caller is simply signed out, and
644
+ // an app that signs in later starts a new schedule.
645
+ if (!token) return
646
+
647
+ /*
648
+ Clamped at both ends. A negative delay is a token that already expired
649
+ — renew now rather than never, since the refresh token almost certainly
650
+ has not. The ceiling is `setTimeout`'s: a delay above 2^31-1 ms
651
+ overflows to a negative int32 and fires immediately, so a nonsensical
652
+ `expires_in` would otherwise become a hot loop against `/token` instead
653
+ of one very distant timer.
654
+ */
655
+ const delay = Math.min(Math.max(0, token.expiresAt - now()), MAX_TIMER_MS)
656
+
657
+ handle = setTimer(() => {
658
+ handle = null
659
+ void refreshAccessToken().then((next) => {
660
+ if (cancelled) return
661
+ if (!next) {
662
+ handlers.onEnded?.()
663
+ return
664
+ }
665
+ handlers.onRenewed?.(next)
666
+ // Re-armed from the *new* expiry rather than a fixed interval, so
667
+ // the schedule follows whatever lifetime Estiva ID actually issued.
668
+ arm()
669
+ })
670
+ }, delay)
501
671
  }
502
672
 
503
- if (!response.ok) {
504
- clearSession()
505
- return null
506
- }
673
+ arm()
507
674
 
508
- const body = (await response.json()) as TokenResponse
509
- if (!body.access_token) {
510
- clearSession()
511
- return null
675
+ return {
676
+ cancel(): void {
677
+ cancelled = true
678
+ if (handle !== null) {
679
+ clearTimer(handle)
680
+ handle = null
681
+ }
682
+ },
512
683
  }
513
-
514
- const next = tokenFrom(body, body.pubkey ?? current.pubkey)
515
- s.setItem(TOKEN_KEY, JSON.stringify(next))
516
- return next
517
684
  },
518
685
 
686
+
519
687
  /**
520
688
  * Leave for Estiva ID to end the session *there* (PEEK-122).
521
689
  *
package/src/index.ts CHANGED
@@ -46,7 +46,7 @@
46
46
  * The thing that made it possible was not cleverness — it was that a browser API
47
47
  * an app must inject is also a browser API a test can observe.
48
48
  */
49
- export const IDENTITY_VERSION = '0.1.1'
49
+ export const IDENTITY_VERSION = '0.1.2'
50
50
 
51
51
  export {
52
52
  type KeyValueStore,
@@ -76,6 +76,8 @@ export {
76
76
  type EstivaIdClient,
77
77
  type StoredToken,
78
78
  type FetchLike,
79
+ type RenewalHandlers,
80
+ type RenewalSchedule,
79
81
  SignInError,
80
82
  createEstivaId,
81
83
  } from './client.js'