@gaballahtech/seba 0.3.0 → 0.5.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.
Files changed (76) hide show
  1. package/ARCHITECTURE.md +4 -2
  2. package/CHANGELOG.md +130 -0
  3. package/MIGRATION.md +60 -0
  4. package/README.md +174 -8
  5. package/SECURITY.md +1 -1
  6. package/assets/seba-icon-favicon.png +0 -0
  7. package/assets/seba-icon-solid.png +0 -0
  8. package/assets/seba-logo-inverse.png +0 -0
  9. package/assets/seba-logo-monochrome.png +0 -0
  10. package/assets/seba-logo-primary.png +0 -0
  11. package/dist/assets/icons.d.ts +32 -0
  12. package/dist/assets/icons.d.ts.map +1 -0
  13. package/dist/assets/icons.js +30 -0
  14. package/dist/assets/icons.js.map +1 -0
  15. package/dist/assets/png-data.d.ts +8 -0
  16. package/dist/assets/png-data.d.ts.map +1 -0
  17. package/dist/assets/png-data.js +13 -0
  18. package/dist/assets/png-data.js.map +1 -0
  19. package/dist/browser/platform.d.ts +6 -0
  20. package/dist/browser/platform.d.ts.map +1 -1
  21. package/dist/browser/platform.js +17 -0
  22. package/dist/browser/platform.js.map +1 -1
  23. package/dist/browser/storage.d.ts +3 -2
  24. package/dist/browser/storage.d.ts.map +1 -1
  25. package/dist/browser/storage.js +26 -14
  26. package/dist/browser/storage.js.map +1 -1
  27. package/dist/cli.d.ts +61 -0
  28. package/dist/cli.d.ts.map +1 -0
  29. package/dist/cli.js +223 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/core/client.d.ts +18 -8
  32. package/dist/core/client.d.ts.map +1 -1
  33. package/dist/core/client.js +130 -6
  34. package/dist/core/client.js.map +1 -1
  35. package/dist/core/errors.d.ts +10 -1
  36. package/dist/core/errors.d.ts.map +1 -1
  37. package/dist/core/errors.js +7 -2
  38. package/dist/core/errors.js.map +1 -1
  39. package/dist/core/events.d.ts +27 -0
  40. package/dist/core/events.d.ts.map +1 -0
  41. package/dist/core/events.js +45 -0
  42. package/dist/core/events.js.map +1 -0
  43. package/dist/core/http.d.ts.map +1 -1
  44. package/dist/core/http.js +6 -1
  45. package/dist/core/http.js.map +1 -1
  46. package/dist/core/oidc.d.ts +17 -0
  47. package/dist/core/oidc.d.ts.map +1 -1
  48. package/dist/core/oidc.js +61 -27
  49. package/dist/core/oidc.js.map +1 -1
  50. package/dist/core/session.d.ts +10 -3
  51. package/dist/core/session.d.ts.map +1 -1
  52. package/dist/core/session.js +131 -20
  53. package/dist/core/session.js.map +1 -1
  54. package/dist/element.d.ts +43 -0
  55. package/dist/element.d.ts.map +1 -0
  56. package/dist/element.js +416 -0
  57. package/dist/element.js.map +1 -0
  58. package/dist/index.d.ts +6 -1
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +3 -0
  61. package/dist/index.js.map +1 -1
  62. package/dist/pod.d.ts +50 -0
  63. package/dist/pod.d.ts.map +1 -0
  64. package/dist/pod.js +264 -0
  65. package/dist/pod.js.map +1 -0
  66. package/dist/react/index.d.ts +52 -0
  67. package/dist/react/index.d.ts.map +1 -0
  68. package/dist/react/index.js +87 -0
  69. package/dist/react/index.js.map +1 -0
  70. package/dist/seba.d.ts +40 -1
  71. package/dist/seba.d.ts.map +1 -1
  72. package/dist/seba.js +90 -2
  73. package/dist/seba.js.map +1 -1
  74. package/dist/types.d.ts +110 -6
  75. package/dist/types.d.ts.map +1 -1
  76. package/package.json +27 -1
package/ARCHITECTURE.md CHANGED
@@ -168,8 +168,10 @@ login(options)
168
168
 
169
169
  The persisted record (`PersistedSession`) is the only durable state:
170
170
  `version`, `webId`, `provider`, `clientId`, `refreshToken`, `key`
171
- (`CryptoKey` + public JWK, or exported JWK), optional `scope`, `createdAt`.
172
- **No access token field exists in the type**, so it cannot be persisted by accident.
171
+ (`CryptoKey` + public JWK, or exported JWK), optional `scope`, `createdAt`, plus
172
+ `accessToken`/`accessTokenExpiresAt` **only** for providers that issue no refresh token
173
+ (soft-persist, so a reload can resume until the token expires). Refresh-token sessions
174
+ never persist an access token.
173
175
 
174
176
  Refresh is serialized through a single in-flight promise in `core/session.ts`, so ten
175
177
  concurrent `fetch()` calls that all see an expired token perform one refresh, not ten
package/CHANGELOG.md CHANGED
@@ -1,5 +1,135 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.0] — 2026-11-06
4
+
5
+ Additive release: a drop-in sign-in component, session events, Pod helpers and React
6
+ bindings. No behaviour change to login, refresh, storage or validation.
7
+
8
+ ### Added
9
+
10
+ - **`<seba-login>`** — a zero-dependency custom element with shadow DOM. It starts the login,
11
+ completes the callback on the redirect route, exposes `login()`/`logout()`/`session`, and
12
+ switches between "Sign in" and "Sign out". Attributes: `client-id`, `redirect-uri`,
13
+ `issuer`, `web-id`, `config` (JSON `SebaConfig` overrides), `label`, `theme`
14
+ (`auto`/`light`/`dark`), `logo`, `account`. `redirect-uri` is optional (defaults to the
15
+ current page URL), `issuer-url` is accepted as a SolidOS-style alias, and `providers`
16
+ renders an issuer picker. The element also emits `login-success` alongside `seba:login`.
17
+ Register it with `defineSebaLogin()`; importing the package stays side-effect free.
18
+ - **Session events** — `onSessionChange`, `offSessionChange`, `onSessionExpired` and
19
+ `getSession()` on the facade, plus `SessionChangeEvent`/`SessionChangeReason` types.
20
+ Reasons: `login`, `restore`, `logout`, `expired`; `expired` fires at most once per session.
21
+ Listener exceptions are swallowed so application callbacks cannot break session handling.
22
+ - **`Seba.pod`** — `parseSubjectTriples`, `objectify` and `toTurtle`, also exported from the
23
+ package root zero-dependency helpers for the Turtle subset used by Solid pods.
24
+ - **`seba client-id` CLI** — a zero-dependency generator for the Solid-OIDC Client ID
25
+ Document. It validates HTTPS/loopback URLs, emits `scope`, `grant_types` and
26
+ `response_types`, supports repeated `--redirect-uri`, and can write the file with `--out`
27
+ (or print it to stdout for build scripts).
28
+ - **React bindings** — `@gaballahtech/seba/react` exports `SebaLoginButton` and `useSession`.
29
+ `react` is an optional peer dependency; the core entry point never imports it. The button
30
+ accepts `config` and `providers` props, and `redirectUri` is optional.
31
+ - **PNG brand assets** — five 512×512 PNGs in `assets/` (primary, inverse, monochrome, solid,
32
+ favicon) served through the `./assets/*` export, plus 48×48 PNG data URIs embedded for the
33
+ component. No SVG is used or shipped. The source PNGs were re-encoded losslessly (pixel
34
+ comparison unchanged) and now total ~24 kB instead of ~64 kB.
35
+
36
+ ### Notes
37
+
38
+ - This release has **not yet been tested against a provider** in its component/React form.
39
+ The underlying login, refresh and DPoP paths are unchanged from 0.4.0.
40
+
41
+ ## [0.4.0] — 2026-11-05
42
+
43
+ Completeness release driven by the first public full-cycle validation
44
+ (solidcommunity.net login → fetch → Pod write; see the validating app's TEST_REPORT.md).
45
+
46
+ ### Fixed (2026-11-06, same release, second validation round)
47
+
48
+ - **All resource fetches and token requests now use `cache: "no-cache"`.** Community
49
+ Solid Server emits no `Cache-Control` on resource GETs, so browsers apply heuristic
50
+ freshness — a burst of rapid writes reads a heuristically stale pre-PUT ETag and every
51
+ conditional `PUT` with `If-Match` fails with `412` (reproduced and diagnosed live via
52
+ instrumented saves: fresh GET → correct `If-Match` → `412`, deterministically, while
53
+ the server never saw a race). `seba.fetch` no longer trusts local caches.
54
+ - **Sessions are persisted even when the provider issues no refresh token** (soft-persist).
55
+ The record then contains the access token + DPoP key + expiry, and
56
+ `restoreSession()` resumes the session until the access token expires. Only refresh-token
57
+ sessions never store the access token (unchanged principle: refresh token or nothing).
58
+ Expiry on restore clears the record with an explicit `SebaSessionExpiredError`
59
+ ("…the provider issued no refresh token…"). This keeps reload working against providers
60
+ like solidcommunity.net, which advertise `offline_access` yet issue no refresh token
61
+ (verified from a real token exchange).
62
+
63
+ ### Added
64
+
65
+ - **Per-issuer compliance tolerances** (`tolerances?: Record<string, ComplianceTolerances>`).
66
+ Relax `allowNonCompliantAudience` / the new `allowNonCompliantCnf` for one provider
67
+ only while every other provider stays fully strict. Entries match the issuer exactly
68
+ or modulo a trailing slash, and override the global boolean flags. Applications that
69
+ talk to more than one provider should switch from the global flags to this.
70
+ - **`allowNonCompliantCnf`** (`SebaConfig` and `OIDCConfiguration`): escape hatch (same
71
+ warn-once pattern) for providers whose ID tokens omit `cnf.jkt` while still issuing
72
+ DPoP-bound access tokens (solidcommunity.net does exactly this). DPoP proofs remain
73
+ mandatory on every request; a present `cnf.jkt` is still verified against the session
74
+ key; signature, `iss`, `exp`/`iat`, `nonce`, `azp` and `client_id` audience checks stay on.
75
+ - **`error.stage`** — stable step identifier on every `SebaError` (`token-exchange`,
76
+ `id-token-signature`, `id-token-audience`, `id-token-cnf-binding`,
77
+ `authorization-response-*`, `network`, …) so applications/support can pinpoint where a
78
+ login failed without parsing messages.
79
+ - **`maxDpopNonceRetries`** (default 1, previously hard-coded) for both the token-endpoint
80
+ and the resource-fetch `use_dpop_nonce` challenge loops; providers rotating nonces on
81
+ every request benefit from 2–3.
82
+ - **`account`** config option: per-account storage namespace (`seba:<account>` IndexedDB
83
+ database plus a namespaced sessionStorage transaction key) enabling more than one
84
+ concurrent session in the same browser profile.
85
+ - **`Seba.getSessionInfo()`** returns a token-free `{ webId, provider, accessTokenExpiresAt }`
86
+ snapshot so applications can warn before the access token expires.
87
+ - **`Seba.completeLogin()`** completes a pending callback without falling back to starting
88
+ a login — pairs with `Seba.isCallbackResponse(url?)` for the explicit
89
+ "login() on click, completeLogin() on the callback route" pattern that avoids the
90
+ never-settling redirect promise. `login()` itself still handles both roles.
91
+ - **MIGRATION.md** with a `@inrupt/solid-client-authn-browser` mapping.
92
+
93
+ ### Fixed
94
+
95
+ - **`allowNonCompliantAudience` was dead code.** Declared on `SebaConfig` and read from the
96
+ provider configuration during token validation, but never copied between the two: it is
97
+ now carried through `resolveConfig` and stamped onto every discovered configuration at all
98
+ three paths (login start, login completion, `SessionManager.discover`) via
99
+ `applyComplianceTolerances`.
100
+ - **`SebaNetworkError` message** now names CORS as the first browser-side suspect, instead of
101
+ only mentioning Node.js proxies.
102
+ - **logout without a provider revocation endpoint** now logs that the refresh token remains
103
+ valid server-side until expiry, instead of silently returning.
104
+
105
+ ## [0.3.1] — 2026-11-05
106
+
107
+ Fixes found during the first real-world login attempt against `solidcommunity.net`
108
+ (manual browser flow, tunnel-published Client ID Document;see the app's TEST_REPORT.md).
109
+
110
+ ### Fixed
111
+
112
+ - **`allowNonCompliantAudience` was dead code.** The flag was declared on `SebaConfig`
113
+ (types.d.ts:75) and documented as the opt-in for providers such as `solidcommunity.net`,
114
+ and `checkIdTokenAudience` read it from the provider configuration object — but nothing
115
+ ever copied it from the user's `Seba` config into that object. `resolveConfig` dropped
116
+ the unknown key, and `discoverProvider` builds the configuration purely from the
117
+ discovery document, so setting the flag had zero effect and the 0.2.1 hard-throw still
118
+ fired. It is now carried through `resolveConfig` (`ResolvedConfig`) and stamped onto
119
+ every discovered provider configuration at all three paths — login start (`#beginLogin`),
120
+ login completion (`#completeLogin`) and the `SessionManager` `discover` callback that
121
+ backs restore/refresh — via a new `applyComplianceTolerances` helper.
122
+
123
+ ### Added
124
+
125
+ - **`allowNonCompliantCnf`** (`SebaConfig` and `OIDCConfiguration`), the same escape-hatch
126
+ pattern as `allowNonCompliantAudience`, for providers whose ID tokens omit `cnf.jkt`
127
+ while still issuing DPoP-bound access tokens (solidcommunity.net does exactly this:
128
+ its ID token has no `cnf` claim). DPoP proofs remain mandatory on every request, an
129
+ ID token that DOES carry `cnf.jkt` is still checked against the session key, and
130
+ signature, `iss`, `exp`/`iat`, `nonce`, `azp` and the `client_id` audience checks all
131
+ stay enforced. Warns once via `console.warn` when it suppresses the requirement.
132
+
3
133
  ## [0.2.1] — 2026-11-05
4
134
 
5
135
  Documentation-only patch. No runtime behaviour changed; the only functional difference is
package/MIGRATION.md ADDED
@@ -0,0 +1,60 @@
1
+ # Migrating to SEBA from `@inrupt/solid-client-authn-browser`
2
+
3
+ This guide maps the common operations of `@inrupt/solid-client-authn-browser` (v3) to
4
+ `@gaballahtech/seba`. It is written from a real migration (a Google-Tasks-style todo app
5
+ validated against solidcommunity.net).
6
+
7
+ | Inrupt | SEBA | Notes |
8
+ |---|---|---|
9
+ | `handleIncomingRedirect()` on page load | `await seba.login()` **or** the explicit pair `seba.isCallbackResponse() && await seba.completeLogin()` | Both work. `login()` on a callback URL completes it; elsewhere it *navigates away* (document never unloads the promise). Use the explicit pair when that pattern is confusing. |
10
+ | `login({ oidcIssuer, redirectUrl, clientName })` | `seba.login({ issuer })` (redirect URL + client id come from one `new Seba({...})`) | |
11
+ | `fetchDefaultSession()` / `fetch(session)` | `seba.fetch(url, init)` | Always DPoP-signed with a fresh proof; cross-origin redirects are returned, never followed with credentials. |
12
+ | `getDefaultSession().info.webId` | `(await seba.getSessionInfo())?.webId` | SessionInfo is token-free and also carries `accessTokenExpiresAt`. |
13
+ | `getDefaultSession().info.isLoggedIn` | `(await seba.getSessionInfo()) !== null` | |
14
+ | `logout()` | `await seba.logout()` | SEBA also revokes the refresh token when the provider advertises revocation. |
15
+ | dynamic client registration | **none by design** — publish a Client ID Document; its URL is the `client_id` | See the README section "Publish a Client ID Document". Host a JSON-LD file at a stable HTTPS URL; no registration call is ever made. |
16
+ | `session.events.sessionRestore` / expiry events | poll `getSessionInfo()` → `accessTokenExpiresAt` | SEBA refreshes automatically; tell users *before* the access token lapses. |
17
+
18
+ ## Conceptual differences worth knowing
19
+
20
+ 1. **Tokens are not exposed.** Inrupt hands you the access token; SEBA never does. Access
21
+ tokens live in memory only, and the refresh token + DPoP key are the only persisted
22
+ secrets. If you think you need the raw token, re-check the design: `seba.fetch` covers it.
23
+ 2. **A brand-new DPoP proof per attempt.** No proof cache, no sharing; replays are structurally
24
+ impossible. Cross-origin redirects are returned to you instead of followed with credentials.
25
+ 3. **Provider conformance gaps are opt-in per provider.** Real-world providers (solidcommunity.net,
26
+ any recent CSS) violate Solid-OIDC §8.1 (`aud` without `"solid"`, missing `cnf.jkt`). Pass
27
+ `tolerances` for exactly those issuers — everything else stays fully strict:
28
+
29
+ ```ts
30
+ const seba = new Seba({
31
+ clientId: location.origin + "/client-id.jsonld",
32
+ redirectUri: location.origin + "/callback",
33
+ tolerances: {
34
+ "https://solidcommunity.net/": { allowNonCompliantAudience: true, allowNonCompliantCnf: true },
35
+ },
36
+ });
37
+ ```
38
+
39
+ 4. **Errors carry `code` and `stage`.** Branch on `error.code`
40
+ (`SEBA_ERR_SESSION_EXPIRED` → re-login; `SEBA_ERR_NETWORK` → transport/CORS) and
41
+ `error.stage` (`token-exchange`, `id-token-*`, `authorization-response-*`) for triage.
42
+
43
+ ## Minimal app skeleton
44
+
45
+ ```ts
46
+ import { Seba } from "@gaballahtech/seba";
47
+
48
+ const seba = new Seba({
49
+ clientId: window.location.origin + "/client-id.jsonld",
50
+ redirectUri: window.location.origin + "/callback",
51
+ });
52
+
53
+ // On mount:
54
+ const info = await seba.getSessionInfo();
55
+ if (seba.isCallbackResponse()) {
56
+ await seba.completeLogin();
57
+ } else if (info === null) {
58
+ // render a login button; call `seba.login()` inside onClick (it navigates away)
59
+ }
60
+ ```
package/README.md CHANGED
@@ -18,6 +18,8 @@ the same in both.
18
18
  ## Requirements
19
19
 
20
20
  - Node.js 20 or newer, or any browser with Web Crypto (ES256), `fetch` and IndexedDB.
21
+ Chrome/Edge and Firefox are the tested browsers; Safari 17 or newer is expected to work
22
+ (older Safari versions fall back to an extractable-JWK DPoP key — see `SECURITY.md`).
21
23
  - A publicly dereferenceable **Solid Client ID Document** served over HTTPS.
22
24
 
23
25
  ## Install
@@ -31,6 +33,23 @@ npm install @gaballahtech/seba
31
33
  Host a JSON-LD document and use its URL as `clientId`. The `client_id` member must equal the
32
34
  document URL, and every callback you use must be listed verbatim.
33
35
 
36
+ The package ships a zero-dependency generator, so the document does not have to be written by
37
+ hand:
38
+
39
+ ```bash
40
+ npx @gaballahtech/seba client-id \
41
+ --client-id https://app.example/client-id.jsonld \
42
+ --redirect-uri https://app.example/auth/callback \
43
+ --client-name "My Solid App" \
44
+ --out public/client-id.jsonld
45
+ ```
46
+
47
+ It validates the URLs (HTTPS, or loopback HTTP for local development), writes spec-shaped
48
+ metadata (`scope`, `grant_types`, `response_types`), and reminds you to serve the file as
49
+ `application/ld+json` at the `client_id` URL. Repeat `--redirect-uri` for additional
50
+ callbacks; run `npx @gaballahtech/seba client-id --help` for all options. Printing to stdout
51
+ (without `--out`) keeps it usable in CI or a build script.
52
+
34
53
  ```json
35
54
  {
36
55
  "@context": ["https://www.w3.org/ns/solid/oidc-context.jsonld"],
@@ -80,9 +99,76 @@ const session = (await seba.restoreSession()) ?? (await seba.login());
80
99
  `login()` with no arguments opens a small built-in dialog asking for a WebID or an identity
81
100
  provider URL. Pass `{ webId }` or `{ issuer }` to skip it.
82
101
 
83
- ### React
102
+ ### Sign-in button
103
+
104
+ `<seba-login>` is a dependency-free custom element. Importing the package has no side
105
+ effects; register the element once with `defineSebaLogin()`.
106
+
107
+ ```html
108
+ <script type="module">
109
+ import { defineSebaLogin } from "@gaballahtech/seba";
110
+
111
+ defineSebaLogin();
112
+ </script>
113
+
114
+ <seba-login
115
+ client-id="https://app.example/client-id.jsonld"
116
+ redirect-uri="https://app.example/auth/callback"
117
+ issuer="https://idp.example"
118
+ theme="auto"
119
+ label="Sign in with Solid"
120
+ ></seba-login>
121
+
122
+ <script type="module">
123
+ const button = document.querySelector("seba-login");
124
+ button.addEventListener("seba:login", (event) => {
125
+ console.log("signed in as", event.detail.session.webId);
126
+ });
127
+ button.addEventListener("seba:error", (event) => {
128
+ console.error(event.detail.error);
129
+ });
130
+ </script>
131
+ ```
132
+
133
+ The element starts the login on click, completes the callback when it is loaded on the
134
+ redirect route, restores the stored session on a normal page load, and switches to "Sign
135
+ out" once a session exists. Attributes: `client-id` is the only required one; `redirect-uri`
136
+ defaults to the current page URL (fragment removed). Also available: `issuer` (or the
137
+ SolidOS-style alias `issuer-url`), `providers` (space/comma separated issuer URLs rendered as
138
+ a picker when there are two or more), `web-id`, `config` (JSON `SebaConfig` overrides such as
139
+ per-provider `tolerances`), `label`, `theme` (`auto` | `light` | `dark`), `logo`, and
140
+ `account`. The element also emits `login-success` as an alias of `seba:login` for apps
141
+ written against `solid-ui`.
142
+
143
+ React (`react` is an optional peer dependency):
144
+
145
+ ```tsx
146
+ import { SebaLoginButton, useSession } from "@gaballahtech/seba/react";
147
+ import type { Seba } from "@gaballahtech/seba";
148
+
149
+ export function App({ seba }: { seba: Seba }) {
150
+ const session = useSession(seba); // null while signed out
151
+
152
+ return (
153
+ <>
154
+ <SebaLoginButton
155
+ clientId="https://app.example/client-id.jsonld"
156
+ redirectUri="https://app.example/auth/callback"
157
+ issuer="https://idp.example"
158
+ onLogin={(session) => console.log(session.webId)}
159
+ />
160
+ <p>{session ? session.webId : "Signed out"}</p>
161
+ </>
162
+ );
163
+ }
164
+ ```
165
+
166
+ The component registers `<seba-login>` on mount. `useSession` follows the session of an
167
+ existing `Seba` instance (the one you use for `fetch`), while the button owns its own.
168
+ `SebaLoginButton` also accepts `providers` (issuer URLs), `config` (JSON-serialized
169
+ `SebaConfig` overrides) and an optional `redirectUri`.
84
170
 
85
- See [`examples/react/App.tsx`](examples/react/App.tsx).
171
+ See also [`examples/react/App.tsx`](examples/react/App.tsx).
86
172
 
87
173
  ## Node.js
88
174
 
@@ -169,6 +255,10 @@ logout(): Promise<void>
169
255
  Clears memory and storage first, then revokes the refresh token if the provider advertises a
170
256
  revocation endpoint. Revocation failures never block logout.
171
257
 
258
+ > ⚠️ When the provider advertises no `revocation_endpoint` (e.g. solidweb.org), logout only
259
+ > clears the local state — the refresh token remains valid on the server until it expires.
260
+ > Revoke it out-of-band or let it lapse.
261
+
172
262
  ### `Session`
173
263
 
174
264
  ```ts
@@ -181,6 +271,76 @@ interface Session {
181
271
 
182
272
  No tokens, no keys, no expiry bookkeeping.
183
273
 
274
+ ### `onSessionChange(listener)` / `onSessionExpired(listener)`
275
+
276
+ ```ts
277
+ onSessionChange(listener: (event: SessionChangeEvent) => void): () => void
278
+ onSessionExpired(listener: (event: SessionChangeEvent) => void): () => void
279
+ offSessionChange(listener: (event: SessionChangeEvent) => void): void
280
+ getSession(): Promise<Session | null>
281
+ ```
282
+
283
+ Subscribe before calling `login()`/`restoreSession()` so no transition is missed. Both
284
+ subscribe methods return an unsubscribe function. Listener exceptions are swallowed and
285
+ never interrupt the session flow.
286
+
287
+ | Surface | Event | `detail` | Emitted when |
288
+ |---|---|---|---|
289
+ | Facade | `reason: "login"` | `{ reason, session }` | An interactive login finished and the session was stored. |
290
+ | Facade | `reason: "restore"` | `{ reason, session }` | `restoreSession()` resumed a stored session. |
291
+ | Facade | `reason: "logout"` | `{ reason, session: null }` | `logout()` cleared the local session. |
292
+ | Facade | `reason: "expired"` | `{ reason, session: null }` | The session cannot continue; at most once per session. |
293
+ | Element | `seba:login` | `{ session }` | Same as the `login` reason. |
294
+ | Element | `seba:restore` | `{ session }` | Same as the `restore` reason. |
295
+ | Element | `seba:logout` | `{ session: null }` | Same as the `logout` reason. |
296
+ | Element | `seba:session-expired` | `{ session: null }` | Same as the `expired` reason. |
297
+ | Element | `login-success` | `{ session }` | Compatibility alias of `seba:login` (SolidOS naming). |
298
+ | Element | `seba:error` | `{ error }` | A login, logout or configuration error in the component. |
299
+
300
+ ```ts
301
+ const unsubscribe = seba.onSessionExpired(() => {
302
+ showSignInDialog();
303
+ });
304
+ ```
305
+
306
+ ### `Seba.pod`
307
+
308
+ Zero-dependency helpers for the Turtle subset used by Solid pods: `parseSubjectTriples`,
309
+ `objectify` and `toTurtle` are also exported from the package root. They handle absolute
310
+ IRIs, blank nodes, string/number/boolean literals, `@lang` and `^^<datatype>` suffixes, and
311
+ ignore `@prefix`/`PREFIX`/`BASE` directives (prefix expansion is not performed).
312
+
313
+ ## Brand & logo usage
314
+
315
+ All artwork is PNG. The five 512×512 files in [`assets/`](assets) are shipped through the
316
+ `./assets/*` export and are also embedded as 48×48 PNG data URIs, so the sign-in button paints
317
+ without a network request. No SVG is generated, fetched or required.
318
+
319
+ | Variant | File | Use |
320
+ |---|---|---|
321
+ | `primary` | `assets/seba-logo-primary.png` | Accent-coloured logo on light backgrounds. |
322
+ | `inverse` | `assets/seba-logo-inverse.png` | White logo on dark backgrounds. |
323
+ | `monochrome` | `assets/seba-logo-monochrome.png` | Single-colour contexts, print, embossing. |
324
+ | `solid` | `assets/seba-icon-solid.png` | Filled app icon. |
325
+ | `favicon` | `assets/seba-icon-favicon.png` | Browser favicon and small avatars. |
326
+
327
+ ```html
328
+ <link rel="icon" href="/node_modules/@gaballahtech/seba/assets/seba-icon-favicon.png" />
329
+ <seba-login ... logo="inverse"></seba-login>
330
+ <seba-login ... logo="none"></seba-login>
331
+ ```
332
+
333
+ ```ts
334
+ import { sebaLogo, sebaLogoImage } from "@gaballahtech/seba";
335
+
336
+ sebaLogo.primary.png; // "assets/seba-logo-primary.png"
337
+ sebaLogoImage("favicon"); // "data:image/png;base64,..."
338
+ ```
339
+
340
+ Keep clear space of at least half the mark's height around the logo, do not recolour or
341
+ distort it, and do not place the accent mark on a low-contrast accent background — use the
342
+ `inverse` or `monochrome` treatment instead.
343
+
184
344
  ## Errors
185
345
 
186
346
  All errors extend `SebaError` and carry a stable `code`:
@@ -204,8 +364,9 @@ All errors extend `SebaError` and carry a stable `code`:
204
364
  - ID token signatures are verified against the discovered JWKS; `aud` must contain the
205
365
  client ID and `"solid"` when the provider advertises `webid`; `cnf.jkt` must match the
206
366
  session key (and is required for `webid`).
207
- - The refresh token and DPoP key are the only persisted secrets; access tokens are held in
208
- a private field.
367
+ - The refresh token and DPoP key are persisted. When a provider issues **no** refresh token,
368
+ the access token is stored too (soft-persist) so a reload can resume the session until it
369
+ expires; refresh-token sessions never store an access token.
209
370
  - Non-extractable `CryptoKey` storage is used when the engine supports structured cloning;
210
371
  otherwise an exported JWK fallback is used (documented in `SECURITY.md`).
211
372
  - Refresh-token rotation is atomic, and concurrent refreshes are collapsed into one.
@@ -222,14 +383,19 @@ restore → logout cycle was executed against that provider. If it was not, it s
222
383
  |---|---|---|---|
223
384
  | Hand-written fake provider (`tests/integration`) | Solid-OIDC-shaped: array `aud` with `"solid"`, `azp`, `cnf.jkt` | Full cycle in CI (`npm test`) | — |
224
385
  | Community Solid Server 7.3.0 | **Not conformant** with Solid-OIDC §8.1: ID token has a string `aud` without `"solid"`, no `cnf` | Discovery, Client ID Document, PKCE/state/nonce, consent, callback, DPoP token exchange all pass; login rejected at ID token validation | No |
225
- | solidcommunity.net | Discovery and issuer handling verified; issuer is `https://solidcommunity.net/` | Discovery only | **No** — no account was available |
386
+ | solidcommunity.net (CSS-based) | **Not conformant** with Solid-OIDC §8.1: ID token `aud` lacks `"solid"`, no `cnf.jkt` (same issue family as CSS #2295). Also: advertises `offline_access` but the token response carries **no refresh token** (verified 2026-11-06; requested scope was `openid webid offline_access`) | Partial cycle with a real account (2026-11-05): login + consent + DPoP **Pod write/read** verified — the written file `seba-todos.ttl` was independently fetched and matched the app's input verbatim. Uses per-issuer `tolerances` for the two conformance gaps. Reload works inside the access-token lifetime via soft-persisted sessions | **No** — partial cycle only |
226
387
  | solidweb.org | Discovery verified; issuer is `https://solidweb.org` (no trailing slash); no revocation endpoint advertised | Discovery only | **No** — no account was available |
227
388
  | solidweb.me | Discovery verified; issuer is `https://solidweb.me/` | Discovery only | **No** — no account was available |
228
389
 
229
390
  > **⚠️ Community Solid Server 7.3.0**: CSS issues `id_token.aud` as a string instead of `["solid", client_id]` and omits `cnf.jkt`, violating Solid-OIDC §8.1. Seba correctly rejects this token. Track the upstream issue at https://github.com/CommunitySolidServer/CommunitySolidServer/issues/2295. Do not weaken Seba's validation to work around this.
230
-
231
- No public provider has completed the full cycle in this release. See
232
- [`VALIDATION_REPORT.md`](VALIDATION_REPORT.md) for transcripts and blockers, and
391
+ >
392
+ > **solidcommunity.net** runs a CSS build from the same conformance family. Login completes only with the two documented opt-ins for that provider (`tolerances: { "https://solidcommunity.net/": { allowNonCompliantAudience: true, allowNonCompliantCnf: true } }`); everything else — signature, `client_id` audience, `iss`, PKCE, DPoP proofs on every request — is fully enforced.
393
+
394
+ Full-cycle validation against a **real public provider** (solidcommunity.net, 2026-11-05:
395
+ login → consent → callback → token exchange → ID-token checks → DPoP fetch → Turtle write/read on
396
+ the Pod) is documented in the validating application's TEST_REPORT.md. Remaining unvalidated steps on
397
+ public providers: `restoreSession()` across a reload and `logout()` revocation. See
398
+ [`VALIDATION_REPORT.md`](VALIDATION_REPORT.md) for the earlier transcripts and blockers, and
233
399
  [`CHANGELOG.md`](CHANGELOG.md) for what changed.
234
400
 
235
401
  SEBA does not support dynamic client registration. The Client ID Document URL is the
package/SECURITY.md CHANGED
@@ -7,7 +7,7 @@
7
7
  | Authorization code interception / mix-up | Access + refresh tokens | PKCE S256, random `state`, RFC 9207 `iss` checked against the issuer the request was built for. |
8
8
  | ID token forgery or substitution | User identity (`WebID`) | Signature verified with the provider's JWKS; `iss`, `aud`, `azp`, `exp`, `iat`, `nonce` validated; algorithm taken from the key metadata, not the token's preference. |
9
9
  | DPoP proof replay | Pod access | A new proof (new `jti`, new signature) for every attempt; no proof cache, no TTL, no reuse option. |
10
- | Token theft from the browser profile | Refresh token, DPoP key | Only the refresh token and key are persisted (IndexedDB); access token stays in memory; no `localStorage`; no cookies set by the library. |
10
+ | Token theft from the browser profile | Refresh token, DPoP key, soft-persisted access token | The refresh token and key are persisted (IndexedDB). When a provider issues no refresh token, the access token is persisted too so a reload can resume the session until it expires; refresh-token sessions never store an access token. No `localStorage`; no cookies set by the library. |
11
11
  | XSS on the application origin | Everything the origin can reach | Out of scope to *prevent*; the library minimises the window (no tokens in DOM, no globals, no logging) and documents the residual risk. |
12
12
  | Credential leak via redirects | Access token, DPoP proof | `redirect: "manual"`; cross-origin redirect responses are returned to the caller, never re-issued with credentials. |
13
13
  | Duplicate side effects from retries | Application state | Automatic replay is limited to replayable requests (`GET`/`HEAD`/`OPTIONS`, or a re-readable body), at most one nonce retry and one refresh retry. |
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,32 @@
1
+ /**
2
+ * SEBA brand assets (PNG only).
3
+ *
4
+ * The 512x512 PNGs in `assets/` are shipped with the package through the `./assets/*`
5
+ * export. A 48x48 PNG rendering of each treatment is inlined as a data URI so the
6
+ * sign-in button paints without a network request and the core keeps zero runtime
7
+ * dependencies.
8
+ */
9
+ /** Supported logo treatments. */
10
+ export type SebaLogoVariant = "primary" | "inverse" | "monochrome" | "solid" | "favicon";
11
+ /** One brand asset. */
12
+ export interface SebaLogoAsset {
13
+ /** Treatment name. */
14
+ readonly variant: SebaLogoVariant;
15
+ /** Human readable description. */
16
+ readonly label: string;
17
+ /** Packaged 512x512 PNG file, relative to the package root. */
18
+ readonly png: string;
19
+ /** Inline `data:image/png;base64,...` rendering for runtime use. */
20
+ readonly dataUri: string;
21
+ /** Intrinsic width of the inlined PNG. */
22
+ readonly width: 48;
23
+ /** Intrinsic height of the inlined PNG. */
24
+ readonly height: 48;
25
+ }
26
+ /** Manifest of every brand asset. */
27
+ export declare const sebaLogo: Readonly<Record<SebaLogoVariant, SebaLogoAsset>>;
28
+ /** Inline PNG data URI for a logo treatment, ready for an `<img src>`. */
29
+ export declare function sebaLogoImage(variant?: SebaLogoVariant): string;
30
+ /** Whether a string names a known logo treatment. */
31
+ export declare function isSebaLogoVariant(value: string): value is SebaLogoVariant;
32
+ //# sourceMappingURL=icons.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icons.d.ts","sourceRoot":"","sources":["../../src/assets/icons.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,iCAAiC;AACjC,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,SAAS,GAAG,YAAY,GAAG,OAAO,GAAG,SAAS,CAAC;AAEzF,uBAAuB;AACvB,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,kCAAkC;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,+DAA+D;IAC/D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,0CAA0C;IAC1C,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC;IACnB,2CAA2C;IAC3C,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;CACrB;AAkBD,qCAAqC;AACrC,eAAO,MAAM,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,eAAe,EAAE,aAAa,CAAC,CAAU,CAAC;AAEjF,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,OAAO,GAAE,eAA2B,GAAG,MAAM,CAE1E;AAED,qDAAqD;AACrD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,eAAe,CAEzE"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * SEBA brand assets (PNG only).
3
+ *
4
+ * The 512x512 PNGs in `assets/` are shipped with the package through the `./assets/*`
5
+ * export. A 48x48 PNG rendering of each treatment is inlined as a data URI so the
6
+ * sign-in button paints without a network request and the core keeps zero runtime
7
+ * dependencies.
8
+ */
9
+ import { SEBA_PNG_DATA } from "./png-data.js";
10
+ function asset(variant, label, png) {
11
+ return { variant, label, png, dataUri: SEBA_PNG_DATA[variant], width: 48, height: 48 };
12
+ }
13
+ const ASSETS = {
14
+ primary: asset("primary", "SEBA logo", "assets/seba-logo-primary.png"),
15
+ inverse: asset("inverse", "SEBA logo (inverse)", "assets/seba-logo-inverse.png"),
16
+ monochrome: asset("monochrome", "SEBA logo (monochrome)", "assets/seba-logo-monochrome.png"),
17
+ solid: asset("solid", "SEBA icon", "assets/seba-icon-solid.png"),
18
+ favicon: asset("favicon", "SEBA favicon", "assets/seba-icon-favicon.png"),
19
+ };
20
+ /** Manifest of every brand asset. */
21
+ export const sebaLogo = ASSETS;
22
+ /** Inline PNG data URI for a logo treatment, ready for an `<img src>`. */
23
+ export function sebaLogoImage(variant = "primary") {
24
+ return ASSETS[variant].dataUri;
25
+ }
26
+ /** Whether a string names a known logo treatment. */
27
+ export function isSebaLogoVariant(value) {
28
+ return Object.prototype.hasOwnProperty.call(ASSETS, value);
29
+ }
30
+ //# sourceMappingURL=icons.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icons.js","sourceRoot":"","sources":["../../src/assets/icons.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAqB9C,SAAS,KAAK,CACZ,OAAwB,EACxB,KAAa,EACb,GAAW;IAEX,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;AACzF,CAAC;AAED,MAAM,MAAM,GAA2C;IACrD,OAAO,EAAE,KAAK,CAAC,SAAS,EAAE,WAAW,EAAE,8BAA8B,CAAC;IACtE,OAAO,EAAE,KAAK,CAAC,SAAS,EAAE,qBAAqB,EAAE,8BAA8B,CAAC;IAChF,UAAU,EAAE,KAAK,CAAC,YAAY,EAAE,wBAAwB,EAAE,iCAAiC,CAAC;IAC5F,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,WAAW,EAAE,4BAA4B,CAAC;IAChE,OAAO,EAAE,KAAK,CAAC,SAAS,EAAE,cAAc,EAAE,8BAA8B,CAAC;CAC1E,CAAC;AAEF,qCAAqC;AACrC,MAAM,CAAC,MAAM,QAAQ,GAAqD,MAAM,CAAC;AAEjF,0EAA0E;AAC1E,MAAM,UAAU,aAAa,CAAC,UAA2B,SAAS;IAChE,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC;AACjC,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC7C,OAAO,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAC7D,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Generated 48x48 PNG renderings of the brand artwork, inlined as data URIs.
3
+ *
4
+ * Generated from the 512x512 files in `assets/` (lossless downscale, same PNG format)
5
+ */
6
+ import type { SebaLogoVariant } from "./icons.js";
7
+ export declare const SEBA_PNG_DATA: Readonly<Record<SebaLogoVariant, string>>;
8
+ //# sourceMappingURL=png-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"png-data.d.ts","sourceRoot":"","sources":["../../src/assets/png-data.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAMnE,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Generated 48x48 PNG renderings of the brand artwork, inlined as data URIs.
3
+ *
4
+ * Generated from the 512x512 files in `assets/` (lossless downscale, same PNG format)
5
+ */
6
+ export const SEBA_PNG_DATA = {
7
+ primary: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAABQVBMVEUAAAB8Tf99T/98Tv97TP98TP99Tv+AUP97S/9+T/99Tf9+Tv98T/97Tf98Tf98Tf98Tf98Tf98Tv98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf99Tv98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf99Tv9+Tv98Tf98Tf98Tf98Tf98Tf98Tf98Tf99Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tv98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf99T/98Tv98Tf/B0+kdAAAAanRSTlMAAAAAAAAAAAAAAAAAABsOZNIERrn7LZvxGXvh5oYLXMz0ozMCP7H5/cBOBieT7dhsEhVz3OqMIwhUxf73qzoBAQM4qfbHVgkDTOnddRZvavCVKLCvaaEDFxwe35ZoD02i+L0HiqVEBAECSK4URgAAAn5JREFUSMetVuda4lAQzSESNVixd8ESS+wdVLB3RMECCFmpet7/Afa7UXbDkqDs5/xL7pzM3DMzZyJJP2JwAS58210G0NYGQP62e3tHZ2dH+/cgkOHt6u4he7q7vJC/yktugLu3r5+m9ff1utFQK4gCYGBwiOTwyMgwyaHBAQCKg3sjgNGxcZI+/0RT04TfR3J8bBRAo0Pyk1PTGjkzOzcPAPNzszOkNj01CQW22egLiySXllcANDcDWFleIrm4oFfxJdxX19ZJbmxuiVNVVVXB79bmBsn1tdXKqwDY3gkEyd29/ZAZ3xMOeyQJCkL7e7tkMLCzDfzNCweHR8dk8OT0DIAqIuq6+KIK4Oz0JEgeHx2eWwAXlySvrm/KuboRicBdrvvN9RXJywsL4JaM3t3DNNF08MbiXnw0obD7uyh5awE8kI9Pog/wnBBRkEylkhBfTzxDEP70SD5UAl7M8iDhT2cAGJpmAMikfyVglvTFDiBJUgvSr1kjhzyZR87IvqbhMml0AsjIZOkrFANkoFjwMZsxWXAGSIDhI6MaqUVJn1F+6wiQkSvwjxVyHzQ7AZRWwWExWvaPFsVzq+IEUIBS0sgHtDJAC+SNZMk8sAOoIT0ST2msMC0Vj+gh1R4Q1iOxakAsoofVn0kJkuy2u7RbrkWr8i+tivSzhau7NVx1NZ9de/sd27vWAClVA2Qd0RZZklCKxUuQJLnFYUQtIuCqFAGXvQhUyIxHkKXrgh6PVWYOLACLkL1VCtmbrZBVSaUipFJxlspPwbKKsUepKcafcv9uL/fvDpur3oVS/8oy86peil/tUQCl8totVVDpZPUu9vp/Hcy8FNhtzv+y36MAAZmg32CuAAAAAElFTkSuQmCC",
8
+ inverse: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAABRFBMVEULCw8JCQ0KCg4ICAwlJSkYGBxra23U1NQPDxNOTlG8vL37+/v///82Njmfn6Hy8vIjIyeBgYPj4+Pn5+eLi40jIyYVFRljY2XOzs/19fWnp6k8PD8NDRFHR0u1tbb5+fn9/f3Dw8RVVVgRERVWVli0tLZHR0owMDSXl5nu7u/a2tpzc3UcHCDu7u4fHyJ5eXve3t7r6+yRkZMsLDAICA0TExdbW17IyMn+/v739/evr7BCQkUMDBAODhJBQUStra/JycpdXWAwMDOtra5UVFfq6urf3997e30gICMHBwt1dXhUVFZxcXPx8fGZmZsyMjUJCQ6zs7Rvb3KlpachISUoKCvg4OEmJimampynp6hubnEaGh76+vqmpqibm5z4+PjAwMFVVVcGBgqPj5GPj5BubnCpqapMTE/IyMipqasxMTV2dnikVltMAAACNklEQVRIx61W6T/jUBR9SxEcjGUQOkJiMPal1JIaaizpCIoWbYcu2jL+/+/zS9J12qRJf+7Hl3vuPffl3nMfIZ9klHpyZ9zn48y9P+/o7Orq7OBu3YXunl6gt6dbcAOhrK9/AKYN9PexVqUw/mVwCMDwyMgwgKHBr86l8NGxcQDixCRjkxMigPGxUXte1P9tSgKmZ2SFE8KV2ZlpQPo+52/Oi9H5hR8AFpeWLRqMLy8tAlhZmKeNvBhfXVsHsLG5FWDVGFubGwDW11b/L4Vv7wR3gb39A9XKr6oWS/Vgfw/YDe5s15USOvxpHB8dl49pOEzLoY6PjFAnh6Ga+L9OAZydX1QSa5GIViF7cX4G4PR3NYd2CehX1/SGEGZBhGhUsNwZITf0+koHLrUq4Ba4CwWM7/dm09FYPB6jRvSHeyNCIHQH3NYDHk3OzPeUSHKmyamUrDGeTPzxmSnpYwPg2eLCEy/pjJLNAa9ZRU6/5C3i7NkOwJJpiIViEHgrvotIJ1kLAOEZEdAlQNIBMVO6GQcAU95RsYLCWgGoli3qZX+9mNWoE4BqQkzOBaUyQArm5JhgYJoD1HAkGk+hzlLxaCSsfhagkdLbqyMl70W3c62ef1xtaxTctEZd82Wcmq/U3g9P+dr2ztu3t80A+ZoPkDmif92PaBMR+PhwEgESOjzxJDPehcxOKgO2UlkS4xX3Ymzm989Zcj9bK/dTdnJPvC+UNlaW96VIvK9d4n2xt/F08P44cbB/hbqdaSuT7DQAAAAASUVORK5CYII=",
9
+ monochrome: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAQAAAD9CzEMAAADKElEQVRYw+XYQUhVaRQH8J9pz+RBQlCRSZtgchMONDDhQxqGkFlkQjiB0MZoUREoRIuhxdDGNi2iMAZaSARFNDQjzmIKEUHetHGRrWooopAHBoEPNHma3Vm8q17nvtfT8sLA/N/mnXPP+Z97zvd95/u+y/8Nu+1OjnyrU5566pStG09eo81DCwKBBQ+1qdlI+v1uygsiv7yb9m8M+S4XvQ5pc/r0yYXSaxft+jLytBPGQ7oZtx0AB9w2E2rHnZD+PPJNDhlUEAgsGnVUavlZylGjFgUCBYMO2bRe+n2uexe+5TPnbItZbHPOs9Dinev2rZ18u/NehK5vXbG3rOVeV7wNLV84b3tl8i06ZX0UCMy5r0XVqudVMbnFfXMCgY+yOm0pT17toHveh8aPHVdXonTxUtQ57nH4Uu/dc1B16QBHTIXpvnTBjpI2PXpK6ne44GXoPeVI6QBnBQLT+jWVybHG734ru4ab9JsWCJwtbXBGIHBrVYI7NUakPV55ZU9E02jnqiLfEgicWVHF5++sxYi02WWn1YdSswYNmkOp3ml9NkesF83+m67SApmUddUD7WrRKiWlFbXaPXDVXyY/TVC5J971o+99a9CADMg4rFuHtBF3K/rHxuBGTN8edp7psHEUwqGc0R6zvVF5DOJ4ZDCsebEbpcIxGfSosvNaAhQMyMe0eQMKlZ1rKjxt0KxVpsSarnNJmzETcj6sP0CVr/zgO19riLTpKFJatOiR88SoP/0tWH+JqqwFn7Qql0Hguef6l0v0TSyPeeOyn1+iIj54440hh/0aCzDnZ8OV01vLLKrVvdwsVlCvW+3GBGjTAfLmw+IUJ22Hto0IUK9X2qw7Oo2DcZ3umJXWWyKzdQfokjGiy0nDsiBr2EldRmR0fWmARhm9jhlSwJh588ZQMOSYXplV+0UJxGdRWnVkR1jwU6QhT8hhIpTyfvGHhYhvdfwQFs2guGA6XItsmVOr+n3OhCdyEc2kqeX/Ta6F06HM0kt800/82CLpg9cSEj06rpQiwcNvEQkf34tI9AKyhESvUEtI9BJYRMLX2CISvYgvIdFPCf9N/APu8294oVkmtwAAAABJRU5ErkJggg==",
10
+ solid: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAABAlBMVEUAAAB8Tv98Tf97TP98TP99Tv+AUP9/Uv+AUv97S/9/U/98Tv98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf99Tv98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf9+T/98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf98Tf99T/98Tv98Tf97S/+QaP96Sv+1m/97TP9+UP/WyP/Wx/+IXf+KYP96S/+LYv/u6f/u6P/Pvv+xlf+EWP+kg//8+//7+v/d0f9/Uv95Sf/Drf/////59//RwP+ffP/h1//39f/39P+jg/+iTbl8AAAANXRSTlMAAAAAAAAAAAAAAAREBC2b8Rl74QtczAI/sfknk+0Vc9wIVMX+ATip9iGK6Q25E9LRIuoBAmJSODcAAAHNSURBVEjH1ZXZVsIwEIbpICqKuAuILIKK4i5MCWUpoG0jSygV3/9VPMWCLdLScOPxv8nJ6XzNTJZ/AoH/IQEmEvzGgwDbkcgOCODv90FYi+7u7e1G1yC4fJEQAOwfHCIiHh7sA0DIM3wdAI6OT9DSyfERAKx7Jh+LJ05xptNEPAYht1LMbJJnKXQodZZ02S8zPJ3J4i9lM+kFpZg7f57LX+ACXeRz5/OrwAZcXhWuf4LESkX8mV0Xri5h014KhG+Kt/a/kmqV2Oe3xZuwA7i7t75I0mSo1Rs1+xzx/s4BPDxa8U25JSFK7c5r2xxbctMiHh+cwJMFyG+KSpBolGoEiaq8yRbwtBjAlvLe1Xr9AWODfk/rvist9AYktcuG+shgzBjpQ9ZVpSUAEm3I2AdljI4ZG37O9soVwJ7OZtJ7uAyQSH/0MY0fj/rEMyWR1NrawKBTgBoDrV0joitQqTY6lDlEO41qxQOo/wbqHgB3StxFr7Kt3wc39n9w3FfDvHyfHJfP//XmfUC8T1QIPxdflpmA3WeELSh520wJthzGxGtk/FY5XYXDjAPfdl+et/uyu92v0FD4W9YkL3tT9NdHAWDHbLvgL3yFxv7H+gL4b+/cCTQELQAAAABJRU5ErkJggg==",
11
+ favicon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAzFBMVEX////9/P/c0P/o4P+vk/+DV//y7v/Aqf+LYv96S/97TP/59//Qv/+Wcf98Tf/+/f/f0/+kg/9/Uf/r5P+0mf+FWf/08f/Er/+OZf97S//7+f/UxP+adf99Tv/i2P+oif+AU/+QaP/u6P+4nv+HXP98TP96Sv+1m//49v+gfv9+UP/WyP/Wx//18v+TbP+IXf+KYP/u6f+Ubf/Pvv+xlf+EWP/8+//7+v/d0f9/Uv95Sf/Drf/RwP+ffP/h1//39f/39P+jg////v+0mP8Z39bnAAABg0lEQVRIx92VaXOCMBCG15sEXY033mdUvFABTzza//+fOhYtZVQkfGv3SyY778O8JJtdgH8boXA4JKKPRGOxaMS3PJ6QCKVESsR9yeVkCikiIsVUUn4rT2dYluAtSJZl0t76XL7wI/9GCvmch7xYKisUXUGVcqn4Ql6p1uoKPoRSr1Urz/SNZstxo7TbDktazcaDvNPtkb7zWT4YcGfXJ71ux60fjrhtntqLOp6ov/eUj4YuYDqz7dC5RigilRYd6boSbW4TZDZ1A0vbMtVWusGRM9NkHLmhrzQbUJbPAST6esO2uz3Afrdlm7V+O4iXADU2IB+sI8DROsiwMegbADmTAU4mgHkGkC/3s3oN4PbgpA9bfAdQvrNO9+zZ2nFPSwpXJbY/mvesedwzSeXKS6A9mCxMVx7MxWTQ9gDGj8DYAxC2JPzTQY7Vvriz/4sTLo1r8V0Eis9/eYs+IOEnKtwEAD4E20yARibeKq+R+xRqxgHavfhAAfGRBeJDMcDY/VvxBc2dXnlb2TJpAAAAAElFTkSuQmCC",
12
+ };
13
+ //# sourceMappingURL=png-data.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"png-data.js","sourceRoot":"","sources":["../../src/assets/png-data.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,MAAM,CAAC,MAAM,aAAa,GAA8C;IACtE,OAAO,EAAE,ghDAAghD;IACzhD,OAAO,EAAE,wxCAAwxC;IACjyC,UAAU,EAAE,4pCAA4pC;IACxqC,KAAK,EAAE,4oCAA4oC;IACnpC,OAAO,EAAE,w4BAAw4B;CACl5B,CAAC"}
@@ -9,6 +9,12 @@ import type { LoginOptions, Platform } from "../types.js";
9
9
  * @throws {@link SebaConfigError} when no browser `window`/`document` exists.
10
10
  */
11
11
  export declare function createBrowserPlatform(): Platform;
12
+ /**
13
+ * Derive a namespaced platform for multi-account support: each account gets its own
14
+ * IndexedDB database and sessionStorage transaction key so parallel `Seba` instances
15
+ * never overwrite each other's session.
16
+ */
17
+ export declare function namespaceBrowserPlatform(platform: Platform, account: string): Platform;
12
18
  /** Re-exported for {@link Seba} type inference in browser-only builds. */
13
19
  export type { LoginOptions };
14
20
  //# sourceMappingURL=platform.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../src/browser/platform.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAA2B,YAAY,EAAE,QAAQ,EAAsC,MAAM,aAAa,CAAC;AAUvH;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,QAAQ,CAqDhD;AAED,0EAA0E;AAC1E,YAAY,EAAE,YAAY,EAAE,CAAC"}
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../src/browser/platform.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAA2B,YAAY,EAAE,QAAQ,EAAsC,MAAM,aAAa,CAAC;AAUvH;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,QAAQ,CAqDhD;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,QAAQ,CAWtF;AAED,0EAA0E;AAC1E,YAAY,EAAE,YAAY,EAAE,CAAC"}
@@ -57,4 +57,21 @@ export function createBrowserPlatform() {
57
57
  },
58
58
  };
59
59
  }
60
+ /**
61
+ * Derive a namespaced platform for multi-account support: each account gets its own
62
+ * IndexedDB database and sessionStorage transaction key so parallel `Seba` instances
63
+ * never overwrite each other's session.
64
+ */
65
+ export function namespaceBrowserPlatform(platform, account) {
66
+ return {
67
+ ...platform,
68
+ pendingAuth: createSessionStoragePendingAuthStore(account),
69
+ createStorage(kind) {
70
+ if (kind === "memory") {
71
+ return memorySessionStorage();
72
+ }
73
+ return createIndexedDbStorage(account);
74
+ },
75
+ };
76
+ }
60
77
  //# sourceMappingURL=platform.js.map