@estiva-app/identity 0.1.0

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/dist/client.js ADDED
@@ -0,0 +1,398 @@
1
+ /**
2
+ * Signing in with Estiva ID — authorization code + PKCE.
3
+ *
4
+ * The app does not hold a password or a key. It sends the person to the identity
5
+ * service and gets back a token whose `sub` is their Nostr pubkey.
6
+ *
7
+ * ## Why a redirect rather than a fetch
8
+ *
9
+ * `POST /token` is reachable cross-origin, but the *session* that authorises the
10
+ * redirect is a `SameSite=Lax` cookie on the identity origin. Lax cookies ride
11
+ * top-level navigations and nothing else, so the only way to prove who you are
12
+ * is to actually go there. That is deliberate on their side (PEEK-90): the origin
13
+ * holding every custodied key does not relax its cookie or send
14
+ * `Access-Control-Allow-Credentials`.
15
+ *
16
+ * **A hidden iframe does not work**, and the reason is the same property: `Lax`
17
+ * withholds the cookie from anything that is not a top-level navigation. The
18
+ * refresh-token grant exists because of this. Do not "simplify" it back.
19
+ *
20
+ * ## The bits that must match exactly
21
+ *
22
+ * `redirect_uri` is compared **exact-string** against the app's registered list —
23
+ * a trailing slash is a different URI and gets a 400 before anything else
24
+ * happens. An identity provider that prefix-matched would be an open redirector
25
+ * handing out authorization codes.
26
+ *
27
+ * The request body is **camelCase JSON** (`codeVerifier`, not `code_verifier`)
28
+ * while the response is snake_case (`access_token`). That is genuinely the
29
+ * contract, not a typo — see `docs/API.md` in estiva-id.
30
+ *
31
+ * ## Everything is injected, and each injection is a real difference
32
+ *
33
+ * This is a factory rather than a module of free functions because the two
34
+ * consumers genuinely differ, and hardcoding either would have made the package
35
+ * come out shaped like whichever app it was extracted from:
36
+ *
37
+ * - **`clientId`** — `estiva-peek` and `estiva-ship` are separate rows in
38
+ * `app_credentials` with separate `redirectUris` and separate kind ceilings.
39
+ * - **`redirectUri`** — Peek registers a fixed `/auth/callback`; Ship routes on
40
+ * `location.hash` and registers its current pathname. Neither is more correct.
41
+ * - **`storage`** — Peek keeps its token in `sessionStorage`, which dies with the
42
+ * tab and keeps a bearer token off a shared machine's disk. Ship uses
43
+ * `localStorage`. **Do not unify this**: NIP-RS slots (CRO-4) must survive a
44
+ * restart and belong in `localStorage`, while an access token does not.
45
+ * Different lifetimes, different homes.
46
+ * - **`keyPrefix`** — so two apps on one origin cannot read each other's keys.
47
+ * - **`navigate`** — the one genuinely untestable act. Injected so a test can
48
+ * observe where the flow *would* go.
49
+ */
50
+ import { clearGuard, markGuard } from './shell.js';
51
+ export class SignInError extends Error {
52
+ }
53
+ export function createEstivaId(config) {
54
+ const now = config.now ?? (() => Date.now());
55
+ const send = () => config.fetch ?? fetch;
56
+ const go = (url) => {
57
+ if (!config.navigate)
58
+ throw new Error('createEstivaId: navigate is required to leave for Estiva ID');
59
+ config.navigate(url);
60
+ };
61
+ const VERIFIER_KEY = `${config.keyPrefix}.codeVerifier`;
62
+ const STATE_KEY = `${config.keyPrefix}.state`;
63
+ const RETURN_KEY = `${config.keyPrefix}.returnTo`;
64
+ const TOKEN_KEY = `${config.keyPrefix}.token`;
65
+ /** Why the last round trip came back empty, handed from the callback to the shell. */
66
+ const REASON_KEY = `${config.keyPrefix}.shellReason`;
67
+ const base64Url = (bytes) => {
68
+ let s = '';
69
+ for (const b of bytes)
70
+ s += String.fromCharCode(b);
71
+ return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
72
+ };
73
+ const randomUrlSafe = (bytes = 32) => base64Url(crypto.getRandomValues(new Uint8Array(bytes)));
74
+ /** `base64url(sha256(verifier))` — the S256 challenge. `plain` is refused upstream. */
75
+ const codeChallenge = async (verifier) => {
76
+ const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
77
+ return base64Url(new Uint8Array(digest));
78
+ };
79
+ /**
80
+ * Shape a token response into what is kept.
81
+ *
82
+ * The expiry is deliberately pessimistic by 30s, so a token is never presented
83
+ * in the window where it is technically alive and about to not be.
84
+ */
85
+ const tokenFrom = (body, pubkey) => ({
86
+ accessToken: body.access_token,
87
+ expiresAt: now() + Math.max(0, (body.expires_in ?? 600) - 30) * 1000,
88
+ pubkey,
89
+ ...(body.refresh_token ? { refreshToken: body.refresh_token } : {}),
90
+ });
91
+ const storedToken = () => {
92
+ const raw = config.storage()?.getItem(TOKEN_KEY);
93
+ if (!raw)
94
+ return null;
95
+ try {
96
+ const parsed = JSON.parse(raw);
97
+ return parsed.accessToken && parsed.expiresAt ? parsed : null;
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ };
103
+ const validToken = (at = now()) => {
104
+ const t = storedToken();
105
+ return t && t.expiresAt > at ? t : null;
106
+ };
107
+ /**
108
+ * Forget this app's session. **Not** an in-flight sign-in — and that
109
+ * separation is the whole point (PEEK-167).
110
+ *
111
+ * This used to clear the verifier, state and `returnTo` as well, which reads
112
+ * as thoroughness and is a bug. Those three are not session state: they are
113
+ * the credentials of an authorization request that is *currently in the air*,
114
+ * owned by the `beginSignIn` that wrote them and spent by the
115
+ * `completeSignIn` that comes back. Clearing them on somebody else's schedule
116
+ * does not tidy anything up — it destroys a sign-in that is still happening.
117
+ *
118
+ * The path that made it matter: a boot whose token has expired renews, fails,
119
+ * clears the session, drops to the shell, and the shell probes and writes a
120
+ * fresh verifier. A *second* renewal awaiting the same in-flight promise then
121
+ * resolves and clears again — now deleting the verifier the probe just wrote.
122
+ * The code comes back to a tab with nothing to redeem it, `completeSignIn`
123
+ * refuses it before `/token` is ever called, and the identity service has no
124
+ * record of a failure at all because nothing was asked of it.
125
+ *
126
+ * Anything that wants both says so, and only `beginSignOut` does.
127
+ */
128
+ const clearSession = () => {
129
+ const s = config.storage();
130
+ if (!s)
131
+ return;
132
+ for (const k of [TOKEN_KEY, REASON_KEY])
133
+ s.removeItem(k);
134
+ };
135
+ /**
136
+ * Abandon a sign-in that has not come back.
137
+ *
138
+ * Deliberately not called on any failure path. A verifier outliving its
139
+ * attempt is harmless — `completeSignIn` clears all three before it does
140
+ * anything else, and a stale one cannot redeem a code issued against a
141
+ * different challenge. Destroying a live one is the expensive direction.
142
+ */
143
+ const clearPendingSignIn = () => {
144
+ const s = config.storage();
145
+ if (!s)
146
+ return;
147
+ for (const k of [VERIFIER_KEY, STATE_KEY, RETURN_KEY])
148
+ s.removeItem(k);
149
+ };
150
+ const stashShellReason = (reason) => {
151
+ config.storage()?.setItem(REASON_KEY, reason);
152
+ };
153
+ /** Read once. Single-use: a stale reason outliving its cause is a lie. */
154
+ const takeShellReason = () => {
155
+ const s = config.storage();
156
+ const raw = s?.getItem(REASON_KEY);
157
+ if (raw)
158
+ s?.removeItem(REASON_KEY);
159
+ return raw ?? null;
160
+ };
161
+ /**
162
+ * Where to go back to, as stashed by `beginSignIn`. Single-use.
163
+ *
164
+ * The refusal path needs this as much as the success path: a silent probe that
165
+ * comes back `login_required` was still started from somewhere, and dropping it
166
+ * puts people on a default screen instead of the link they opened (PEEK-123).
167
+ */
168
+ const takeReturnTo = () => {
169
+ const s = config.storage();
170
+ const raw = s?.getItem(RETURN_KEY);
171
+ if (raw)
172
+ s?.removeItem(RETURN_KEY);
173
+ return raw ?? null;
174
+ };
175
+ return {
176
+ redirectUri: config.redirectUri,
177
+ codeChallenge,
178
+ storedToken,
179
+ validToken,
180
+ clearSession,
181
+ clearPendingSignIn,
182
+ stashShellReason,
183
+ takeShellReason,
184
+ takeReturnTo,
185
+ hasSession: () => validToken() !== null,
186
+ /**
187
+ * The access token to hand anything that talks to the relay.
188
+ *
189
+ * Every such call needs it, because Buzz answers a non-member with
190
+ * `403 relay_membership_required` and a backend is a member of nothing. One
191
+ * call site forgetting it is not a visible failure — the read comes back
192
+ * empty and the widget renders nothing — so it is worth one name for "the
193
+ * current token" rather than copies of `validToken()?.accessToken`.
194
+ */
195
+ currentAccessToken: () => validToken()?.accessToken,
196
+ /**
197
+ * Whether this tab has a sign-in of its own in the air.
198
+ *
199
+ * `beginSignIn` writes the verifier and state together and `completeSignIn`
200
+ * spends them together, so either key answers this. Both are read because
201
+ * the interesting question is not which key survived — it is whether the
202
+ * code that just arrived belongs to anything this tab did.
203
+ */
204
+ hasPendingSignIn: () => {
205
+ const s = config.storage();
206
+ return Boolean(s?.getItem(VERIFIER_KEY) && s.getItem(STATE_KEY));
207
+ },
208
+ /**
209
+ * Leave for the identity service. Never returns — the browser navigates away.
210
+ *
211
+ * `returnTo` is kept locally rather than smuggled through `state`: `state` is
212
+ * a CSRF token, and widening it into a general-purpose payload invites
213
+ * treating whatever comes back as trustworthy. What comes back is only ever
214
+ * compared.
215
+ */
216
+ async beginSignIn(returnTo, { silent = false } = {}) {
217
+ const s = config.storage();
218
+ if (!s)
219
+ throw new Error('sign-in requires a browser');
220
+ const verifier = randomUrlSafe();
221
+ const state = randomUrlSafe(16);
222
+ s.setItem(VERIFIER_KEY, verifier);
223
+ s.setItem(STATE_KEY, state);
224
+ s.setItem(RETURN_KEY, returnTo);
225
+ /*
226
+ The guard is marked HERE, synchronously with the writes above, and not
227
+ further down next to the navigation (PEEK-167).
228
+
229
+ It used to sit just before the navigation, which reads as the same thing
230
+ and is not: `codeChallenge` awaits a `crypto.subtle` digest in between.
231
+ For that whole window the verifier and state have already been overwritten
232
+ while `readGuard` still answers null, so a second boot decision landing in
233
+ it decides to probe as well.
234
+
235
+ Two probes then write two verifier/state pairs into one tab's storage, and
236
+ only one of the two navigations commits. When the one that commits is not
237
+ the one that wrote last, the code comes back matched to a state that is no
238
+ longer there and `completeSignIn` refuses it — an `exchange_failed` for a
239
+ code that was never even presented to `/token`.
240
+
241
+ Marking it with the other single-use writes is what makes the guard cover
242
+ the window it exists for: they are the things this attempt has already
243
+ spent, and they become true together or not at all.
244
+ */
245
+ if (silent)
246
+ markGuard(s, now());
247
+ const url = new URL('/authorize', config.base);
248
+ url.searchParams.set('client_id', config.clientId);
249
+ url.searchParams.set('redirect_uri', config.redirectUri());
250
+ url.searchParams.set('response_type', 'code');
251
+ url.searchParams.set('code_challenge', await codeChallenge(verifier));
252
+ url.searchParams.set('code_challenge_method', 'S256');
253
+ url.searchParams.set('state', state);
254
+ /*
255
+ A silent attempt asks not to be prompted (PEEK-117), so a browser with no
256
+ session comes straight back with `error=login_required` instead of landing
257
+ on the identity origin's sign-in page.
258
+ */
259
+ if (silent)
260
+ url.searchParams.set('prompt', 'none');
261
+ go(url.toString());
262
+ },
263
+ /**
264
+ * Finish the round trip: verify `state`, redeem the code, keep the token.
265
+ *
266
+ * Throws `SignInError` with something a human can read — every refusal from
267
+ * `/token` is a deliberately uniform `invalid_grant`, so the useful detail is
268
+ * on this side of the exchange.
269
+ */
270
+ async completeSignIn(search) {
271
+ const s = config.storage();
272
+ if (!s)
273
+ throw new SignInError('sign-in requires a browser');
274
+ const params = new URLSearchParams(search);
275
+ const error = params.get('error');
276
+ if (error)
277
+ throw new SignInError(`Estiva ID refused the sign-in (${error}).`);
278
+ const code = params.get('code');
279
+ const state = params.get('state');
280
+ const expectedState = s.getItem(STATE_KEY);
281
+ const verifier = s.getItem(VERIFIER_KEY);
282
+ const returnTo = s.getItem(RETURN_KEY) || '/';
283
+ // Clear first: these are single-use, and a failed attempt must not leave a
284
+ // verifier lying around for a second code to be redeemed against.
285
+ s.removeItem(STATE_KEY);
286
+ s.removeItem(VERIFIER_KEY);
287
+ s.removeItem(RETURN_KEY);
288
+ if (!code)
289
+ throw new SignInError('Estiva ID sent no authorization code back.');
290
+ if (!verifier || !expectedState) {
291
+ throw new SignInError('This sign-in did not start in this tab. Try signing in again.');
292
+ }
293
+ if (state !== expectedState) {
294
+ // The one check that is not about ergonomics: a code arriving with
295
+ // someone else's state is the shape of a login-CSRF, refused outright.
296
+ throw new SignInError('Sign-in state did not match. Try signing in again.');
297
+ }
298
+ const response = await send()(new URL('/token', config.base).toString(), {
299
+ method: 'POST',
300
+ headers: { 'content-type': 'application/json' },
301
+ // camelCase — see the file header.
302
+ body: JSON.stringify({
303
+ grantType: 'authorization_code',
304
+ code,
305
+ clientId: config.clientId,
306
+ redirectUri: config.redirectUri(),
307
+ codeVerifier: verifier,
308
+ }),
309
+ });
310
+ if (!response.ok) {
311
+ throw new SignInError(response.status === 400
312
+ ? 'Estiva ID rejected this sign-in code. It may have expired — codes last 60 seconds.'
313
+ : `Estiva ID returned ${response.status} exchanging the code.`);
314
+ }
315
+ const body = (await response.json());
316
+ if (!body.access_token || !body.pubkey)
317
+ throw new SignInError('Estiva ID returned no token.');
318
+ const token = tokenFrom(body, body.pubkey);
319
+ s.setItem(TOKEN_KEY, JSON.stringify(token));
320
+ // The attempt worked, so the next boot is entitled to a fresh silent one.
321
+ clearGuard(s);
322
+ return { token, returnTo };
323
+ },
324
+ /**
325
+ * Renew the access token without leaving the page (PEEK-108).
326
+ *
327
+ * This is the whole reason the grant exists. Before it, an expired token
328
+ * could only be replaced by a top-level navigation through `/authorize`,
329
+ * which costs whatever the person had on screen.
330
+ *
331
+ * Returns `null` for every failure, and the failures are not worth
332
+ * distinguishing: Estiva ID answers `invalid_grant` for all of them by
333
+ * design, and the caller's only move either way is to fall back to the
334
+ * shell. The stored token is cleared on refusal so a spent refresh token is
335
+ * never presented twice — a replay is what Estiva ID treats as theft.
336
+ */
337
+ async refreshAccessToken() {
338
+ const s = config.storage();
339
+ const current = storedToken();
340
+ if (!s || !current?.refreshToken)
341
+ return null;
342
+ let response;
343
+ try {
344
+ response = await send()(new URL('/token', config.base).toString(), {
345
+ method: 'POST',
346
+ headers: { 'content-type': 'application/json' },
347
+ body: JSON.stringify({
348
+ grantType: 'refresh_token',
349
+ refreshToken: current.refreshToken,
350
+ clientId: config.clientId,
351
+ }),
352
+ });
353
+ }
354
+ catch {
355
+ // Unreachable rather than refused. The token in hand may still be fine,
356
+ // so this deliberately does not clear the session — a flaky network must
357
+ // not sign somebody out.
358
+ return null;
359
+ }
360
+ if (!response.ok) {
361
+ clearSession();
362
+ return null;
363
+ }
364
+ const body = (await response.json());
365
+ if (!body.access_token) {
366
+ clearSession();
367
+ return null;
368
+ }
369
+ const next = tokenFrom(body, body.pubkey ?? current.pubkey);
370
+ s.setItem(TOKEN_KEY, JSON.stringify(next));
371
+ return next;
372
+ },
373
+ /**
374
+ * Leave for Estiva ID to end the session *there* (PEEK-122).
375
+ *
376
+ * Clearing the local token is not signing out. The `estiva_id_session` cookie
377
+ * on the identity origin is untouched by it, so the shell's silent probe
378
+ * succeeds on the next boot and puts the person straight back in — which is
379
+ * exactly what "sign out doesn't sign me out" was.
380
+ *
381
+ * The cookie can only be reached by going there, for the same reason entry
382
+ * works the way it does. The reason is stashed first so the shell offers a
383
+ * passkey on return rather than silently probing for a session just ended.
384
+ */
385
+ beginSignOut() {
386
+ clearSession();
387
+ // Going to /logout makes any half-started sign-in moot, and this is the
388
+ // one caller that genuinely means both.
389
+ clearPendingSignIn();
390
+ stashShellReason('no_session');
391
+ const url = new URL('/logout', config.base);
392
+ url.searchParams.set('client_id', config.clientId);
393
+ url.searchParams.set('post_logout_redirect_uri', config.redirectUri());
394
+ go(url.toString());
395
+ },
396
+ };
397
+ }
398
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `@estiva-app/identity` — signing in with Estiva ID, once.
3
+ *
4
+ * ## What is in here, and what is deliberately not
5
+ *
6
+ * **In:** the PKCE flow, token storage and validity, silent renewal, sign-out,
7
+ * the returnTo and shell-reason vocabulary, and the auth-shell state machine.
8
+ * Everything that decides *whether somebody is signed in* and *what to say when
9
+ * they are not*.
10
+ *
11
+ * **Not in: any app's data layer.** `peek-app/src/auth/useEstivaIdAuth.ts` is the
12
+ * adapter Convex's `ConvexProviderWithAuth` reads, and it stays in Peek. If it
13
+ * were here, Ship and every future app would inherit a Convex dependency to work
14
+ * around — which is precisely the failure this package's sequencing exists to
15
+ * avoid. Ship needs no adapter at all: it hands the token straight to its relay
16
+ * client, and that is the test that the line held.
17
+ *
18
+ * **Not in: the shell's rendering.** It is a full-page surface and each app will
19
+ * want its own. What is shared is the state machine and the reason vocabulary,
20
+ * which is where the expensive mistakes live. This also keeps the package clear
21
+ * of `@estiva-app/ui` — ADR 0002 §2 names identity depending on ui as the trigger
22
+ * that folds ui back into the foundation repo, and that should be a decision
23
+ * rather than a discovery.
24
+ *
25
+ * ## Every difference between the two consumers is a parameter
26
+ *
27
+ * `clientId`, `redirectUri`, `storage`, `keyPrefix` and `navigate` are all
28
+ * injected, because Peek and Ship genuinely differ on every one of them and
29
+ * hardcoding any would have made the package come out Peek-shaped. See
30
+ * `client.ts` for what each difference actually is.
31
+ *
32
+ * ## The globals prediction this package falsified
33
+ *
34
+ * Before starting, the expectation on record was that identity could not follow
35
+ * ADR 0002 §4a as cleanly as `@estiva-app/protocol` did — that being
36
+ * browser-shaped throughout, it would need `lib: dom` and a looser rule.
37
+ *
38
+ * It did not. `crypto`, `btoa`, `TextEncoder`, `URL`, `URLSearchParams`, `fetch`
39
+ * and `console` are declared inside the modules that use them and read inside
40
+ * function bodies; the DOM's `Storage` became a local `KeyValueStore` interface
41
+ * that `sessionStorage` and `localStorage` both satisfy structurally; and
42
+ * navigation is a required `navigate` callback rather than a `location` reach.
43
+ * The published declarations name no ambient global, and the package builds with
44
+ * `types: []` and no `lib: dom` exactly as `protocol` does.
45
+ *
46
+ * The thing that made it possible was not cleverness — it was that a browser API
47
+ * an app must inject is also a browser API a test can observe.
48
+ */
49
+ export declare const IDENTITY_VERSION = "0.1.0";
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';
52
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
package/dist/index.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `@estiva-app/identity` — signing in with Estiva ID, once.
3
+ *
4
+ * ## What is in here, and what is deliberately not
5
+ *
6
+ * **In:** the PKCE flow, token storage and validity, silent renewal, sign-out,
7
+ * the returnTo and shell-reason vocabulary, and the auth-shell state machine.
8
+ * Everything that decides *whether somebody is signed in* and *what to say when
9
+ * they are not*.
10
+ *
11
+ * **Not in: any app's data layer.** `peek-app/src/auth/useEstivaIdAuth.ts` is the
12
+ * adapter Convex's `ConvexProviderWithAuth` reads, and it stays in Peek. If it
13
+ * were here, Ship and every future app would inherit a Convex dependency to work
14
+ * around — which is precisely the failure this package's sequencing exists to
15
+ * avoid. Ship needs no adapter at all: it hands the token straight to its relay
16
+ * client, and that is the test that the line held.
17
+ *
18
+ * **Not in: the shell's rendering.** It is a full-page surface and each app will
19
+ * want its own. What is shared is the state machine and the reason vocabulary,
20
+ * which is where the expensive mistakes live. This also keeps the package clear
21
+ * of `@estiva-app/ui` — ADR 0002 §2 names identity depending on ui as the trigger
22
+ * that folds ui back into the foundation repo, and that should be a decision
23
+ * rather than a discovery.
24
+ *
25
+ * ## Every difference between the two consumers is a parameter
26
+ *
27
+ * `clientId`, `redirectUri`, `storage`, `keyPrefix` and `navigate` are all
28
+ * injected, because Peek and Ship genuinely differ on every one of them and
29
+ * hardcoding any would have made the package come out Peek-shaped. See
30
+ * `client.ts` for what each difference actually is.
31
+ *
32
+ * ## The globals prediction this package falsified
33
+ *
34
+ * Before starting, the expectation on record was that identity could not follow
35
+ * ADR 0002 §4a as cleanly as `@estiva-app/protocol` did — that being
36
+ * browser-shaped throughout, it would need `lib: dom` and a looser rule.
37
+ *
38
+ * It did not. `crypto`, `btoa`, `TextEncoder`, `URL`, `URLSearchParams`, `fetch`
39
+ * and `console` are declared inside the modules that use them and read inside
40
+ * function bodies; the DOM's `Storage` became a local `KeyValueStore` interface
41
+ * that `sessionStorage` and `localStorage` both satisfy structurally; and
42
+ * navigation is a required `navigate` callback rather than a `location` reach.
43
+ * The published declarations name no ambient global, and the package builds with
44
+ * `types: []` and no `lib: dom` exactly as `protocol` does.
45
+ *
46
+ * The thing that made it possible was not cleverness — it was that a browser API
47
+ * an app must inject is also a browser API a test can observe.
48
+ */
49
+ export const IDENTITY_VERSION = '0.1.0';
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
+ export { SignInError, createEstivaId, } from './client.js';
52
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAA;AAEvC,OAAO,EAQL,aAAa,EACb,UAAU,EACV,cAAc,EACd,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,UAAU,EACV,sBAAsB,EACtB,SAAS,EACT,SAAS,EACT,UAAU,EACV,aAAa,GACd,MAAM,YAAY,CAAA;AAEnB,OAAO,EAKL,WAAW,EACX,cAAc,GACf,MAAM,aAAa,CAAA"}