@estiva-app/identity 0.1.1 → 0.1.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 +89 -0
- package/README.md +41 -1
- package/dist/client.d.ts +28 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +215 -50
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +267 -48
- package/src/index.ts +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,94 @@
|
|
|
1
1
|
# @estiva-app/identity
|
|
2
2
|
|
|
3
|
+
## 0.1.3 — 2026-09-07
|
|
4
|
+
|
|
5
|
+
**Fixes a dead end reported from production.** No API change; `expiresAt` is now
|
|
6
|
+
derived differently, so a consumer needs no code change to get the fix.
|
|
7
|
+
|
|
8
|
+
`expires_in` is a **duration**, so acting on it means adding it to the local
|
|
9
|
+
clock. The service decides with the token's own `exp` claim, against **its**
|
|
10
|
+
clock. Those two answers agree only for as long as the clocks do.
|
|
11
|
+
|
|
12
|
+
When they diverge in the unsafe direction — a browser clock ahead of the
|
|
13
|
+
service, which is what a machine waking from a night's sleep readily produces —
|
|
14
|
+
the app holds a token it believes is live and every server refuses. And it does
|
|
15
|
+
not merely retry badly, it cannot recover at all:
|
|
16
|
+
|
|
17
|
+
- `POST /sign` answers `401 Invalid token: "exp" claim timestamp check failed`.
|
|
18
|
+
- `validToken()` keeps returning that same token, because by its own arithmetic
|
|
19
|
+
the token is fine.
|
|
20
|
+
- So every caller that asks gets the dead token again — including the one Convex
|
|
21
|
+
asks — and nothing ever renews.
|
|
22
|
+
- Peek's shell then sees a live token plus an unauthenticated verdict and calls
|
|
23
|
+
it `token_rejected`, whose copy says a configuration problem that signing in
|
|
24
|
+
again will not fix. It was an expiry, and signing in again was exactly what
|
|
25
|
+
fixed it. Clearing site data was the only way out.
|
|
26
|
+
|
|
27
|
+
**Both bounds are now computed and the earlier wins.** `exp` is what the service
|
|
28
|
+
will actually check, so it is the one that matters; `expires_in` is kept because
|
|
29
|
+
it covers a token with no readable `exp`, and a clock running *behind* the
|
|
30
|
+
service's, where `exp` alone would be the more generous of the two. The 30s
|
|
31
|
+
hold-back applies to whichever wins.
|
|
32
|
+
|
|
33
|
+
Reading the claim is not verification — that is the resource server's job and it
|
|
34
|
+
holds the key. This reads one number out of a payload the app already has, so
|
|
35
|
+
that "is this token still good" is answered by the same fact the service will
|
|
36
|
+
answer it with. A payload that will not parse yields no bound and the token is
|
|
37
|
+
still held: a token this cannot read is still a token.
|
|
38
|
+
|
|
39
|
+
5 new tests, 59 to 64, including two controls that the `expires_in` fallback is
|
|
40
|
+
untouched. Reverting the clamp fails exactly the two tests that depend on it.
|
|
41
|
+
|
|
42
|
+
## 0.1.2 — 2026-09-07
|
|
43
|
+
|
|
44
|
+
**No wire behaviour, no break.** `scheduleRenewal` is new and the guard on
|
|
45
|
+
`refreshAccessToken` only prevents a call that should never have been made, so a
|
|
46
|
+
consumer on `^0.1.0` needs no change — ADR 0002 §4b puts the break on MINOR
|
|
47
|
+
within `0.x`.
|
|
48
|
+
|
|
49
|
+
Two of SHA-4's five "hard-won behaviour that must survive" items did not, in
|
|
50
|
+
fact, survive — and both for the same reason. They lived in
|
|
51
|
+
`peek-app/src/auth/useEstivaIdAuth.ts`, the `ConvexProviderWithAuth` adapter,
|
|
52
|
+
which is deliberately the one file kept *out* of this package. So Peek kept them
|
|
53
|
+
and Ship, the second consumer whose entire purpose was to catch exactly this,
|
|
54
|
+
silently did not get them. Wiring a second consumer proves a package is not
|
|
55
|
+
shaped like the first app; it does not prove the first app handed everything
|
|
56
|
+
over.
|
|
57
|
+
|
|
58
|
+
- **`refreshAccessToken` now coalesces concurrent callers.** A refresh token is
|
|
59
|
+
single-use and Estiva ID reads a replay as theft, revoking the whole chain — so
|
|
60
|
+
two renewals in the air at once do not waste a round trip, they sign somebody
|
|
61
|
+
out of everything. Two callers is not hypothetical: Ship signs the NIP-98 auth
|
|
62
|
+
event and the content event as separate `POST /sign` calls, and a token that
|
|
63
|
+
expires between them answers `401` to both, each of which renews.
|
|
64
|
+
|
|
65
|
+
Peek has had this since PEEK-108 as a `refreshing` ref in its hook. Ship has
|
|
66
|
+
never had it. It belongs here, where every caller of every consumer gets it
|
|
67
|
+
without having thought about it. Coalescing, not caching: the promise is
|
|
68
|
+
dropped the moment it settles, and a test asserts the *next* expiry renews
|
|
69
|
+
again — a guard that held the promise forever would pass the first test and be
|
|
70
|
+
a worse bug than the one it fixed.
|
|
71
|
+
|
|
72
|
+
- **`scheduleRenewal(handlers)` renews before expiry rather than reacting to
|
|
73
|
+
it.** `tokenFrom` has always stored an expiry 30 seconds early; nothing in the
|
|
74
|
+
package acted on it. Reacting to expiry means somebody's next action is what
|
|
75
|
+
discovers the session ended. Renewing early means they notice nothing, which
|
|
76
|
+
is the whole point of PEEK-108.
|
|
77
|
+
|
|
78
|
+
The timer is injected (`setTimer`/`clearTimer`) for the same reason `fetch` and
|
|
79
|
+
`navigate` are: "we scheduled it" and "we did nothing" are otherwise the same
|
|
80
|
+
observation. The handle type never reaches the published `.d.ts` — it is
|
|
81
|
+
`number` in a browser and an object in Node, and naming either fails
|
|
82
|
+
`check-identity.yml`, correctly.
|
|
83
|
+
|
|
84
|
+
**Known limitation, documented here rather than rediscovered per app:** a
|
|
85
|
+
`setTimeout` is throttled in a background tab — to once a minute in Chrome and
|
|
86
|
+
Safari, and frozen outright after five minutes hidden — so a backgrounded tab
|
|
87
|
+
can miss its window. It degrades safely, because the refresh token long
|
|
88
|
+
outlives the access token: a late renewal is still a renewal, and anything that
|
|
89
|
+
beats the timer takes the existing `401`-and-retry path. The guard above is
|
|
90
|
+
what stops the two racing.
|
|
91
|
+
|
|
3
92
|
## 0.1.1 — 2026-08-28
|
|
4
93
|
|
|
5
94
|
**No wire behaviour**, and no break: `pendingStore` is optional and defaults to
|
package/README.md
CHANGED
|
@@ -7,7 +7,8 @@ renewal, sign-out, and the auth-shell state machine.
|
|
|
7
7
|
npm install @estiva-app/identity
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Nothing from outside the `@estiva-app` foundation, no data layer, and no UI.
|
|
11
|
+
CI enforces all three.
|
|
11
12
|
|
|
12
13
|
## The seam this package exists for
|
|
13
14
|
|
|
@@ -42,6 +43,7 @@ package come out Peek-shaped, which is the whole risk SHA-4 names.
|
|
|
42
43
|
| `pendingStore` — in-flight PKCE credentials | defaults to `storage` | `sessionStorage`, because the flow starts and ends in one tab |
|
|
43
44
|
| `keyPrefix` | `peek.estivaId` | `ship.estiva-id` |
|
|
44
45
|
| `navigate` | the one genuinely untestable act, so it is observable | |
|
|
46
|
+
| `setTimer` / `clearTimer` | the timers `scheduleRenewal` runs on, injected for the same reason | |
|
|
45
47
|
|
|
46
48
|
**Do not unify the two stores.** NIP-RS read-state slots (CRO-4) must survive a
|
|
47
49
|
restart and belong in `localStorage`; an access token does not. Different
|
|
@@ -67,6 +69,44 @@ const client = createEstivaId({
|
|
|
67
69
|
export const { validToken, beginSignIn, completeSignIn, beginSignOut } = client
|
|
68
70
|
```
|
|
69
71
|
|
|
72
|
+
## Renewal is scheduled, not reactive
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const schedule = client.scheduleRenewal({
|
|
76
|
+
onRenewed: (token) => setSignedIn(true),
|
|
77
|
+
onEnded: () => setSignedIn(false), // drop to the shell
|
|
78
|
+
})
|
|
79
|
+
// later, on sign-out or unmount
|
|
80
|
+
schedule.cancel()
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`tokenFrom` stores an expiry 30 seconds early on purpose, and `scheduleRenewal`
|
|
84
|
+
is what acts on it. Reacting to expiry instead means somebody's *next action* is
|
|
85
|
+
what discovers the session is over — a failed publish, a blank list, at best a
|
|
86
|
+
retry they can feel. Renewing early means they notice nothing, which is the whole
|
|
87
|
+
point of PEEK-108.
|
|
88
|
+
|
|
89
|
+
**Two renewals must never be in the air at once.** A refresh token is single-use
|
|
90
|
+
and Estiva ID reads a replay as evidence of theft, revoking the whole chain — so
|
|
91
|
+
a duplicate renewal does not waste a round trip, it signs somebody out of
|
|
92
|
+
everything. `refreshAccessToken` coalesces concurrent callers into one request,
|
|
93
|
+
and it does so for every caller, not only the scheduler: Ship signs the NIP-98
|
|
94
|
+
auth event and the content event as two separate `POST /sign` calls, and a token
|
|
95
|
+
that expires between them answers `401` to both.
|
|
96
|
+
|
|
97
|
+
**Known limitation: `setTimeout` is throttled in a background tab.** Chrome and
|
|
98
|
+
Safari clamp a hidden tab's timers to roughly once a minute, and may freeze them
|
|
99
|
+
outright after five minutes hidden, so a backgrounded tab can miss its renewal
|
|
100
|
+
window. This is recorded here rather than rediscovered once per app.
|
|
101
|
+
|
|
102
|
+
It degrades safely. The refresh token outlives the access token by a long way, so
|
|
103
|
+
a late renewal is still a renewal, and anything that beats the late timer takes
|
|
104
|
+
the existing `401`-and-retry path — which is exactly why the coalescing guard
|
|
105
|
+
above is not an optimisation. A `visibilitychange` listener that re-armed on
|
|
106
|
+
foreground would tighten it and is deliberately absent: `document` is a global
|
|
107
|
+
this package does not touch (ADR 0002 §4a). An app that wants it can `cancel()`
|
|
108
|
+
and call `scheduleRenewal` again.
|
|
109
|
+
|
|
70
110
|
## The prediction this package falsified
|
|
71
111
|
|
|
72
112
|
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;
|
package/dist/client.d.ts.map
CHANGED
|
@@ -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;
|
|
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) {
|
|
@@ -81,18 +86,68 @@ export function createEstivaId(config) {
|
|
|
81
86
|
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
|
|
82
87
|
return base64Url(new Uint8Array(digest));
|
|
83
88
|
};
|
|
89
|
+
/**
|
|
90
|
+
* The `exp` claim, in epoch ms, or null if the token does not carry a readable one.
|
|
91
|
+
*
|
|
92
|
+
* Not verification — that is the resource server's job and it holds the key.
|
|
93
|
+
* This reads one number out of a payload the app already holds, for one
|
|
94
|
+
* purpose: so that "is this token still good" is answered by the same fact the
|
|
95
|
+
* service will answer it with.
|
|
96
|
+
*
|
|
97
|
+
* Parse failures return null rather than throwing. A token this cannot read is
|
|
98
|
+
* still a token, and refusing to hold one because its middle segment is not
|
|
99
|
+
* base64url would be a worse failure than the one this prevents.
|
|
100
|
+
*/
|
|
101
|
+
const expFromJwt = (accessToken) => {
|
|
102
|
+
const payload = accessToken.split('.')[1];
|
|
103
|
+
if (!payload)
|
|
104
|
+
return null;
|
|
105
|
+
try {
|
|
106
|
+
const json = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
|
|
107
|
+
const exp = JSON.parse(json).exp;
|
|
108
|
+
return typeof exp === 'number' && Number.isFinite(exp) ? exp * 1000 : null;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
84
114
|
/**
|
|
85
115
|
* Shape a token response into what is kept.
|
|
86
116
|
*
|
|
87
117
|
* The expiry is deliberately pessimistic by 30s, so a token is never presented
|
|
88
118
|
* in the window where it is technically alive and about to not be.
|
|
119
|
+
*
|
|
120
|
+
* ## Why `exp` and not just `expires_in`
|
|
121
|
+
*
|
|
122
|
+
* `expires_in` describes a *duration*, so acting on it means adding it to the
|
|
123
|
+
* local clock — and the service decides using the token's own `exp` against
|
|
124
|
+
* *its* clock. Those two answers agree only while the clocks do.
|
|
125
|
+
*
|
|
126
|
+
* When they diverge in the unsafe direction the app believes a dead token is
|
|
127
|
+
* live, and the failure is genuinely nasty: `POST /sign` answers
|
|
128
|
+
* `401 Invalid token: "exp" claim timestamp check failed`, while `validToken`
|
|
129
|
+
* keeps returning that same token to every caller that asks — including the
|
|
130
|
+
* one Convex asks — so nothing ever renews and nothing ever recovers. Peek got
|
|
131
|
+
* into exactly this state overnight on a machine that had slept, and the only
|
|
132
|
+
* way out was clearing site data and signing in again.
|
|
133
|
+
*
|
|
134
|
+
* So both bounds are computed and **the earlier wins**. `exp` is what the
|
|
135
|
+
* service will actually check; `expires_in` still covers a token with no
|
|
136
|
+
* readable `exp`, and a clock that is *behind* the service's, where `exp`
|
|
137
|
+
* alone would be the more generous of the two.
|
|
89
138
|
*/
|
|
90
|
-
const tokenFrom = (body, pubkey) =>
|
|
91
|
-
accessToken
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
139
|
+
const tokenFrom = (body, pubkey) => {
|
|
140
|
+
const accessToken = body.access_token;
|
|
141
|
+
const fromDuration = now() + Math.max(0, (body.expires_in ?? 600) - 30) * 1000;
|
|
142
|
+
const exp = expFromJwt(accessToken);
|
|
143
|
+
const fromClaim = exp === null ? null : exp - 30_000;
|
|
144
|
+
return {
|
|
145
|
+
accessToken,
|
|
146
|
+
expiresAt: fromClaim === null ? fromDuration : Math.min(fromDuration, fromClaim),
|
|
147
|
+
pubkey,
|
|
148
|
+
...(body.refresh_token ? { refreshToken: body.refresh_token } : {}),
|
|
149
|
+
};
|
|
150
|
+
};
|
|
96
151
|
const storedToken = () => {
|
|
97
152
|
const raw = config.storage()?.getItem(TOKEN_KEY);
|
|
98
153
|
if (!raw)
|
|
@@ -177,6 +232,83 @@ export function createEstivaId(config) {
|
|
|
177
232
|
s?.removeItem(RETURN_KEY);
|
|
178
233
|
return raw ?? null;
|
|
179
234
|
};
|
|
235
|
+
/**
|
|
236
|
+
* Renew the access token without leaving the page (PEEK-108).
|
|
237
|
+
*
|
|
238
|
+
* This is the whole reason the grant exists. Before it, an expired token
|
|
239
|
+
* could only be replaced by a top-level navigation through `/authorize`,
|
|
240
|
+
* which costs whatever the person had on screen.
|
|
241
|
+
*
|
|
242
|
+
* Returns `null` for every failure, and the failures are not worth
|
|
243
|
+
* distinguishing: Estiva ID answers `invalid_grant` for all of them by
|
|
244
|
+
* design, and the caller's only move either way is to fall back to the
|
|
245
|
+
* shell. The stored token is cleared on refusal so a spent refresh token is
|
|
246
|
+
* never presented twice — a replay is what Estiva ID treats as theft.
|
|
247
|
+
*/
|
|
248
|
+
const performRefresh = async () => {
|
|
249
|
+
const s = config.storage();
|
|
250
|
+
const current = storedToken();
|
|
251
|
+
if (!s || !current?.refreshToken)
|
|
252
|
+
return null;
|
|
253
|
+
let response;
|
|
254
|
+
try {
|
|
255
|
+
response = await send()(new URL('/token', config.base).toString(), {
|
|
256
|
+
method: 'POST',
|
|
257
|
+
headers: { 'content-type': 'application/json' },
|
|
258
|
+
body: JSON.stringify({
|
|
259
|
+
grantType: 'refresh_token',
|
|
260
|
+
refreshToken: current.refreshToken,
|
|
261
|
+
clientId: config.clientId,
|
|
262
|
+
}),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// Unreachable rather than refused. The token in hand may still be fine,
|
|
267
|
+
// so this deliberately does not clear the session — a flaky network must
|
|
268
|
+
// not sign somebody out.
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
if (!response.ok) {
|
|
272
|
+
clearSession();
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
const body = (await response.json());
|
|
276
|
+
if (!body.access_token) {
|
|
277
|
+
clearSession();
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
const next = tokenFrom(body, body.pubkey ?? current.pubkey);
|
|
281
|
+
s.setItem(TOKEN_KEY, JSON.stringify(next));
|
|
282
|
+
return next;
|
|
283
|
+
};
|
|
284
|
+
/**
|
|
285
|
+
* The renewal in flight, if there is one.
|
|
286
|
+
*
|
|
287
|
+
* **A refresh token is single-use and a replay is read as theft**, which
|
|
288
|
+
* revokes the whole chain — so two renewals must never be in the air at once.
|
|
289
|
+
* Two easily can be: Ship signs the NIP-98 auth event and the content event as
|
|
290
|
+
* separate `POST /sign` calls, and a token that expires between them answers
|
|
291
|
+
* `401` to both, each of which renews.
|
|
292
|
+
*
|
|
293
|
+
* Peek has guarded this since PEEK-108 with a `refreshing` ref in its Convex
|
|
294
|
+
* adapter, and SHA-4 lists the guard as one of the behaviours that must
|
|
295
|
+
* survive the extraction — but the guard is the one part that did not travel,
|
|
296
|
+
* because it lived in the hook rather than the client. So Peek kept it and
|
|
297
|
+
* Ship never had it. It belongs here, where every caller of every consumer
|
|
298
|
+
* gets it whether or not they thought about it.
|
|
299
|
+
*
|
|
300
|
+
* Coalescing rather than caching: the promise is dropped the moment it
|
|
301
|
+
* settles, so the *next* expiry renews again.
|
|
302
|
+
*/
|
|
303
|
+
let refreshing = null;
|
|
304
|
+
const refreshAccessToken = () => {
|
|
305
|
+
if (refreshing)
|
|
306
|
+
return refreshing;
|
|
307
|
+
refreshing = performRefresh().finally(() => {
|
|
308
|
+
refreshing = null;
|
|
309
|
+
});
|
|
310
|
+
return refreshing;
|
|
311
|
+
};
|
|
180
312
|
return {
|
|
181
313
|
redirectUri: config.redirectUri,
|
|
182
314
|
codeChallenge,
|
|
@@ -327,54 +459,87 @@ export function createEstivaId(config) {
|
|
|
327
459
|
clearGuard(s);
|
|
328
460
|
return { token, returnTo };
|
|
329
461
|
},
|
|
462
|
+
refreshAccessToken,
|
|
330
463
|
/**
|
|
331
|
-
* Renew the
|
|
464
|
+
* Renew shortly **before** the token expires, rather than reacting to it
|
|
465
|
+
* having expired.
|
|
466
|
+
*
|
|
467
|
+
* SHA-4 lists this as one of the behaviours that must survive the
|
|
468
|
+
* extraction, and it is the one that reads as an optimisation and is not.
|
|
469
|
+
* Reacting to expiry means somebody's next action is the thing that
|
|
470
|
+
* discovers the session is over — a failed publish, a blank list, or at best
|
|
471
|
+
* a retry they can feel. Renewing 30 seconds early (`tokenFrom` already
|
|
472
|
+
* subtracts them) means they notice nothing at all.
|
|
473
|
+
*
|
|
474
|
+
* It did not travel with the rest of SHA-4: the timer lived in Peek's Convex
|
|
475
|
+
* adapter, so Peek kept it and Ship, whose whole point was to be the second
|
|
476
|
+
* consumer, silently did not get it. Ship renews only when `POST /sign`
|
|
477
|
+
* answers `401`, which is precisely "on failure".
|
|
478
|
+
*
|
|
479
|
+
* ## The timer is a `setTimeout`, and browsers throttle those
|
|
332
480
|
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
481
|
+
* A background tab gets its timers clamped — to once a minute in Chrome and
|
|
482
|
+
* Safari, and after five minutes of being hidden they may be frozen outright.
|
|
483
|
+
* So a tab left in the background can miss its renewal window entirely.
|
|
336
484
|
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
485
|
+
* This is a known limitation rather than a bug, and it is recorded here so
|
|
486
|
+
* it is not rediscovered once per app. It degrades safely: a late renewal is
|
|
487
|
+
* still a valid renewal, because the refresh token outlives the access token
|
|
488
|
+
* by a long way, and a call that beats the late timer gets the existing
|
|
489
|
+
* `401`-and-retry path. What it must not do is renew *twice* in the scramble,
|
|
490
|
+
* which is what the guard on `refreshAccessToken` is for.
|
|
491
|
+
*
|
|
492
|
+
* A `visibilitychange` listener that re-arms on foreground would tighten
|
|
493
|
+
* this, and is deliberately not here: `document` is a global this package
|
|
494
|
+
* does not touch (ADR 0002 §4a), and an app that wants it can `cancel()` and
|
|
495
|
+
* call this again.
|
|
342
496
|
*/
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
497
|
+
scheduleRenewal(handlers = {}) {
|
|
498
|
+
const setTimer = config.setTimer ?? ((run, ms) => setTimeout(run, ms));
|
|
499
|
+
const clearTimer = config.clearTimer ?? ((handle) => clearTimeout(handle));
|
|
500
|
+
let handle = null;
|
|
501
|
+
let cancelled = false;
|
|
502
|
+
const arm = () => {
|
|
503
|
+
const token = storedToken();
|
|
504
|
+
// Nothing to renew. Not an ending — the caller is simply signed out, and
|
|
505
|
+
// an app that signs in later starts a new schedule.
|
|
506
|
+
if (!token)
|
|
507
|
+
return;
|
|
508
|
+
/*
|
|
509
|
+
Clamped at both ends. A negative delay is a token that already expired
|
|
510
|
+
— renew now rather than never, since the refresh token almost certainly
|
|
511
|
+
has not. The ceiling is `setTimeout`'s: a delay above 2^31-1 ms
|
|
512
|
+
overflows to a negative int32 and fires immediately, so a nonsensical
|
|
513
|
+
`expires_in` would otherwise become a hot loop against `/token` instead
|
|
514
|
+
of one very distant timer.
|
|
515
|
+
*/
|
|
516
|
+
const delay = Math.min(Math.max(0, token.expiresAt - now()), MAX_TIMER_MS);
|
|
517
|
+
handle = setTimer(() => {
|
|
518
|
+
handle = null;
|
|
519
|
+
void refreshAccessToken().then((next) => {
|
|
520
|
+
if (cancelled)
|
|
521
|
+
return;
|
|
522
|
+
if (!next) {
|
|
523
|
+
handlers.onEnded?.();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
handlers.onRenewed?.(next);
|
|
527
|
+
// Re-armed from the *new* expiry rather than a fixed interval, so
|
|
528
|
+
// the schedule follows whatever lifetime Estiva ID actually issued.
|
|
529
|
+
arm();
|
|
530
|
+
});
|
|
531
|
+
}, delay);
|
|
532
|
+
};
|
|
533
|
+
arm();
|
|
534
|
+
return {
|
|
535
|
+
cancel() {
|
|
536
|
+
cancelled = true;
|
|
537
|
+
if (handle !== null) {
|
|
538
|
+
clearTimer(handle);
|
|
539
|
+
handle = null;
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
};
|
|
378
543
|
},
|
|
379
544
|
/**
|
|
380
545
|
* Leave for Estiva ID to end the session *there* (PEEK-122).
|
package/dist/client.js.map
CHANGED
|
@@ -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.
|
|
49
|
+
export declare const IDENTITY_VERSION = "0.1.3";
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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.
|
|
49
|
+
export const IDENTITY_VERSION = '0.1.3';
|
|
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,
|
|
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.
|
|
3
|
+
"version": "0.1.3",
|
|
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
|
@@ -67,12 +67,27 @@ declare const crypto: {
|
|
|
67
67
|
subtle: { digest(algorithm: string, data: Uint8Array): Promise<ArrayBuffer> }
|
|
68
68
|
}
|
|
69
69
|
declare const btoa: (binary: string) => string
|
|
70
|
+
/** Reading the `exp` claim out of a token the app already holds. */
|
|
71
|
+
declare const atob: (base64: string) => string
|
|
70
72
|
declare const TextEncoder: { new (): { encode(input: string): Uint8Array } }
|
|
71
73
|
declare const URL: {
|
|
72
74
|
new (url: string, base?: string): { searchParams: { set(k: string, v: string): void }; toString(): string }
|
|
73
75
|
}
|
|
74
76
|
declare const URLSearchParams: { new (init: string): { get(name: string): string | null } }
|
|
75
77
|
|
|
78
|
+
/**
|
|
79
|
+
* The timer pair, for {@link EstivaIdClient.scheduleRenewal}.
|
|
80
|
+
*
|
|
81
|
+
* Declared here for the same reason as everything above, and returning
|
|
82
|
+
* `unknown` for one more: the handle's real type is `number` in a browser and an
|
|
83
|
+
* object in Node, and naming either would put an ambient type in the published
|
|
84
|
+
* `.d.ts` — which `check-identity.yml` fails the build over, correctly. Nothing
|
|
85
|
+
* outside this module ever sees a handle; `scheduleRenewal` hands back a
|
|
86
|
+
* `cancel` function instead.
|
|
87
|
+
*/
|
|
88
|
+
declare const setTimeout: (run: () => void, ms: number) => unknown
|
|
89
|
+
declare const clearTimeout: (handle: unknown) => void
|
|
90
|
+
|
|
76
91
|
/** Just enough of `fetch` to redeem a code. A parameter as well as a global. */
|
|
77
92
|
export type FetchLike = (
|
|
78
93
|
url: string,
|
|
@@ -122,6 +137,14 @@ export interface EstivaIdConfig {
|
|
|
122
137
|
navigate?: (url: string) => void
|
|
123
138
|
now?: () => number
|
|
124
139
|
fetch?: FetchLike
|
|
140
|
+
/**
|
|
141
|
+
* The timers `scheduleRenewal` runs on, injected for exactly the reason
|
|
142
|
+
* `fetch` and `navigate` are: a test cannot wait ten minutes to find out
|
|
143
|
+
* whether the renewal was scheduled, and "we scheduled it" and "we did
|
|
144
|
+
* nothing" are otherwise the same observation.
|
|
145
|
+
*/
|
|
146
|
+
setTimer?: (run: () => void, ms: number) => unknown
|
|
147
|
+
clearTimer?: (handle: unknown) => void
|
|
125
148
|
}
|
|
126
149
|
|
|
127
150
|
export interface StoredToken {
|
|
@@ -143,8 +166,35 @@ export interface StoredToken {
|
|
|
143
166
|
refreshToken?: string
|
|
144
167
|
}
|
|
145
168
|
|
|
169
|
+
/**
|
|
170
|
+
* `setTimeout`'s ceiling. Above 2^31-1 ms the delay overflows to a negative
|
|
171
|
+
* int32 and the timer fires immediately.
|
|
172
|
+
*/
|
|
173
|
+
const MAX_TIMER_MS = 2_147_483_647
|
|
174
|
+
|
|
146
175
|
export class SignInError extends Error {}
|
|
147
176
|
|
|
177
|
+
/** What a scheduled renewal reports back. Both optional; neither is required to be useful. */
|
|
178
|
+
export interface RenewalHandlers {
|
|
179
|
+
/** A renewal succeeded. The session continues, on a later expiry. */
|
|
180
|
+
onRenewed?: (token: StoredToken) => void
|
|
181
|
+
/**
|
|
182
|
+
* A renewal failed, and the session is over.
|
|
183
|
+
*
|
|
184
|
+
* The only honest thing an app can do here is drop to the shell. Note this
|
|
185
|
+
* fires on refusal *and* on an unreachable network — `refreshAccessToken`
|
|
186
|
+
* distinguishes them for the purpose of clearing the stored token, but from a
|
|
187
|
+
* scheduler's point of view a renewal that did not happen is a renewal that
|
|
188
|
+
* did not happen, and the shell's own probe is what sorts out which it was.
|
|
189
|
+
*/
|
|
190
|
+
onEnded?: () => void
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface RenewalSchedule {
|
|
194
|
+
/** Stop renewing. Idempotent, and safe to call after the session has ended. */
|
|
195
|
+
cancel: () => void
|
|
196
|
+
}
|
|
197
|
+
|
|
148
198
|
interface TokenResponse {
|
|
149
199
|
access_token?: string
|
|
150
200
|
expires_in?: number
|
|
@@ -158,6 +208,7 @@ export interface EstivaIdClient {
|
|
|
158
208
|
beginSignIn: (returnTo: string, options?: { silent?: boolean }) => Promise<void>
|
|
159
209
|
completeSignIn: (search: string) => Promise<{ token: StoredToken; returnTo: string }>
|
|
160
210
|
refreshAccessToken: () => Promise<StoredToken | null>
|
|
211
|
+
scheduleRenewal: (handlers?: RenewalHandlers) => RenewalSchedule
|
|
161
212
|
storedToken: () => StoredToken | null
|
|
162
213
|
validToken: (now?: number) => StoredToken | null
|
|
163
214
|
hasSession: () => boolean
|
|
@@ -202,18 +253,67 @@ export function createEstivaId(config: EstivaIdConfig): EstivaIdClient {
|
|
|
202
253
|
return base64Url(new Uint8Array(digest))
|
|
203
254
|
}
|
|
204
255
|
|
|
256
|
+
/**
|
|
257
|
+
* The `exp` claim, in epoch ms, or null if the token does not carry a readable one.
|
|
258
|
+
*
|
|
259
|
+
* Not verification — that is the resource server's job and it holds the key.
|
|
260
|
+
* This reads one number out of a payload the app already holds, for one
|
|
261
|
+
* purpose: so that "is this token still good" is answered by the same fact the
|
|
262
|
+
* service will answer it with.
|
|
263
|
+
*
|
|
264
|
+
* Parse failures return null rather than throwing. A token this cannot read is
|
|
265
|
+
* still a token, and refusing to hold one because its middle segment is not
|
|
266
|
+
* base64url would be a worse failure than the one this prevents.
|
|
267
|
+
*/
|
|
268
|
+
const expFromJwt = (accessToken: string): number | null => {
|
|
269
|
+
const payload = accessToken.split('.')[1]
|
|
270
|
+
if (!payload) return null
|
|
271
|
+
try {
|
|
272
|
+
const json = atob(payload.replace(/-/g, '+').replace(/_/g, '/'))
|
|
273
|
+
const exp = (JSON.parse(json) as { exp?: unknown }).exp
|
|
274
|
+
return typeof exp === 'number' && Number.isFinite(exp) ? exp * 1000 : null
|
|
275
|
+
} catch {
|
|
276
|
+
return null
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
205
280
|
/**
|
|
206
281
|
* Shape a token response into what is kept.
|
|
207
282
|
*
|
|
208
283
|
* The expiry is deliberately pessimistic by 30s, so a token is never presented
|
|
209
284
|
* in the window where it is technically alive and about to not be.
|
|
285
|
+
*
|
|
286
|
+
* ## Why `exp` and not just `expires_in`
|
|
287
|
+
*
|
|
288
|
+
* `expires_in` describes a *duration*, so acting on it means adding it to the
|
|
289
|
+
* local clock — and the service decides using the token's own `exp` against
|
|
290
|
+
* *its* clock. Those two answers agree only while the clocks do.
|
|
291
|
+
*
|
|
292
|
+
* When they diverge in the unsafe direction the app believes a dead token is
|
|
293
|
+
* live, and the failure is genuinely nasty: `POST /sign` answers
|
|
294
|
+
* `401 Invalid token: "exp" claim timestamp check failed`, while `validToken`
|
|
295
|
+
* keeps returning that same token to every caller that asks — including the
|
|
296
|
+
* one Convex asks — so nothing ever renews and nothing ever recovers. Peek got
|
|
297
|
+
* into exactly this state overnight on a machine that had slept, and the only
|
|
298
|
+
* way out was clearing site data and signing in again.
|
|
299
|
+
*
|
|
300
|
+
* So both bounds are computed and **the earlier wins**. `exp` is what the
|
|
301
|
+
* service will actually check; `expires_in` still covers a token with no
|
|
302
|
+
* readable `exp`, and a clock that is *behind* the service's, where `exp`
|
|
303
|
+
* alone would be the more generous of the two.
|
|
210
304
|
*/
|
|
211
|
-
const tokenFrom = (body: TokenResponse, pubkey: string): StoredToken =>
|
|
212
|
-
accessToken
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
305
|
+
const tokenFrom = (body: TokenResponse, pubkey: string): StoredToken => {
|
|
306
|
+
const accessToken = body.access_token as string
|
|
307
|
+
const fromDuration = now() + Math.max(0, (body.expires_in ?? 600) - 30) * 1000
|
|
308
|
+
const exp = expFromJwt(accessToken)
|
|
309
|
+
const fromClaim = exp === null ? null : exp - 30_000
|
|
310
|
+
return {
|
|
311
|
+
accessToken,
|
|
312
|
+
expiresAt: fromClaim === null ? fromDuration : Math.min(fromDuration, fromClaim),
|
|
313
|
+
pubkey,
|
|
314
|
+
...(body.refresh_token ? { refreshToken: body.refresh_token } : {}),
|
|
315
|
+
}
|
|
316
|
+
}
|
|
217
317
|
|
|
218
318
|
const storedToken = (): StoredToken | null => {
|
|
219
319
|
const raw = config.storage()?.getItem(TOKEN_KEY)
|
|
@@ -300,6 +400,88 @@ export function createEstivaId(config: EstivaIdConfig): EstivaIdClient {
|
|
|
300
400
|
return raw ?? null
|
|
301
401
|
}
|
|
302
402
|
|
|
403
|
+
/**
|
|
404
|
+
* Renew the access token without leaving the page (PEEK-108).
|
|
405
|
+
*
|
|
406
|
+
* This is the whole reason the grant exists. Before it, an expired token
|
|
407
|
+
* could only be replaced by a top-level navigation through `/authorize`,
|
|
408
|
+
* which costs whatever the person had on screen.
|
|
409
|
+
*
|
|
410
|
+
* Returns `null` for every failure, and the failures are not worth
|
|
411
|
+
* distinguishing: Estiva ID answers `invalid_grant` for all of them by
|
|
412
|
+
* design, and the caller's only move either way is to fall back to the
|
|
413
|
+
* shell. The stored token is cleared on refusal so a spent refresh token is
|
|
414
|
+
* never presented twice — a replay is what Estiva ID treats as theft.
|
|
415
|
+
*/
|
|
416
|
+
const performRefresh = async (): Promise<StoredToken | null> => {
|
|
417
|
+
const s = config.storage()
|
|
418
|
+
const current = storedToken()
|
|
419
|
+
if (!s || !current?.refreshToken) return null
|
|
420
|
+
|
|
421
|
+
let response: Awaited<ReturnType<FetchLike>>
|
|
422
|
+
try {
|
|
423
|
+
response = await send()(new URL('/token', config.base).toString(), {
|
|
424
|
+
method: 'POST',
|
|
425
|
+
headers: { 'content-type': 'application/json' },
|
|
426
|
+
body: JSON.stringify({
|
|
427
|
+
grantType: 'refresh_token',
|
|
428
|
+
refreshToken: current.refreshToken,
|
|
429
|
+
clientId: config.clientId,
|
|
430
|
+
}),
|
|
431
|
+
})
|
|
432
|
+
} catch {
|
|
433
|
+
// Unreachable rather than refused. The token in hand may still be fine,
|
|
434
|
+
// so this deliberately does not clear the session — a flaky network must
|
|
435
|
+
// not sign somebody out.
|
|
436
|
+
return null
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!response.ok) {
|
|
440
|
+
clearSession()
|
|
441
|
+
return null
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const body = (await response.json()) as TokenResponse
|
|
445
|
+
if (!body.access_token) {
|
|
446
|
+
clearSession()
|
|
447
|
+
return null
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const next = tokenFrom(body, body.pubkey ?? current.pubkey)
|
|
451
|
+
s.setItem(TOKEN_KEY, JSON.stringify(next))
|
|
452
|
+
return next
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* The renewal in flight, if there is one.
|
|
457
|
+
*
|
|
458
|
+
* **A refresh token is single-use and a replay is read as theft**, which
|
|
459
|
+
* revokes the whole chain — so two renewals must never be in the air at once.
|
|
460
|
+
* Two easily can be: Ship signs the NIP-98 auth event and the content event as
|
|
461
|
+
* separate `POST /sign` calls, and a token that expires between them answers
|
|
462
|
+
* `401` to both, each of which renews.
|
|
463
|
+
*
|
|
464
|
+
* Peek has guarded this since PEEK-108 with a `refreshing` ref in its Convex
|
|
465
|
+
* adapter, and SHA-4 lists the guard as one of the behaviours that must
|
|
466
|
+
* survive the extraction — but the guard is the one part that did not travel,
|
|
467
|
+
* because it lived in the hook rather than the client. So Peek kept it and
|
|
468
|
+
* Ship never had it. It belongs here, where every caller of every consumer
|
|
469
|
+
* gets it whether or not they thought about it.
|
|
470
|
+
*
|
|
471
|
+
* Coalescing rather than caching: the promise is dropped the moment it
|
|
472
|
+
* settles, so the *next* expiry renews again.
|
|
473
|
+
*/
|
|
474
|
+
let refreshing: Promise<StoredToken | null> | null = null
|
|
475
|
+
|
|
476
|
+
const refreshAccessToken = (): Promise<StoredToken | null> => {
|
|
477
|
+
if (refreshing) return refreshing
|
|
478
|
+
refreshing = performRefresh().finally(() => {
|
|
479
|
+
refreshing = null
|
|
480
|
+
})
|
|
481
|
+
return refreshing
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
|
|
303
485
|
return {
|
|
304
486
|
redirectUri: config.redirectUri,
|
|
305
487
|
codeChallenge,
|
|
@@ -464,58 +646,95 @@ export function createEstivaId(config: EstivaIdConfig): EstivaIdClient {
|
|
|
464
646
|
return { token, returnTo }
|
|
465
647
|
},
|
|
466
648
|
|
|
649
|
+
refreshAccessToken,
|
|
650
|
+
|
|
467
651
|
/**
|
|
468
|
-
* Renew the
|
|
652
|
+
* Renew shortly **before** the token expires, rather than reacting to it
|
|
653
|
+
* having expired.
|
|
469
654
|
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
655
|
+
* SHA-4 lists this as one of the behaviours that must survive the
|
|
656
|
+
* extraction, and it is the one that reads as an optimisation and is not.
|
|
657
|
+
* Reacting to expiry means somebody's next action is the thing that
|
|
658
|
+
* discovers the session is over — a failed publish, a blank list, or at best
|
|
659
|
+
* a retry they can feel. Renewing 30 seconds early (`tokenFrom` already
|
|
660
|
+
* subtracts them) means they notice nothing at all.
|
|
473
661
|
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
662
|
+
* It did not travel with the rest of SHA-4: the timer lived in Peek's Convex
|
|
663
|
+
* adapter, so Peek kept it and Ship, whose whole point was to be the second
|
|
664
|
+
* consumer, silently did not get it. Ship renews only when `POST /sign`
|
|
665
|
+
* answers `401`, which is precisely "on failure".
|
|
666
|
+
*
|
|
667
|
+
* ## The timer is a `setTimeout`, and browsers throttle those
|
|
668
|
+
*
|
|
669
|
+
* A background tab gets its timers clamped — to once a minute in Chrome and
|
|
670
|
+
* Safari, and after five minutes of being hidden they may be frozen outright.
|
|
671
|
+
* So a tab left in the background can miss its renewal window entirely.
|
|
672
|
+
*
|
|
673
|
+
* This is a known limitation rather than a bug, and it is recorded here so
|
|
674
|
+
* it is not rediscovered once per app. It degrades safely: a late renewal is
|
|
675
|
+
* still a valid renewal, because the refresh token outlives the access token
|
|
676
|
+
* by a long way, and a call that beats the late timer gets the existing
|
|
677
|
+
* `401`-and-retry path. What it must not do is renew *twice* in the scramble,
|
|
678
|
+
* which is what the guard on `refreshAccessToken` is for.
|
|
679
|
+
*
|
|
680
|
+
* A `visibilitychange` listener that re-arms on foreground would tighten
|
|
681
|
+
* this, and is deliberately not here: `document` is a global this package
|
|
682
|
+
* does not touch (ADR 0002 §4a), and an app that wants it can `cancel()` and
|
|
683
|
+
* call this again.
|
|
479
684
|
*/
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
let
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
685
|
+
scheduleRenewal(handlers: RenewalHandlers = {}): RenewalSchedule {
|
|
686
|
+
const setTimer = config.setTimer ?? ((run, ms) => setTimeout(run, ms))
|
|
687
|
+
const clearTimer = config.clearTimer ?? ((handle) => clearTimeout(handle))
|
|
688
|
+
|
|
689
|
+
let handle: unknown = null
|
|
690
|
+
let cancelled = false
|
|
691
|
+
|
|
692
|
+
const arm = (): void => {
|
|
693
|
+
const token = storedToken()
|
|
694
|
+
// Nothing to renew. Not an ending — the caller is simply signed out, and
|
|
695
|
+
// an app that signs in later starts a new schedule.
|
|
696
|
+
if (!token) return
|
|
697
|
+
|
|
698
|
+
/*
|
|
699
|
+
Clamped at both ends. A negative delay is a token that already expired
|
|
700
|
+
— renew now rather than never, since the refresh token almost certainly
|
|
701
|
+
has not. The ceiling is `setTimeout`'s: a delay above 2^31-1 ms
|
|
702
|
+
overflows to a negative int32 and fires immediately, so a nonsensical
|
|
703
|
+
`expires_in` would otherwise become a hot loop against `/token` instead
|
|
704
|
+
of one very distant timer.
|
|
705
|
+
*/
|
|
706
|
+
const delay = Math.min(Math.max(0, token.expiresAt - now()), MAX_TIMER_MS)
|
|
707
|
+
|
|
708
|
+
handle = setTimer(() => {
|
|
709
|
+
handle = null
|
|
710
|
+
void refreshAccessToken().then((next) => {
|
|
711
|
+
if (cancelled) return
|
|
712
|
+
if (!next) {
|
|
713
|
+
handlers.onEnded?.()
|
|
714
|
+
return
|
|
715
|
+
}
|
|
716
|
+
handlers.onRenewed?.(next)
|
|
717
|
+
// Re-armed from the *new* expiry rather than a fixed interval, so
|
|
718
|
+
// the schedule follows whatever lifetime Estiva ID actually issued.
|
|
719
|
+
arm()
|
|
720
|
+
})
|
|
721
|
+
}, delay)
|
|
501
722
|
}
|
|
502
723
|
|
|
503
|
-
|
|
504
|
-
clearSession()
|
|
505
|
-
return null
|
|
506
|
-
}
|
|
724
|
+
arm()
|
|
507
725
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
726
|
+
return {
|
|
727
|
+
cancel(): void {
|
|
728
|
+
cancelled = true
|
|
729
|
+
if (handle !== null) {
|
|
730
|
+
clearTimer(handle)
|
|
731
|
+
handle = null
|
|
732
|
+
}
|
|
733
|
+
},
|
|
512
734
|
}
|
|
513
|
-
|
|
514
|
-
const next = tokenFrom(body, body.pubkey ?? current.pubkey)
|
|
515
|
-
s.setItem(TOKEN_KEY, JSON.stringify(next))
|
|
516
|
-
return next
|
|
517
735
|
},
|
|
518
736
|
|
|
737
|
+
|
|
519
738
|
/**
|
|
520
739
|
* Leave for Estiva ID to end the session *there* (PEEK-122).
|
|
521
740
|
*
|
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.
|
|
49
|
+
export const IDENTITY_VERSION = '0.1.3'
|
|
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'
|