@estiva-app/identity 0.1.0 → 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 +76 -0
- package/README.md +50 -4
- package/dist/client.d.ts +60 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +184 -63
- 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 +259 -60
- package/src/index.ts +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,81 @@
|
|
|
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
|
+
|
|
53
|
+
## 0.1.1 — 2026-08-28
|
|
54
|
+
|
|
55
|
+
**No wire behaviour**, and no break: `pendingStore` is optional and defaults to
|
|
56
|
+
`storage`, so a consumer on `^0.1.0` needs no change. Additive, so a PATCH —
|
|
57
|
+
ADR 0002 §4b puts the break on MINOR within `0.x`.
|
|
58
|
+
|
|
59
|
+
- **`pendingStore`**, a second storage seam. Wiring Ship — the second consumer —
|
|
60
|
+
found that the package assumed **one** store because it was extracted from
|
|
61
|
+
Peek, which uses one. Ship uses two, with different lifetimes on purpose: its
|
|
62
|
+
*session* lives in `localStorage` so it outlives a tab, and its *in-flight PKCE
|
|
63
|
+
credentials* live in `sessionStorage` because the flow starts and ends in one
|
|
64
|
+
tab, which is also why the silent-attempt guard belongs there.
|
|
65
|
+
|
|
66
|
+
This is the exact "a library pulled from one app comes out shaped like that
|
|
67
|
+
app" failure SHA-4 names, found by the mechanism SHA-4 prescribes for finding
|
|
68
|
+
it. The ticket said wiring Ship was the point rather than a formality; it was
|
|
69
|
+
right, and this is what it caught.
|
|
70
|
+
|
|
71
|
+
`clearSession` now spans both stores — the token from `storage`, the shell
|
|
72
|
+
reason from `pendingStore` — while still leaving the verifier, state and
|
|
73
|
+
returnTo alone. PEEK-167's separation had to survive the split, or the split
|
|
74
|
+
would have reintroduced the bug the separation exists to prevent. A test
|
|
75
|
+
asserts exactly that, and a control asserts the single-store default still
|
|
76
|
+
behaves as Peek expects, so none of it can pass on a package that quietly
|
|
77
|
+
ignores the new parameter.
|
|
78
|
+
|
|
3
79
|
## 0.1.0 — 2026-08-27
|
|
4
80
|
|
|
5
81
|
First release. SHA-4, extracted during REW-2.
|
package/README.md
CHANGED
|
@@ -38,13 +38,21 @@ package come out Peek-shaped, which is the whole risk SHA-4 names.
|
|
|
38
38
|
| --- | --- | --- |
|
|
39
39
|
| `clientId` | `estiva-peek` | `estiva-ship` |
|
|
40
40
|
| `redirectUri` | a fixed `/auth/callback` | its current pathname — Ship routes on `location.hash` |
|
|
41
|
-
| `storage` | `sessionStorage`,
|
|
41
|
+
| `storage` — the session | `sessionStorage`, dies with the tab | `localStorage`, outlives a tab |
|
|
42
|
+
| `pendingStore` — in-flight PKCE credentials | defaults to `storage` | `sessionStorage`, because the flow starts and ends in one tab |
|
|
42
43
|
| `keyPrefix` | `peek.estivaId` | `ship.estiva-id` |
|
|
43
44
|
| `navigate` | the one genuinely untestable act, so it is observable | |
|
|
45
|
+
| `setTimer` / `clearTimer` | the timers `scheduleRenewal` runs on, injected for the same reason | |
|
|
44
46
|
|
|
45
|
-
**Do not unify
|
|
46
|
-
and belong in `localStorage`; an access token does not. Different
|
|
47
|
-
different homes.
|
|
47
|
+
**Do not unify the two stores.** NIP-RS read-state slots (CRO-4) must survive a
|
|
48
|
+
restart and belong in `localStorage`; an access token does not. Different
|
|
49
|
+
lifetimes, different homes.
|
|
50
|
+
|
|
51
|
+
`pendingStore` exists because wiring the second consumer found the package
|
|
52
|
+
assuming one store — it was extracted from Peek, which uses one. That is the "a
|
|
53
|
+
library pulled from one app comes out shaped like that app" failure SHA-4 names,
|
|
54
|
+
caught by the mechanism SHA-4 prescribes. Worth knowing as evidence that the
|
|
55
|
+
second-consumer rule earns its keep rather than being ceremony.
|
|
48
56
|
|
|
49
57
|
An app instantiates once and re-exports its own names, so call sites do not churn:
|
|
50
58
|
|
|
@@ -60,6 +68,44 @@ const client = createEstivaId({
|
|
|
60
68
|
export const { validToken, beginSignIn, completeSignIn, beginSignOut } = client
|
|
61
69
|
```
|
|
62
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
|
+
|
|
63
109
|
## The prediction this package falsified
|
|
64
110
|
|
|
65
111
|
Before starting, the expectation on the ticket was that identity **could not**
|
package/dist/client.d.ts
CHANGED
|
@@ -38,11 +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`**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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.
|
|
46
49
|
* - **`keyPrefix`** — so two apps on one origin cannot read each other's keys.
|
|
47
50
|
* - **`navigate`** — the one genuinely untestable act. Injected so a test can
|
|
48
51
|
* observe where the flow *would* go.
|
|
@@ -68,14 +71,45 @@ export interface EstivaIdConfig {
|
|
|
68
71
|
* **exactly** — a trailing slash is a refused sign-in.
|
|
69
72
|
*/
|
|
70
73
|
redirectUri: () => string;
|
|
71
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Where the **session** lives — the token and nothing else.
|
|
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.
|
|
80
|
+
*/
|
|
72
81
|
storage: () => KeyValueStore | null;
|
|
82
|
+
/**
|
|
83
|
+
* Where an **in-flight sign-in's** single-use credentials live — the verifier,
|
|
84
|
+
* the state, the returnTo, the shell reason and the silent-attempt guard.
|
|
85
|
+
*
|
|
86
|
+
* Defaults to {@link storage}, which is what Peek wants: one store for both.
|
|
87
|
+
*
|
|
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.
|
|
92
|
+
*
|
|
93
|
+
* This parameter was added by wiring the second consumer. The package had
|
|
94
|
+
* assumed one store because it was extracted from Peek, which uses one: the
|
|
95
|
+
* exact "comes out shaped like the app it came from" failure SHA-4 names, found
|
|
96
|
+
* by the mechanism SHA-4 prescribes for finding it.
|
|
97
|
+
*/
|
|
98
|
+
pendingStore?: () => KeyValueStore | null;
|
|
73
99
|
/** Namespaces this app's keys, e.g. `peek.estivaId`. */
|
|
74
100
|
keyPrefix: string;
|
|
75
101
|
/** Defaults to the runtime's `location.assign`-equivalent via `navigate`. */
|
|
76
102
|
navigate?: (url: string) => void;
|
|
77
103
|
now?: () => number;
|
|
78
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;
|
|
79
113
|
}
|
|
80
114
|
export interface StoredToken {
|
|
81
115
|
accessToken: string;
|
|
@@ -97,6 +131,25 @@ export interface StoredToken {
|
|
|
97
131
|
}
|
|
98
132
|
export declare class SignInError extends Error {
|
|
99
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
|
+
}
|
|
100
153
|
export interface EstivaIdClient {
|
|
101
154
|
redirectUri: () => string;
|
|
102
155
|
codeChallenge: (verifier: string) => Promise<string>;
|
|
@@ -108,6 +161,7 @@ export interface EstivaIdClient {
|
|
|
108
161
|
returnTo: string;
|
|
109
162
|
}>;
|
|
110
163
|
refreshAccessToken: () => Promise<StoredToken | null>;
|
|
164
|
+
scheduleRenewal: (handlers?: RenewalHandlers) => RenewalSchedule;
|
|
111
165
|
storedToken: () => StoredToken | null;
|
|
112
166
|
validToken: (now?: number) => StoredToken | null;
|
|
113
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
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -38,20 +38,30 @@
|
|
|
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`**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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.
|
|
46
49
|
* - **`keyPrefix`** — so two apps on one origin cannot read each other's keys.
|
|
47
50
|
* - **`navigate`** — the one genuinely untestable act. Injected so a test can
|
|
48
51
|
* observe where the flow *would* go.
|
|
49
52
|
*/
|
|
50
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;
|
|
51
59
|
export class SignInError extends Error {
|
|
52
60
|
}
|
|
53
61
|
export function createEstivaId(config) {
|
|
54
62
|
const now = config.now ?? (() => Date.now());
|
|
63
|
+
/** In-flight credentials, which may live somewhere shorter than the session. */
|
|
64
|
+
const pending = config.pendingStore ?? config.storage;
|
|
55
65
|
const send = () => config.fetch ?? fetch;
|
|
56
66
|
const go = (url) => {
|
|
57
67
|
if (!config.navigate)
|
|
@@ -126,11 +136,11 @@ export function createEstivaId(config) {
|
|
|
126
136
|
* Anything that wants both says so, and only `beginSignOut` does.
|
|
127
137
|
*/
|
|
128
138
|
const clearSession = () => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
139
|
+
config.storage()?.removeItem(TOKEN_KEY);
|
|
140
|
+
// The reason lives with the in-flight credentials, which may be a different
|
|
141
|
+
// store — see `pendingStore`. Still cleared here: a reason outliving the
|
|
142
|
+
// session it described is a lie, and that was true before the split.
|
|
143
|
+
pending()?.removeItem(REASON_KEY);
|
|
134
144
|
};
|
|
135
145
|
/**
|
|
136
146
|
* Abandon a sign-in that has not come back.
|
|
@@ -141,18 +151,18 @@ export function createEstivaId(config) {
|
|
|
141
151
|
* different challenge. Destroying a live one is the expensive direction.
|
|
142
152
|
*/
|
|
143
153
|
const clearPendingSignIn = () => {
|
|
144
|
-
const s =
|
|
154
|
+
const s = pending();
|
|
145
155
|
if (!s)
|
|
146
156
|
return;
|
|
147
157
|
for (const k of [VERIFIER_KEY, STATE_KEY, RETURN_KEY])
|
|
148
158
|
s.removeItem(k);
|
|
149
159
|
};
|
|
150
160
|
const stashShellReason = (reason) => {
|
|
151
|
-
|
|
161
|
+
pending()?.setItem(REASON_KEY, reason);
|
|
152
162
|
};
|
|
153
163
|
/** Read once. Single-use: a stale reason outliving its cause is a lie. */
|
|
154
164
|
const takeShellReason = () => {
|
|
155
|
-
const s =
|
|
165
|
+
const s = pending();
|
|
156
166
|
const raw = s?.getItem(REASON_KEY);
|
|
157
167
|
if (raw)
|
|
158
168
|
s?.removeItem(REASON_KEY);
|
|
@@ -166,12 +176,89 @@ export function createEstivaId(config) {
|
|
|
166
176
|
* puts people on a default screen instead of the link they opened (PEEK-123).
|
|
167
177
|
*/
|
|
168
178
|
const takeReturnTo = () => {
|
|
169
|
-
const s =
|
|
179
|
+
const s = pending();
|
|
170
180
|
const raw = s?.getItem(RETURN_KEY);
|
|
171
181
|
if (raw)
|
|
172
182
|
s?.removeItem(RETURN_KEY);
|
|
173
183
|
return raw ?? null;
|
|
174
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
|
+
};
|
|
175
262
|
return {
|
|
176
263
|
redirectUri: config.redirectUri,
|
|
177
264
|
codeChallenge,
|
|
@@ -202,7 +289,7 @@ export function createEstivaId(config) {
|
|
|
202
289
|
* code that just arrived belongs to anything this tab did.
|
|
203
290
|
*/
|
|
204
291
|
hasPendingSignIn: () => {
|
|
205
|
-
const s =
|
|
292
|
+
const s = pending();
|
|
206
293
|
return Boolean(s?.getItem(VERIFIER_KEY) && s.getItem(STATE_KEY));
|
|
207
294
|
},
|
|
208
295
|
/**
|
|
@@ -214,7 +301,7 @@ export function createEstivaId(config) {
|
|
|
214
301
|
* compared.
|
|
215
302
|
*/
|
|
216
303
|
async beginSignIn(returnTo, { silent = false } = {}) {
|
|
217
|
-
const s =
|
|
304
|
+
const s = pending();
|
|
218
305
|
if (!s)
|
|
219
306
|
throw new Error('sign-in requires a browser');
|
|
220
307
|
const verifier = randomUrlSafe();
|
|
@@ -268,8 +355,9 @@ export function createEstivaId(config) {
|
|
|
268
355
|
* on this side of the exchange.
|
|
269
356
|
*/
|
|
270
357
|
async completeSignIn(search) {
|
|
271
|
-
const s =
|
|
272
|
-
|
|
358
|
+
const s = pending();
|
|
359
|
+
const sessionStore = config.storage();
|
|
360
|
+
if (!s || !sessionStore)
|
|
273
361
|
throw new SignInError('sign-in requires a browser');
|
|
274
362
|
const params = new URLSearchParams(search);
|
|
275
363
|
const error = params.get('error');
|
|
@@ -316,59 +404,92 @@ export function createEstivaId(config) {
|
|
|
316
404
|
if (!body.access_token || !body.pubkey)
|
|
317
405
|
throw new SignInError('Estiva ID returned no token.');
|
|
318
406
|
const token = tokenFrom(body, body.pubkey);
|
|
319
|
-
|
|
407
|
+
sessionStore.setItem(TOKEN_KEY, JSON.stringify(token));
|
|
320
408
|
// The attempt worked, so the next boot is entitled to a fresh silent one.
|
|
321
409
|
clearGuard(s);
|
|
322
410
|
return { token, returnTo };
|
|
323
411
|
},
|
|
412
|
+
refreshAccessToken,
|
|
324
413
|
/**
|
|
325
|
-
* Renew the
|
|
414
|
+
* Renew shortly **before** the token expires, rather than reacting to it
|
|
415
|
+
* having expired.
|
|
416
|
+
*
|
|
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.
|
|
423
|
+
*
|
|
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.
|
|
326
434
|
*
|
|
327
|
-
* This is
|
|
328
|
-
*
|
|
329
|
-
*
|
|
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.
|
|
330
441
|
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
* never presented twice — a replay is what Estiva ID treats as theft.
|
|
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.
|
|
336
446
|
*/
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
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
|
+
};
|
|
372
493
|
},
|
|
373
494
|
/**
|
|
374
495
|
* 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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,OAAO,EAAwC,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAsExF,MAAM,OAAO,WAAY,SAAQ,KAAK;CAAG;AA4BzC,MAAM,UAAU,cAAc,CAAC,MAAsB;IACnD,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;IAC5C,MAAM,IAAI,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,IAAI,KAAK,CAAA;IACxC,MAAM,EAAE,GAAG,CAAC,GAAW,EAAE,EAAE;QACzB,IAAI,CAAC,MAAM,CAAC,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAA;QACpG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;IACtB,CAAC,CAAA;IAED,MAAM,YAAY,GAAG,GAAG,MAAM,CAAC,SAAS,eAAe,CAAA;IACvD,MAAM,SAAS,GAAG,GAAG,MAAM,CAAC,SAAS,QAAQ,CAAA;IAC7C,MAAM,UAAU,GAAG,GAAG,MAAM,CAAC,SAAS,WAAW,CAAA;IACjD,MAAM,SAAS,GAAG,GAAG,MAAM,CAAC,SAAS,QAAQ,CAAA;IAC7C,sFAAsF;IACtF,MAAM,UAAU,GAAG,GAAG,MAAM,CAAC,SAAS,cAAc,CAAA;IAEpD,MAAM,SAAS,GAAG,CAAC,KAAiB,EAAU,EAAE;QAC9C,IAAI,CAAC,GAAG,EAAE,CAAA;QACV,KAAK,MAAM,CAAC,IAAI,KAAK;YAAE,CAAC,IAAI,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;QAClD,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;IAC3E,CAAC,CAAA;IAED,MAAM,aAAa,GAAG,CAAC,KAAK,GAAG,EAAE,EAAU,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IAEtG,uFAAuF;IACvF,MAAM,aAAa,GAAG,KAAK,EAAE,QAAgB,EAAmB,EAAE;QAChE,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAA;QACxF,OAAO,SAAS,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAA;IAC1C,CAAC,CAAA;IAED;;;;;OAKG;IACH,MAAM,SAAS,GAAG,CAAC,IAAmB,EAAE,MAAc,EAAe,EAAE,CAAC,CAAC;QACvE,WAAW,EAAE,IAAI,CAAC,YAAsB;QACxC,SAAS,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,IAAI,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI;QACpE,MAAM;QACN,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACpE,CAAC,CAAA;IAEF,MAAM,WAAW,GAAG,GAAuB,EAAE;QAC3C,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,SAAS,CAAC,CAAA;QAChD,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAA;QACrB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAgB,CAAA;YAC7C,OAAO,MAAM,CAAC,WAAW,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAA;QAC/D,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC,CAAA;IAED,MAAM,UAAU,GAAG,CAAC,EAAE,GAAG,GAAG,EAAE,EAAsB,EAAE;QACpD,MAAM,CAAC,GAAG,WAAW,EAAE,CAAA;QACvB,OAAO,CAAC,IAAI,CAAC,CAAC,SAAS,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IACzC,CAAC,CAAA;IAED;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,MAAM,YAAY,GAAG,GAAS,EAAE;QAC9B,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAA;QAC1B,IAAI,CAAC,CAAC;YAAE,OAAM;QACd,KAAK,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC;YAAE,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;IAC1D,CAAC,CAAA;IAED;;;;;;;OAOG;IACH,MAAM,kBAAkB,GAAG,GAAS,EAAE;QACpC,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAA;QAC1B,IAAI,CAAC,CAAC;YAAE,OAAM;QACd,KAAK,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,SAAS,EAAE,UAAU,CAAC;YAAE,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;IACxE,CAAC,CAAA;IAED,MAAM,gBAAgB,GAAG,CAAC,MAAmB,EAAQ,EAAE;QACrD,MAAM,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,UAAU,EAAE,MAAM,CAAC,CAAA;IAC/C,CAAC,CAAA;IAED,0EAA0E;IAC1E,MAAM,eAAe,GAAG,GAAuB,EAAE;QAC/C,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAA;QAC1B,MAAM,GAAG,GAAG,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAA;QAClC,IAAI,GAAG;YAAE,CAAC,EAAE,UAAU,CAAC,UAAU,CAAC,CAAA;QAClC,OAAQ,GAA0B,IAAI,IAAI,CAAA;IAC5C,CAAC,CAAA;IAED;;;;;;OAMG;IACH,MAAM,YAAY,GAAG,GAAkB,EAAE;QACvC,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAA;QAC1B,MAAM,GAAG,GAAG,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAA;QAClC,IAAI,GAAG;YAAE,CAAC,EAAE,UAAU,CAAC,UAAU,CAAC,CAAA;QAClC,OAAO,GAAG,IAAI,IAAI,CAAA;IACpB,CAAC,CAAA;IAED,OAAO;QACL,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,aAAa;QACb,WAAW;QACX,UAAU;QACV,YAAY;QACZ,kBAAkB;QAClB,gBAAgB;QAChB,eAAe;QACf,YAAY;QAEZ,UAAU,EAAE,GAAG,EAAE,CAAC,UAAU,EAAE,KAAK,IAAI;QAEvC;;;;;;;;WAQG;QACH,kBAAkB,EAAE,GAAG,EAAE,CAAC,UAAU,EAAE,EAAE,WAAW;QAEnD;;;;;;;WAOG;QACH,gBAAgB,EAAE,GAAG,EAAE;YACrB,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAA;YAC1B,OAAO,OAAO,CAAC,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAA;QAClE,CAAC;QAED;;;;;;;WAOG;QACH,KAAK,CAAC,WAAW,CAAC,QAAgB,EAAE,EAAE,MAAM,GAAG,KAAK,KAA2B,EAAE;YAC/E,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAA;YAC1B,IAAI,CAAC,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAA;YAErD,MAAM,QAAQ,GAAG,aAAa,EAAE,CAAA;YAChC,MAAM,KAAK,GAAG,aAAa,CAAC,EAAE,CAAC,CAAA;YAC/B,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAA;YACjC,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;YAC3B,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAA;YAE/B;;;;;;;;;;;;;;;;;;;cAmBE;YACF,IAAI,MAAM;gBAAE,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAA;YAE/B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,CAAA;YAC9C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAA;YAClD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,cAAc,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAA;YAC1D,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,EAAE,MAAM,CAAC,CAAA;YAC7C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,gBAAgB,EAAE,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAA;YACrE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,uBAAuB,EAAE,MAAM,CAAC,CAAA;YACrD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;YAEpC;;;;cAIE;YACF,IAAI,MAAM;gBAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;YAElD,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAA;QACpB,CAAC;QAED;;;;;;WAMG;QACH,KAAK,CAAC,cAAc,CAAC,MAAc;YACjC,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAA;YAC1B,IAAI,CAAC,CAAC;gBAAE,MAAM,IAAI,WAAW,CAAC,4BAA4B,CAAC,CAAA;YAE3D,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,MAAM,CAAC,CAAA;YAC1C,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YACjC,IAAI,KAAK;gBAAE,MAAM,IAAI,WAAW,CAAC,kCAAkC,KAAK,IAAI,CAAC,CAAA;YAE7E,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YAC/B,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YACjC,MAAM,aAAa,GAAG,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;YAC1C,MAAM,QAAQ,GAAG,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAA;YACxC,MAAM,QAAQ,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,GAAG,CAAA;YAE7C,2EAA2E;YAC3E,kEAAkE;YAClE,CAAC,CAAC,UAAU,CAAC,SAAS,CAAC,CAAA;YACvB,CAAC,CAAC,UAAU,CAAC,YAAY,CAAC,CAAA;YAC1B,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC,CAAA;YAExB,IAAI,CAAC,IAAI;gBAAE,MAAM,IAAI,WAAW,CAAC,4CAA4C,CAAC,CAAA;YAC9E,IAAI,CAAC,QAAQ,IAAI,CAAC,aAAa,EAAE,CAAC;gBAChC,MAAM,IAAI,WAAW,CAAC,+DAA+D,CAAC,CAAA;YACxF,CAAC;YACD,IAAI,KAAK,KAAK,aAAa,EAAE,CAAC;gBAC5B,mEAAmE;gBACnE,uEAAuE;gBACvE,MAAM,IAAI,WAAW,CAAC,oDAAoD,CAAC,CAAA;YAC7E,CAAC;YAED,MAAM,QAAQ,GAAG,MAAM,IAAI,EAAE,CAAC,IAAI,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE;gBACvE,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,mCAAmC;gBACnC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,SAAS,EAAE,oBAAoB;oBAC/B,IAAI;oBACJ,QAAQ,EAAE,MAAM,CAAC,QAAQ;oBACzB,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE;oBACjC,YAAY,EAAE,QAAQ;iBACvB,CAAC;aACH,CAAC,CAAA;YAEF,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,WAAW,CACnB,QAAQ,CAAC,MAAM,KAAK,GAAG;oBACrB,CAAC,CAAC,oFAAoF;oBACtF,CAAC,CAAC,sBAAsB,QAAQ,CAAC,MAAM,uBAAuB,CACjE,CAAA;YACH,CAAC;YAED,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAkB,CAAA;YACrD,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,MAAM;gBAAE,MAAM,IAAI,WAAW,CAAC,8BAA8B,CAAC,CAAA;YAE7F,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;YAC1C,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAA;YAC3C,0EAA0E;YAC1E,UAAU,CAAC,CAAC,CAAC,CAAA;YACb,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAA;QAC5B,CAAC;QAED;;;;;;;;;;;;WAYG;QACH,KAAK,CAAC,kBAAkB;YACtB,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAA;YAC1B,MAAM,OAAO,GAAG,WAAW,EAAE,CAAA;YAC7B,IAAI,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY;gBAAE,OAAO,IAAI,CAAA;YAE7C,IAAI,QAAwC,CAAA;YAC5C,IAAI,CAAC;gBACH,QAAQ,GAAG,MAAM,IAAI,EAAE,CAAC,IAAI,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE;oBACjE,MAAM,EAAE,MAAM;oBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;oBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;wBACnB,SAAS,EAAE,eAAe;wBAC1B,YAAY,EAAE,OAAO,CAAC,YAAY;wBAClC,QAAQ,EAAE,MAAM,CAAC,QAAQ;qBAC1B,CAAC;iBACH,CAAC,CAAA;YACJ,CAAC;YAAC,MAAM,CAAC;gBACP,wEAAwE;gBACxE,yEAAyE;gBACzE,yBAAyB;gBACzB,OAAO,IAAI,CAAA;YACb,CAAC;YAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,YAAY,EAAE,CAAA;gBACd,OAAO,IAAI,CAAA;YACb,CAAC;YAED,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAkB,CAAA;YACrD,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;gBACvB,YAAY,EAAE,CAAA;gBACd,OAAO,IAAI,CAAA;YACb,CAAC;YAED,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;YAC3D,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAA;YAC1C,OAAO,IAAI,CAAA;QACb,CAAC;QAED;;;;;;;;;;;WAWG;QACH,YAAY;YACV,YAAY,EAAE,CAAA;YACd,wEAAwE;YACxE,wCAAwC;YACxC,kBAAkB,EAAE,CAAA;YACpB,gBAAgB,CAAC,YAAY,CAAC,CAAA;YAC9B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,CAAA;YAC3C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAA;YAClD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,0BAA0B,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAA;YACtE,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAA;QACpB,CAAC;KACF,CAAA;AACH,CAAC"}
|
|
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.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
|
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.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,
|
|
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.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
|
@@ -38,11 +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`**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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.
|
|
46
49
|
* - **`keyPrefix`** — so two apps on one origin cannot read each other's keys.
|
|
47
50
|
* - **`navigate`** — the one genuinely untestable act. Injected so a test can
|
|
48
51
|
* observe where the flow *would* go.
|
|
@@ -70,6 +73,19 @@ declare const URL: {
|
|
|
70
73
|
}
|
|
71
74
|
declare const URLSearchParams: { new (init: string): { get(name: string): string | null } }
|
|
72
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
|
+
|
|
73
89
|
/** Just enough of `fetch` to redeem a code. A parameter as well as a global. */
|
|
74
90
|
export type FetchLike = (
|
|
75
91
|
url: string,
|
|
@@ -88,14 +104,45 @@ export interface EstivaIdConfig {
|
|
|
88
104
|
* **exactly** — a trailing slash is a refused sign-in.
|
|
89
105
|
*/
|
|
90
106
|
redirectUri: () => string
|
|
91
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Where the **session** lives — the token and nothing else.
|
|
109
|
+
*
|
|
110
|
+
* Peek uses `sessionStorage`: it dies with the tab, which keeps a bearer token
|
|
111
|
+
* off a shared machine's disk. Ship uses `localStorage` so a session survives
|
|
112
|
+
* a tab close. Returns null outside a browser.
|
|
113
|
+
*/
|
|
92
114
|
storage: () => KeyValueStore | null
|
|
115
|
+
/**
|
|
116
|
+
* Where an **in-flight sign-in's** single-use credentials live — the verifier,
|
|
117
|
+
* the state, the returnTo, the shell reason and the silent-attempt guard.
|
|
118
|
+
*
|
|
119
|
+
* Defaults to {@link storage}, which is what Peek wants: one store for both.
|
|
120
|
+
*
|
|
121
|
+
* **Ship needs them separated, and that is why this exists.** Its session
|
|
122
|
+
* belongs in `localStorage` so it outlives a tab, while the PKCE credentials
|
|
123
|
+
* are per-tab by nature — the flow starts and ends in one tab, and the guard's
|
|
124
|
+
* whole point is that a genuinely new tab is entitled to a fresh attempt.
|
|
125
|
+
*
|
|
126
|
+
* This parameter was added by wiring the second consumer. The package had
|
|
127
|
+
* assumed one store because it was extracted from Peek, which uses one: the
|
|
128
|
+
* exact "comes out shaped like the app it came from" failure SHA-4 names, found
|
|
129
|
+
* by the mechanism SHA-4 prescribes for finding it.
|
|
130
|
+
*/
|
|
131
|
+
pendingStore?: () => KeyValueStore | null
|
|
93
132
|
/** Namespaces this app's keys, e.g. `peek.estivaId`. */
|
|
94
133
|
keyPrefix: string
|
|
95
134
|
/** Defaults to the runtime's `location.assign`-equivalent via `navigate`. */
|
|
96
135
|
navigate?: (url: string) => void
|
|
97
136
|
now?: () => number
|
|
98
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
|
|
99
146
|
}
|
|
100
147
|
|
|
101
148
|
export interface StoredToken {
|
|
@@ -117,8 +164,35 @@ export interface StoredToken {
|
|
|
117
164
|
refreshToken?: string
|
|
118
165
|
}
|
|
119
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
|
+
|
|
120
173
|
export class SignInError extends Error {}
|
|
121
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
|
+
|
|
122
196
|
interface TokenResponse {
|
|
123
197
|
access_token?: string
|
|
124
198
|
expires_in?: number
|
|
@@ -132,6 +206,7 @@ export interface EstivaIdClient {
|
|
|
132
206
|
beginSignIn: (returnTo: string, options?: { silent?: boolean }) => Promise<void>
|
|
133
207
|
completeSignIn: (search: string) => Promise<{ token: StoredToken; returnTo: string }>
|
|
134
208
|
refreshAccessToken: () => Promise<StoredToken | null>
|
|
209
|
+
scheduleRenewal: (handlers?: RenewalHandlers) => RenewalSchedule
|
|
135
210
|
storedToken: () => StoredToken | null
|
|
136
211
|
validToken: (now?: number) => StoredToken | null
|
|
137
212
|
hasSession: () => boolean
|
|
@@ -147,6 +222,8 @@ export interface EstivaIdClient {
|
|
|
147
222
|
|
|
148
223
|
export function createEstivaId(config: EstivaIdConfig): EstivaIdClient {
|
|
149
224
|
const now = config.now ?? (() => Date.now())
|
|
225
|
+
/** In-flight credentials, which may live somewhere shorter than the session. */
|
|
226
|
+
const pending = config.pendingStore ?? config.storage
|
|
150
227
|
const send = () => config.fetch ?? fetch
|
|
151
228
|
const go = (url: string) => {
|
|
152
229
|
if (!config.navigate) throw new Error('createEstivaId: navigate is required to leave for Estiva ID')
|
|
@@ -225,9 +302,11 @@ export function createEstivaId(config: EstivaIdConfig): EstivaIdClient {
|
|
|
225
302
|
* Anything that wants both says so, and only `beginSignOut` does.
|
|
226
303
|
*/
|
|
227
304
|
const clearSession = (): void => {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
305
|
+
config.storage()?.removeItem(TOKEN_KEY)
|
|
306
|
+
// The reason lives with the in-flight credentials, which may be a different
|
|
307
|
+
// store — see `pendingStore`. Still cleared here: a reason outliving the
|
|
308
|
+
// session it described is a lie, and that was true before the split.
|
|
309
|
+
pending()?.removeItem(REASON_KEY)
|
|
231
310
|
}
|
|
232
311
|
|
|
233
312
|
/**
|
|
@@ -239,18 +318,18 @@ export function createEstivaId(config: EstivaIdConfig): EstivaIdClient {
|
|
|
239
318
|
* different challenge. Destroying a live one is the expensive direction.
|
|
240
319
|
*/
|
|
241
320
|
const clearPendingSignIn = (): void => {
|
|
242
|
-
const s =
|
|
321
|
+
const s = pending()
|
|
243
322
|
if (!s) return
|
|
244
323
|
for (const k of [VERIFIER_KEY, STATE_KEY, RETURN_KEY]) s.removeItem(k)
|
|
245
324
|
}
|
|
246
325
|
|
|
247
326
|
const stashShellReason = (reason: ShellReason): void => {
|
|
248
|
-
|
|
327
|
+
pending()?.setItem(REASON_KEY, reason)
|
|
249
328
|
}
|
|
250
329
|
|
|
251
330
|
/** Read once. Single-use: a stale reason outliving its cause is a lie. */
|
|
252
331
|
const takeShellReason = (): ShellReason | null => {
|
|
253
|
-
const s =
|
|
332
|
+
const s = pending()
|
|
254
333
|
const raw = s?.getItem(REASON_KEY)
|
|
255
334
|
if (raw) s?.removeItem(REASON_KEY)
|
|
256
335
|
return (raw as ShellReason | null) ?? null
|
|
@@ -264,12 +343,94 @@ export function createEstivaId(config: EstivaIdConfig): EstivaIdClient {
|
|
|
264
343
|
* puts people on a default screen instead of the link they opened (PEEK-123).
|
|
265
344
|
*/
|
|
266
345
|
const takeReturnTo = (): string | null => {
|
|
267
|
-
const s =
|
|
346
|
+
const s = pending()
|
|
268
347
|
const raw = s?.getItem(RETURN_KEY)
|
|
269
348
|
if (raw) s?.removeItem(RETURN_KEY)
|
|
270
349
|
return raw ?? null
|
|
271
350
|
}
|
|
272
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
|
+
|
|
273
434
|
return {
|
|
274
435
|
redirectUri: config.redirectUri,
|
|
275
436
|
codeChallenge,
|
|
@@ -303,7 +464,7 @@ export function createEstivaId(config: EstivaIdConfig): EstivaIdClient {
|
|
|
303
464
|
* code that just arrived belongs to anything this tab did.
|
|
304
465
|
*/
|
|
305
466
|
hasPendingSignIn: () => {
|
|
306
|
-
const s =
|
|
467
|
+
const s = pending()
|
|
307
468
|
return Boolean(s?.getItem(VERIFIER_KEY) && s.getItem(STATE_KEY))
|
|
308
469
|
},
|
|
309
470
|
|
|
@@ -316,7 +477,7 @@ export function createEstivaId(config: EstivaIdConfig): EstivaIdClient {
|
|
|
316
477
|
* compared.
|
|
317
478
|
*/
|
|
318
479
|
async beginSignIn(returnTo: string, { silent = false }: { silent?: boolean } = {}): Promise<void> {
|
|
319
|
-
const s =
|
|
480
|
+
const s = pending()
|
|
320
481
|
if (!s) throw new Error('sign-in requires a browser')
|
|
321
482
|
|
|
322
483
|
const verifier = randomUrlSafe()
|
|
@@ -373,8 +534,9 @@ export function createEstivaId(config: EstivaIdConfig): EstivaIdClient {
|
|
|
373
534
|
* on this side of the exchange.
|
|
374
535
|
*/
|
|
375
536
|
async completeSignIn(search: string): Promise<{ token: StoredToken; returnTo: string }> {
|
|
376
|
-
const s =
|
|
377
|
-
|
|
537
|
+
const s = pending()
|
|
538
|
+
const sessionStore = config.storage()
|
|
539
|
+
if (!s || !sessionStore) throw new SignInError('sign-in requires a browser')
|
|
378
540
|
|
|
379
541
|
const params = new URLSearchParams(search)
|
|
380
542
|
const error = params.get('error')
|
|
@@ -427,64 +589,101 @@ export function createEstivaId(config: EstivaIdConfig): EstivaIdClient {
|
|
|
427
589
|
if (!body.access_token || !body.pubkey) throw new SignInError('Estiva ID returned no token.')
|
|
428
590
|
|
|
429
591
|
const token = tokenFrom(body, body.pubkey)
|
|
430
|
-
|
|
592
|
+
sessionStore.setItem(TOKEN_KEY, JSON.stringify(token))
|
|
431
593
|
// The attempt worked, so the next boot is entitled to a fresh silent one.
|
|
432
594
|
clearGuard(s)
|
|
433
595
|
return { token, returnTo }
|
|
434
596
|
},
|
|
435
597
|
|
|
598
|
+
refreshAccessToken,
|
|
599
|
+
|
|
436
600
|
/**
|
|
437
|
-
* Renew the
|
|
601
|
+
* Renew shortly **before** the token expires, rather than reacting to it
|
|
602
|
+
* having expired.
|
|
438
603
|
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
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.
|
|
442
610
|
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
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".
|
|
615
|
+
*
|
|
616
|
+
* ## The timer is a `setTimeout`, and browsers throttle those
|
|
617
|
+
*
|
|
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.
|
|
448
633
|
*/
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
let
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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)
|
|
470
671
|
}
|
|
471
672
|
|
|
472
|
-
|
|
473
|
-
clearSession()
|
|
474
|
-
return null
|
|
475
|
-
}
|
|
673
|
+
arm()
|
|
476
674
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
675
|
+
return {
|
|
676
|
+
cancel(): void {
|
|
677
|
+
cancelled = true
|
|
678
|
+
if (handle !== null) {
|
|
679
|
+
clearTimer(handle)
|
|
680
|
+
handle = null
|
|
681
|
+
}
|
|
682
|
+
},
|
|
481
683
|
}
|
|
482
|
-
|
|
483
|
-
const next = tokenFrom(body, body.pubkey ?? current.pubkey)
|
|
484
|
-
s.setItem(TOKEN_KEY, JSON.stringify(next))
|
|
485
|
-
return next
|
|
486
684
|
},
|
|
487
685
|
|
|
686
|
+
|
|
488
687
|
/**
|
|
489
688
|
* Leave for Estiva ID to end the session *there* (PEEK-122).
|
|
490
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.
|
|
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'
|