@estiva-app/identity 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # @estiva-app/identity
2
2
 
3
+ ## 0.2.2 — 2026-09-08
4
+
5
+ **Additive.** `token` gains a function form; a string behaves exactly as before,
6
+ so `^0.2.0` consumers need no change.
7
+
8
+ - **`estivaIdSigner`'s `token` may now be `() => string | undefined`**, read per
9
+ signature instead of captured once.
10
+
11
+ Found while adopting `scheduleRenewal` in Ship, which builds its signer at
12
+ module scope (`web/src/api/store.ts`) and passed the token as a string. A
13
+ schedule replaces the *stored* token before it expires; a signer holding a
14
+ string keeps presenting the old one, pays a `401` on the next signature, and
15
+ its retry spends a **second** grant to reach a token the app already had. Two
16
+ rotations per expiry — and the renewal the schedule exists to make invisible is
17
+ not invisible at all. Adopting the schedule without this would have been
18
+ decorative on the write path.
19
+
20
+ A string stays supported deliberately: a script that obtains one token and
21
+ exits has nothing to read from, and making those write `() => t` would be
22
+ noise. With a reader, a renewal from anywhere is picked up without the signer
23
+ being told — including one written by another tab into shared storage.
24
+
25
+ A reader returning `undefined` is treated as {@link SignerTokenExpired} and
26
+ `/sign` is never called: signed out mid-session is not a signing failure, and
27
+ asking the service to judge an absent token is a round trip whose answer is
28
+ already known.
29
+
30
+ - **Corrected a claim this package inherited and spread.** The `renew` doc said
31
+ access tokens "live ten minutes rather than an hour". Measured against
32
+ production on 2026-09-08, a token has ~3600s: `expires_in_sec` read 3544 on a
33
+ live session and 3554 immediately after a renewal. The sentence came from
34
+ Ship's own comment and I carried it here when `POST /sign` moved in, so it was
35
+ about to be the shared layer's version of a wrong fact. No behaviour depended
36
+ on it — `expires_in` is read from the response and never assumed.
37
+
38
+ 3 new tests, 80 to 83.
39
+
3
40
  ## 0.2.1 — 2026-09-08
4
41
 
5
42
  **Additive.** `SignRefused` is new; nothing that catches `Error` or reads a
package/README.md CHANGED
@@ -75,8 +75,14 @@ export const { validToken, beginSignIn, completeSignIn, beginSignOut } = client
75
75
  // Peek: one round trip, a token read fresh each time
76
76
  const event = await signViaEstivaId(unsigned, { base, token, expectedPubkey: myPubkey })
77
77
 
78
- // Ship: a protocol Signer, built once, renewing itself
79
- const signer = estivaIdSigner({ base, pubkey, token, renew: client.refreshAccessToken })
78
+ // Ship: a protocol Signer, built once. Pass a READER, not a string, if anything
79
+ // else can replace the token — `scheduleRenewal` does.
80
+ const signer = estivaIdSigner({
81
+ base,
82
+ pubkey,
83
+ token: () => client.validToken()?.accessToken,
84
+ renew: client.refreshAccessToken,
85
+ })
80
86
  ```
81
87
 
82
88
  An app publishes as a person without ever seeing their secret. `@estiva-app/protocol`
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.2.1";
49
+ export declare const IDENTITY_VERSION = "0.2.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
51
  export { type EstivaIdConfig, type EstivaIdClient, type StoredToken, type FetchLike, type RenewalHandlers, type RenewalSchedule, SignInError, createEstivaId, } from './client.js';
52
52
  export { type SignViaEstivaIdOptions, type EstivaIdSignerOptions, type SignFetchLike, SignerTokenExpired, SignRefused, signViaEstivaId, estivaIdSigner, } from './signer.js';
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.2.1';
49
+ export const IDENTITY_VERSION = '0.2.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
  export { SignerTokenExpired, SignRefused, signViaEstivaId, estivaIdSigner, } from './signer.js';
package/dist/signer.d.ts CHANGED
@@ -121,15 +121,27 @@ export interface EstivaIdSignerOptions {
121
121
  base: string;
122
122
  /** The pubkey the token was issued for — `sub` in the JWT. */
123
123
  pubkey: string;
124
- /** A bearer token from `POST /token`, audience-bound to this app. */
125
- token: string;
124
+ /**
125
+ * A bearer token from `POST /token`, audience-bound to this app — or a
126
+ * function returning the current one.
127
+ *
128
+ * **Pass the function if anything else can replace the token**, which
129
+ * {@link EstivaIdClient.scheduleRenewal} does by design. A string is captured
130
+ * once and held, so a scheduled renewal would refresh the *stored* token while
131
+ * this signer kept presenting the old one: the next signature still pays a
132
+ * `401`, and the retry spends a **second** grant for a token the app already
133
+ * holds. Two rotations per expiry, and the invisible renewal the schedule
134
+ * exists to provide never happens.
135
+ *
136
+ * A string stays supported because a script that obtains one token and exits
137
+ * has nothing to read from, and making those pass `() => t` would be noise.
138
+ */
139
+ token: string | (() => string | undefined);
126
140
  /**
127
141
  * Renew the token, once, when `/sign` says it has expired (PEEK-108).
128
142
  *
129
143
  * Optional: without it a `401` surfaces as {@link SignerTokenExpired} for the
130
- * caller to deal with, exactly as before. With it, an expiry is invisible —
131
- * which matters more than it used to, because access tokens live ten minutes
132
- * rather than an hour, so a session of any length crosses one.
144
+ * caller to deal with, exactly as before. With it, an expiry is invisible.
133
145
  *
134
146
  * Returning `null` means renewal failed and the expiry is real.
135
147
  *
@@ -1 +1 @@
1
- {"version":3,"file":"signer.d.ts","sourceRoot":"","sources":["../src/signer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAE9E;;;;;;;;GAQG;AACH,OAAO,CAAC,MAAM,KAAK,EAAE,CACnB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,KACpE,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,CAAA;AAEtE,kFAAkF;AAClF,MAAM,MAAM,aAAa,GAAG,OAAO,KAAK,CAAA;AAExC,mEAAmE;AACnE,qBAAa,kBAAmB,SAAQ,KAAK;;CAK5C;AAED;;;;;;;;;;GAUG;AACH,qBAAa,WAAY,SAAQ,KAAK;IAElC,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM,EACvB,OAAO,EAAE,MAAM;CAKlB;AAED,MAAM,WAAW,sBAAsB;IACrC,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAA;IACZ,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAA;IACb;;;;;;;;;;;;OAYG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,aAAa,EACvB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,WAAW,CAAC,CAkDtB;AAED,MAAM,WAAW,qBAAqB;IACpC,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAA;IACZ,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAA;IACd,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAA;IACb;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACpC,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,qBAAqB,GAAG,MAAM,CAwCzG"}
1
+ {"version":3,"file":"signer.d.ts","sourceRoot":"","sources":["../src/signer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAE9E;;;;;;;;GAQG;AACH,OAAO,CAAC,MAAM,KAAK,EAAE,CACnB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,KACpE,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,CAAA;AAEtE,kFAAkF;AAClF,MAAM,MAAM,aAAa,GAAG,OAAO,KAAK,CAAA;AAExC,mEAAmE;AACnE,qBAAa,kBAAmB,SAAQ,KAAK;;CAK5C;AAED;;;;;;;;;;GAUG;AACH,qBAAa,WAAY,SAAQ,KAAK;IAElC,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM,EACvB,OAAO,EAAE,MAAM;CAKlB;AAED,MAAM,WAAW,sBAAsB;IACrC,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAA;IACZ,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAA;IACb;;;;;;;;;;;;OAYG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,aAAa,EACvB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,WAAW,CAAC,CAkDtB;AAED,MAAM,WAAW,qBAAqB;IACpC,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAA;IACZ,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAA;IACd;;;;;;;;;;;;;;OAcG;IACH,KAAK,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,SAAS,CAAC,CAAA;IAC1C;;;;;;;;;;OAUG;IACH,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACpC,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,qBAAqB,GAAG,MAAM,CAqDzG"}
package/dist/signer.js CHANGED
@@ -90,7 +90,15 @@ export async function signViaEstivaId(unsigned, options) {
90
90
  * rather than being rediscovered on each one.
91
91
  */
92
92
  export function estivaIdSigner({ base, pubkey, token, renew, fetch: send }) {
93
- let bearer = token;
93
+ /*
94
+ A held bearer only when there is nothing to read from. When `token` is a
95
+ function the current value is asked for per signature, so a renewal from
96
+ anywhere — a schedule, another tab's write into shared storage, the retry
97
+ below — is picked up without this signer being told.
98
+ */
99
+ const read = typeof token === 'function' ? token : undefined;
100
+ let held = typeof token === 'string' ? token : undefined;
101
+ const bearerNow = () => (read ? read() : held);
94
102
  return {
95
103
  pubkey,
96
104
  kind: 'estiva-id',
@@ -106,6 +114,9 @@ export function estivaIdSigner({ base, pubkey, token, renew, fetch: send }) {
106
114
  expectedPubkey: pubkey,
107
115
  ...(send ? { fetch: send } : {}),
108
116
  });
117
+ const bearer = bearerNow();
118
+ if (bearer === undefined)
119
+ throw new SignerTokenExpired();
109
120
  try {
110
121
  return await once(bearer);
111
122
  }
@@ -124,8 +135,11 @@ export function estivaIdSigner({ base, pubkey, token, renew, fetch: send }) {
124
135
  const renewed = await renew();
125
136
  if (!renewed)
126
137
  throw error;
127
- bearer = renewed;
128
- return await once(bearer);
138
+ // Only a held bearer needs replacing. A reader already sees the new one,
139
+ // because `renew` wrote it where the reader reads from.
140
+ if (read === undefined)
141
+ held = renewed;
142
+ return await once(renewed);
129
143
  }
130
144
  },
131
145
  };
@@ -1 +1 @@
1
- {"version":3,"file":"signer.js","sourceRoot":"","sources":["../src/signer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"signer.js","sourceRoot":"","sources":["../src/signer.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estiva-app/identity",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Estiva ID sign-in: authorization code + PKCE, token storage, silent renewal, sign-out, signing via POST /sign, and the auth-shell state machine. Knows nothing about any app's data layer.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/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.2.1'
49
+ export const IDENTITY_VERSION = '0.2.2'
50
50
 
51
51
  export {
52
52
  type KeyValueStore,
package/src/signer.ts CHANGED
@@ -184,15 +184,27 @@ export interface EstivaIdSignerOptions {
184
184
  base: string
185
185
  /** The pubkey the token was issued for — `sub` in the JWT. */
186
186
  pubkey: string
187
- /** A bearer token from `POST /token`, audience-bound to this app. */
188
- token: string
187
+ /**
188
+ * A bearer token from `POST /token`, audience-bound to this app — or a
189
+ * function returning the current one.
190
+ *
191
+ * **Pass the function if anything else can replace the token**, which
192
+ * {@link EstivaIdClient.scheduleRenewal} does by design. A string is captured
193
+ * once and held, so a scheduled renewal would refresh the *stored* token while
194
+ * this signer kept presenting the old one: the next signature still pays a
195
+ * `401`, and the retry spends a **second** grant for a token the app already
196
+ * holds. Two rotations per expiry, and the invisible renewal the schedule
197
+ * exists to provide never happens.
198
+ *
199
+ * A string stays supported because a script that obtains one token and exits
200
+ * has nothing to read from, and making those pass `() => t` would be noise.
201
+ */
202
+ token: string | (() => string | undefined)
189
203
  /**
190
204
  * Renew the token, once, when `/sign` says it has expired (PEEK-108).
191
205
  *
192
206
  * Optional: without it a `401` surfaces as {@link SignerTokenExpired} for the
193
- * caller to deal with, exactly as before. With it, an expiry is invisible —
194
- * which matters more than it used to, because access tokens live ten minutes
195
- * rather than an hour, so a session of any length crosses one.
207
+ * caller to deal with, exactly as before. With it, an expiry is invisible.
196
208
  *
197
209
  * Returning `null` means renewal failed and the expiry is real.
198
210
  *
@@ -210,7 +222,15 @@ export interface EstivaIdSignerOptions {
210
222
  * rather than being rediscovered on each one.
211
223
  */
212
224
  export function estivaIdSigner({ base, pubkey, token, renew, fetch: send }: EstivaIdSignerOptions): Signer {
213
- let bearer = token
225
+ /*
226
+ A held bearer only when there is nothing to read from. When `token` is a
227
+ function the current value is asked for per signature, so a renewal from
228
+ anywhere — a schedule, another tab's write into shared storage, the retry
229
+ below — is picked up without this signer being told.
230
+ */
231
+ const read = typeof token === 'function' ? token : undefined
232
+ let held = typeof token === 'string' ? token : undefined
233
+ const bearerNow = (): string | undefined => (read ? read() : held)
214
234
 
215
235
  return {
216
236
  pubkey,
@@ -229,6 +249,9 @@ export function estivaIdSigner({ base, pubkey, token, renew, fetch: send }: Esti
229
249
  ...(send ? { fetch: send } : {}),
230
250
  })
231
251
 
252
+ const bearer = bearerNow()
253
+ if (bearer === undefined) throw new SignerTokenExpired()
254
+
232
255
  try {
233
256
  return await once(bearer)
234
257
  } catch (error) {
@@ -244,8 +267,10 @@ export function estivaIdSigner({ base, pubkey, token, renew, fetch: send }: Esti
244
267
  if (!(error instanceof SignerTokenExpired) || !renew) throw error
245
268
  const renewed = await renew()
246
269
  if (!renewed) throw error
247
- bearer = renewed
248
- return await once(bearer)
270
+ // Only a held bearer needs replacing. A reader already sees the new one,
271
+ // because `renew` wrote it where the reader reads from.
272
+ if (read === undefined) held = renewed
273
+ return await once(renewed)
249
274
  }
250
275
  },
251
276
  }