solid-logic 6.0.0-2 → 6.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/_virtual/_rolldown/runtime.cjs.js +1 -1
- package/dist/acl/accessControlSubjects.cjs.js +2 -0
- package/dist/acl/accessControlSubjects.cjs.js.map +1 -0
- package/dist/acl/accessControlSubjects.d.ts +8 -0
- package/dist/acl/accessControlSubjects.d.ts.map +1 -0
- package/dist/acl/accessControlSubjects.esm.js +75 -0
- package/dist/acl/accessControlSubjects.esm.js.map +1 -0
- package/dist/acl/aclLogic.cjs.js +1 -1
- package/dist/acl/aclLogic.cjs.js.map +1 -1
- package/dist/acl/aclLogic.d.ts +7 -0
- package/dist/acl/aclLogic.d.ts.map +1 -0
- package/dist/acl/aclLogic.esm.js +111 -27
- package/dist/acl/aclLogic.esm.js.map +1 -1
- package/dist/authSession/authSession.cjs.js.map +1 -1
- package/dist/authSession/authSession.d.ts +42 -0
- package/dist/authSession/authSession.d.ts.map +1 -0
- package/dist/authSession/authSession.esm.js.map +1 -1
- package/dist/authSession/events.cjs.js.map +1 -1
- package/dist/authSession/events.d.ts +25 -0
- package/dist/authSession/events.d.ts.map +1 -0
- package/dist/authSession/events.esm.js.map +1 -1
- package/dist/authSession/flagAuthorizationOnTransitions.cjs.js +1 -1
- package/dist/authSession/flagAuthorizationOnTransitions.cjs.js.map +1 -1
- package/dist/authSession/flagAuthorizationOnTransitions.d.ts +76 -0
- package/dist/authSession/flagAuthorizationOnTransitions.d.ts.map +1 -0
- package/dist/authSession/flagAuthorizationOnTransitions.esm.js +1 -1
- package/dist/authSession/flagAuthorizationOnTransitions.esm.js.map +1 -1
- package/dist/authSession/identityState.cjs.js +1 -1
- package/dist/authSession/identityState.cjs.js.map +1 -1
- package/dist/authSession/identityState.d.ts +199 -0
- package/dist/authSession/identityState.d.ts.map +1 -0
- package/dist/authSession/identityState.esm.js +1 -1
- package/dist/authSession/identityState.esm.js.map +1 -1
- package/dist/authSession/issuer.d.ts +8 -0
- package/dist/authSession/issuer.d.ts.map +1 -0
- package/dist/authSession/session.cjs.js +1 -1
- package/dist/authSession/session.cjs.js.map +1 -1
- package/dist/authSession/session.d.ts +45 -0
- package/dist/authSession/session.d.ts.map +1 -0
- package/dist/authSession/session.esm.js +1 -1
- package/dist/authSession/session.esm.js.map +1 -1
- package/dist/authn/SolidAuthnLogic.cjs.js.map +1 -1
- package/dist/authn/SolidAuthnLogic.d.ts +61 -0
- package/dist/authn/SolidAuthnLogic.d.ts.map +1 -0
- package/dist/authn/SolidAuthnLogic.esm.js.map +1 -1
- package/dist/authn/authUtil.cjs.js.map +1 -1
- package/dist/authn/authUtil.d.ts +17 -0
- package/dist/authn/authUtil.d.ts.map +1 -0
- package/dist/authn/serverLogout.d.ts +6 -0
- package/dist/authn/serverLogout.d.ts.map +1 -0
- package/dist/chat/chatLogic.cjs.js +1 -1
- package/dist/chat/chatLogic.cjs.js.map +1 -1
- package/dist/chat/chatLogic.d.ts +3 -0
- package/dist/chat/chatLogic.d.ts.map +1 -0
- package/dist/chat/chatLogic.esm.js +5 -5
- package/dist/chat/chatLogic.esm.js.map +1 -1
- package/dist/directory/catalogDirectory.cjs.js +2 -0
- package/dist/directory/catalogDirectory.cjs.js.map +1 -0
- package/dist/directory/catalogDirectory.d.ts +5 -0
- package/dist/directory/catalogDirectory.d.ts.map +1 -0
- package/dist/directory/catalogDirectory.esm.js +54 -0
- package/dist/directory/catalogDirectory.esm.js.map +1 -0
- package/dist/directory/contactsDirectory.cjs.js +2 -0
- package/dist/directory/contactsDirectory.cjs.js.map +1 -0
- package/dist/directory/contactsDirectory.d.ts +7 -0
- package/dist/directory/contactsDirectory.d.ts.map +1 -0
- package/dist/directory/contactsDirectory.esm.js +91 -0
- package/dist/directory/contactsDirectory.esm.js.map +1 -0
- package/dist/directory/directoryHelpers.cjs.js +2 -0
- package/dist/directory/directoryHelpers.cjs.js.map +1 -0
- package/dist/directory/directoryHelpers.d.ts +18 -0
- package/dist/directory/directoryHelpers.d.ts.map +1 -0
- package/dist/directory/directoryHelpers.esm.js +89 -0
- package/dist/directory/directoryHelpers.esm.js.map +1 -0
- package/dist/directory/directoryLogic.cjs.js +2 -0
- package/dist/directory/directoryLogic.cjs.js.map +1 -0
- package/dist/directory/directoryLogic.d.ts +6 -0
- package/dist/directory/directoryLogic.d.ts.map +1 -0
- package/dist/directory/directoryLogic.esm.js +41 -0
- package/dist/directory/directoryLogic.esm.js.map +1 -0
- package/dist/directory/friendsDirectory.cjs.js +2 -0
- package/dist/directory/friendsDirectory.cjs.js.map +1 -0
- package/dist/directory/friendsDirectory.d.ts +6 -0
- package/dist/directory/friendsDirectory.d.ts.map +1 -0
- package/dist/directory/friendsDirectory.esm.js +58 -0
- package/dist/directory/friendsDirectory.esm.js.map +1 -0
- package/dist/inbox/inboxLogic.d.ts +3 -0
- package/dist/inbox/inboxLogic.d.ts.map +1 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +10 -9
- package/dist/index.esm.js.map +1 -1
- package/dist/issuer/issuerLogic.d.ts +8 -0
- package/dist/issuer/issuerLogic.d.ts.map +1 -0
- package/dist/logic/CustomError.cjs.js.map +1 -1
- package/dist/logic/CustomError.d.ts +21 -0
- package/dist/logic/CustomError.d.ts.map +1 -0
- package/dist/logic/CustomError.esm.js.map +1 -1
- package/dist/logic/solidLogic.cjs.js +1 -1
- package/dist/logic/solidLogic.cjs.js.map +1 -1
- package/dist/logic/solidLogic.d.ts +6 -0
- package/dist/logic/solidLogic.d.ts.map +1 -0
- package/dist/logic/solidLogic.esm.js +29 -27
- package/dist/logic/solidLogic.esm.js.map +1 -1
- package/dist/logic/solidLogicSingleton.cjs.js +1 -1
- package/dist/logic/solidLogicSingleton.cjs.js.map +1 -1
- package/dist/logic/solidLogicSingleton.d.ts +4 -0
- package/dist/logic/solidLogicSingleton.d.ts.map +1 -0
- package/dist/logic/solidLogicSingleton.esm.js +2 -1
- package/dist/logic/solidLogicSingleton.esm.js.map +1 -1
- package/dist/profile/profileAclDocuments.d.ts +2 -0
- package/dist/profile/profileAclDocuments.d.ts.map +1 -0
- package/dist/profile/profileDocuments.d.ts +2 -0
- package/dist/profile/profileDocuments.d.ts.map +1 -0
- package/dist/profile/profileLogic.cjs.js.map +1 -1
- package/dist/profile/profileLogic.d.ts +3 -0
- package/dist/profile/profileLogic.d.ts.map +1 -0
- package/dist/profile/profileLogic.esm.js +2 -1
- package/dist/profile/profileLogic.esm.js.map +1 -1
- package/dist/resource/resourceHttp.d.ts +5 -0
- package/dist/resource/resourceHttp.d.ts.map +1 -0
- package/dist/resource/resourceLogic.cjs.js +1 -1
- package/dist/resource/resourceLogic.cjs.js.map +1 -1
- package/dist/resource/resourceLogic.d.ts +4 -0
- package/dist/resource/resourceLogic.d.ts.map +1 -0
- package/dist/resource/resourceLogic.esm.js +36 -26
- package/dist/resource/resourceLogic.esm.js.map +1 -1
- package/dist/resource/resourceMetadata.d.ts +5 -0
- package/dist/resource/resourceMetadata.d.ts.map +1 -0
- package/dist/typeIndex/typeIndexAclDocuments.d.ts +2 -0
- package/dist/typeIndex/typeIndexAclDocuments.d.ts.map +1 -0
- package/dist/typeIndex/typeIndexDocuments.d.ts +3 -0
- package/dist/typeIndex/typeIndexDocuments.d.ts.map +1 -0
- package/dist/typeIndex/typeIndexLogic.cjs.js.map +1 -1
- package/dist/typeIndex/typeIndexLogic.d.ts +3 -0
- package/dist/typeIndex/typeIndexLogic.d.ts.map +1 -0
- package/dist/typeIndex/typeIndexLogic.esm.js +10 -5
- package/dist/typeIndex/typeIndexLogic.esm.js.map +1 -1
- package/dist/types.d.ts +187 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/util/containerLogic.cjs.js.map +1 -1
- package/dist/util/containerLogic.d.ts +12 -0
- package/dist/util/containerLogic.d.ts.map +1 -0
- package/dist/util/containerLogic.esm.js.map +1 -1
- package/dist/util/debug.d.ts +5 -0
- package/dist/util/debug.d.ts.map +1 -0
- package/dist/util/ns.cjs.js.map +1 -1
- package/dist/util/ns.d.ts +2 -0
- package/dist/util/ns.d.ts.map +1 -0
- package/dist/util/utilityLogic.cjs.js +1 -1
- package/dist/util/utilityLogic.cjs.js.map +1 -1
- package/dist/util/utilityLogic.d.ts +16 -0
- package/dist/util/utilityLogic.d.ts.map +1 -0
- package/dist/util/utilityLogic.esm.js +6 -2
- package/dist/util/utilityLogic.esm.js.map +1 -1
- package/dist/util/utils.cjs.js +1 -1
- package/dist/util/utils.cjs.js.map +1 -1
- package/dist/util/utils.d.ts +8 -0
- package/dist/util/utils.d.ts.map +1 -0
- package/dist/util/utils.esm.js +5 -3
- package/dist/util/utils.esm.js.map +1 -1
- package/package.json +6 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identityState.esm.js","names":[],"sources":["../../src/authSession/identityState.ts"],"sourcesContent":["/**\n * One identity state per session.\n *\n * The session's identity is assembled from two sources — the OIDC session\n * itself and (on NSS localhost setups) a cookie-backed fallback — and is\n * observed by consumers that must invalidate what the previous identity\n * fetched. Doing that with independent flags and predicates per call site let\n * each new async path (a slow restore, a probe answering late, a refocus)\n * reintroduce the same class of bug, so the identity lives here, once:\n *\n * TEMPORARY, pending the uvdsl change: two mechanisms here exist only because\n * `@uvdsl/solid-oidc-client-browser` announces `sessionStateChange` when\n * `isActive` changes and nothing else — it does not report a WebID change that\n * keeps the session active, and it does not push a login/logout made in\n * another tab. Until it does (issue drafted), the cookie fallback is the only\n * way to see an NSS cookie identity change at all (see\n * `reportCookieIdentity`) and `watchTokenUpdates()` is the only way to see an\n * A -> B switch. Both are isolated here so they become a small deletion, not\n * a hunt, when uvdsl reports identity changes itself.\n *\n * - `sessionIsActive()` is the single activity rule (`isActive` is\n * authoritative, an explicit `false` wins over a cached WebID);\n * - every observation (session event, token update, refocus resync, cookie\n * probe result) is applied to ONE record, so a change is detected and\n * reported once, whichever path noticed it;\n * - the record carries a `version` (bumped when a transition is applied) and\n * the newest refresh attempt, so an answer that belongs to an identity the\n * session has since left — a restore started under Alice answering after\n * Bob logged in — is dropped instead of being applied;\n * - the transition is derived from the previous/next snapshots\n * (`classifyTransition`, `identityReplaced`), not from flags: there is\n * nothing to keep in sync, and an unchanged identity costs no event.\n *\n * Reported events (same vocabulary as before):\n * 'logout' — the session went from active to inactive;\n * 'sessionChange' — any other change of active state or WebID;\n * 'identityReplaced' — an identity that WAS actively established is gone or\n * replaced, so data fetched under it must be dropped.\n * Start-up (none -> A) and a token refresh for the same identity are not\n * replacements.\n *\n * A session whose backing store no longer holds it (a cross-tab logout) is\n * marked `cleared` here: the local session object keeps reporting the previous\n * identity, so the mark is what makes the derived reads report logged out\n * until the session reports an identity again.\n *\n * DO NOT keep identity state anywhere else: `SolidAuthnLogic` reads it,\n * `authSession` publishes it (`info`, events) and `solidLogicSingleton`\n * decides from it whose credentials a request would carry.\n */\n\nexport type SessionLike = {\n isActive?: boolean\n webId?: string\n info?: { isLoggedIn?: boolean, webId?: string }\n addEventListener?: (type: string, listener: () => void) => void\n setTokenDetails?: (...args: unknown[]) => unknown\n restore?: () => Promise<unknown>\n}\n\nexport type IdentityEvent = 'logout' | 'sessionChange' | 'identityReplaced'\n\n/** The session's own view: what the raw session reports, cleared-aware. */\nexport type IdentitySnapshot = { isActive: boolean, webId?: string }\n\nexport type DocumentLike = {\n visibilityState?: string\n addEventListener?: (type: string, listener: () => void) => void\n removeEventListener?: (type: string, listener: () => void) => void\n}\n\n/**\n * Which legacy event a session transition should emit:\n * 'logout' — the session went from active to inactive;\n * 'sessionChange' — any other change of active state or WebID (a login here\n * is also announced as 'login' by SolidAuthnLogic; the\n * duplicate is harmless — consumers only invalidate);\n * null — nothing changed, so a refocused tab with the same\n * identity costs no event and no invalidation.\n */\nexport function classifyTransition (\n prev: IdentitySnapshot,\n next: IdentitySnapshot\n): 'logout' | 'sessionChange' | null {\n if (prev.isActive !== next.isActive) return next.isActive ? 'sessionChange' : 'logout'\n return next.webId !== prev.webId ? 'sessionChange' : null\n}\n\n/**\n * Whether an established identity was replaced or cleared — a session that had\n * a WebID no longer reports the same one (A -> B), or no longer reports being\n * active (A -> logged out, A -> none). Data fetched under the previous\n * identity cannot be re-validated document by document, so this is the signal\n * to discard it.\n *\n * Only the transition OUT of an ACTIVELY established identity counts: once the\n * session has gone inactive (webId possibly retained), the replacement was\n * already reported — clearing the WebID afterwards or logging in as someone\n * else is not a second replacement.\n */\nexport function identityReplaced (prev: IdentitySnapshot, next: IdentitySnapshot): boolean {\n if (prev.webId === undefined) return false\n if (!prev.isActive) return false\n return next.webId !== prev.webId || !next.isActive\n}\n\n/**\n * Whether the session counts as active. `isActive` is authoritative — an\n * explicit `false` wins even when a WebID is still cached (a logout that has\n * not cleared it yet); the WebID only fills in an undefined state.\n */\nexport const sessionIsActive = (session: SessionLike): boolean =>\n session.isActive === true || (session.isActive === undefined && Boolean(session.webId))\n\n/**\n * How long a refocus resync may delay the snapshot comparison.\n */\nconst RESYNC_TIMEOUT_MS = 2000\n\ntype Subscriber = {\n onEvent?: (event: IdentityEvent) => void\n /** The whole transition, once — for consumers that only invalidate. */\n onTransition?: (events: IdentityEvent[]) => void\n onRefocus?: () => void | Promise<void>\n resync?: () => unknown\n}\n\ntype Attempt = {\n id: number\n /** The record version the attempt started from. */\n version: number\n /** The raw identity the attempt started from. */\n raw: { isActive: boolean, webId?: string }\n promise: Promise<unknown>\n}\n\ntype Record = {\n session: SessionLike\n /** The last applied view of the session, cleared-aware. */\n raw: IdentitySnapshot\n /** The backing store no longer holds the session (a cross-tab logout). */\n cleared: boolean\n /** The cookie-probed identity, when the session does not own one. */\n cookieWebId: string | null\n /** Bumped whenever a session transition is applied. */\n version: number\n /** The newest resync attempt, or undefined when none is running. */\n attempt?: Attempt\n attemptId: number\n subscribers: Set<Subscriber>\n /** Whether the session state listener is attached (once per session). */\n listenerAttached: boolean\n /** Detaches the document listener when the last subscriber leaves. */\n detachDocument?: () => void\n /** Wrapped once per session, see watchTokenUpdates(). */\n watchingTokenUpdates: boolean\n}\n\nconst records = new WeakMap<object, Record>()\n\n// One restore at a time per session: `restore()` can mutate the session before\n// it resolves, so two overlapping restores could write an older identity back\n// over a newer one. Every caller (the refocus resync and `checkUser()`) goes\n// through this lock, and one that arrives while a restore is in flight joins\n// it instead of starting another.\nconst restoresInFlight = new WeakMap<object, Promise<unknown>>()\n\nconst recordOf = (session: SessionLike): Record | undefined => {\n if (typeof session !== 'object' || session === null) return undefined\n let record = records.get(session as object)\n if (!record) {\n record = {\n session,\n raw: { isActive: false },\n cleared: false,\n cookieWebId: null,\n version: 0,\n attemptId: 0,\n subscribers: new Set(),\n listenerAttached: false,\n watchingTokenUpdates: false\n }\n record.raw = snapshotOf(record)\n records.set(session as object, record)\n }\n return record\n}\n\n/**\n * Whether a session was reported cleared: its backing store lost the session\n * while this tab still had one, so its identity no longer holds here.\n */\nexport function sessionWasCleared (session: unknown): boolean {\n if (typeof session !== 'object' || session === null) return false\n return records.get(session as object)?.cleared === true\n}\n\n/**\n * Whether the session explicitly reports itself inactive. An explicit `false`\n * — `isActive` on the session or `isLoggedIn` on the legacy `info` shape —\n * wins over a retained WebID: a partial logout that has not cleared the cached\n * WebID must not keep identifying the previous user. Consumers that would\n * otherwise act on the WebID alone (authenticated fetch, currentUser) use this\n * to stand down.\n */\nexport function sessionExplicitlyInactive (session: SessionLike): boolean {\n if (sessionWasCleared(session)) return true\n return session?.isActive === false || session?.info?.isLoggedIn === false\n}\n\n/**\n * Whether the OIDC session currently owns the identity. A session that was\n * reported cleared does not, and neither does one that explicitly reports\n * itself logged out (a legacy shape that still carries a WebID would otherwise\n * pass `sessionIsActive()` alone).\n */\nexport function sessionOwnsIdentity (session: SessionLike): boolean {\n return !sessionExplicitlyInactive(session) && sessionIsActive(session)\n}\n\n/** The WebID the raw session publishes, or undefined when it owns none. */\nexport function sessionIdentityWebId (session: SessionLike): string | undefined {\n return sessionOwnsIdentity(session) ? session.webId : undefined\n}\n\nconst snapshotOf = (record: Record): IdentitySnapshot => {\n // A cleared session answers as logged out until it reports an identity\n // again: the local session object may still carry the previous WebID.\n if (record.cleared) return { isActive: false }\n return { isActive: sessionIsActive(record.session), webId: record.session.webId }\n}\n\n/**\n * The identity a caller should act as: the session's own when it owns one, the\n * cookie-probed one when the session is inactive (or cleared) and a probe\n * established it, and undefined when neither does. This is the last word for\n * `currentUser()`, `info` consumers and the fetch bridge.\n */\nexport function effectiveIdentity (session: SessionLike): { webId?: string, source: 'none' | 'session' | 'cookie' } {\n const record = recordOf(session)\n if (!record) return { source: 'none' }\n if (sessionOwnsIdentity(session)) {\n return session.webId === undefined ? { source: 'none' } : { webId: session.webId, source: 'session' }\n }\n if (record.cookieWebId !== null) return { webId: record.cookieWebId, source: 'cookie' }\n // A session that only reports a logout keeps no identity — the retained\n // WebID must not be used (see sessionExplicitlyInactive).\n return { source: 'none' }\n}\n\n/**\n * The legacy `info` shape is session-derived and stays derived: callers\n * snapshot and restore it, so a retained value must never answer for the\n * session. `isLoggedIn` follows `sessionIsActive` — an explicit `isActive:\n * false` reports logged out even when a WebID is still cached, or the fetch\n * bridge would keep routing anonymous requests through the authenticated\n * fetch.\n */\nexport function legacySessionInfo (session: SessionLike): { webId?: string, isLoggedIn?: boolean } {\n if (sessionWasCleared(session)) return { webId: undefined, isLoggedIn: false }\n return { webId: session.webId, isLoggedIn: sessionIsActive(session) }\n}\n\n/**\n * Runs `session.restore()`, sharing an attempt that is already in flight.\n *\n * @returns the shared promise, or undefined when the session has no restore.\n */\nexport function restoreSession (session: SessionLike | undefined): Promise<unknown> | undefined {\n const restore = session?.restore\n if (typeof restore !== 'function' || typeof session !== 'object' || session === null) {\n return undefined\n }\n const key = session as object\n const inFlight = restoresInFlight.get(key)\n if (inFlight) return inFlight\n const started = Promise.resolve()\n .then(() => restore.call(session))\n .finally(() => { restoresInFlight.delete(key) })\n restoresInFlight.set(key, started)\n return started\n}\n\n/**\n * Reports one applied transition. `onEvent` receives each event separately\n * (the legacy vocabulary); `onTransition` receives the whole transition once,\n * so a consumer that only invalidates does not do its work twice when a\n * transition carries two events (a logout that also replaces the identity).\n *\n * Every subscriber's `onTransition` runs BEFORE any subscriber's `onEvent`:\n * a consumer that hears the event can rely on the transition having been\n * applied everywhere else (the store is already invalidated when the legacy\n * listeners run), instead of depending on the order subscriptions happened to\n * be created in.\n */\nconst deliver = (record: Record, events: IdentityEvent[]): void => {\n record.subscribers.forEach(subscriber => subscriber.onTransition?.(events))\n record.subscribers.forEach(subscriber => {\n events.forEach(event => subscriber.onEvent?.(event))\n })\n}\n\n/**\n * Re-reads the session and reports what changed since the last observation.\n * The cleared mark is dropped first when the session reports an identity\n * again, otherwise it would mask the new identity forever.\n */\nfunction note (record: Record): void {\n if (record.cleared && sessionIsActive(record.session)) {\n record.cleared = false\n }\n // The session owns the identity again: a cookie identity remembered from\n // before it activated is stale — the probe only runs while the session is\n // inactive, and after a session logout the remembered cookie value must not\n // answer as if it had just been probed. Silent on purpose: the session\n // transition is what is reported, not this book-keeping.\n if (sessionOwnsIdentity(record.session)) record.cookieWebId = null\n const next = snapshotOf(record)\n const event = classifyTransition(record.raw, next)\n const replaced = identityReplaced(record.raw, next)\n const moved = event !== null || replaced\n record.raw = next\n if (!moved) return\n // The session moved on: an attempt that started before this transition is\n // answering about a state that no longer holds.\n record.version += 1\n const events: IdentityEvent[] = []\n if (event) events.push(event)\n if (replaced) events.push('identityReplaced')\n deliver(record, events)\n}\n\n/**\n * The backing store has no session while this tab still believes it is signed\n * in: report the logout and the replacement for the identity that was active,\n * once — a later refocus that still finds no session must not repeat it.\n */\nfunction reportCleared (record: Record): void {\n if (record.cleared) return\n const wasActive = record.raw.isActive\n const wasEstablished = record.raw.webId !== undefined\n // Only a session that was actually in use has to be invalidated: an\n // anonymous tab is cleared already, and marking it would make snapshotOf()\n // report the cleared state — masking a later login until some other\n // observation happens to run.\n if (!wasActive || !wasEstablished) {\n record.raw = snapshotOf(record)\n if (wasActive) {\n record.version += 1\n deliver(record, ['logout'])\n }\n return\n }\n // The local session object still reports the old identity: the mark is what\n // makes the derived reads stop answering for it, and the cleared snapshot is\n // the baseline — a later activation is a new login, and a still-cleared\n // session cannot report the logout twice.\n record.cleared = true\n record.raw = snapshotOf(record)\n record.version += 1\n deliver(record, ['logout', 'identityReplaced'])\n}\n\n/**\n * Starts (or joins) the newest resync attempt. A caller that joins an attempt\n * in flight must judge its answer against the identity the ATTEMPT started\n * from, not the one it sees now — hence the baseline is stored with the\n * attempt.\n */\nfunction startAttempt (record: Record, action: () => unknown): Attempt {\n if (record.attempt) return record.attempt\n const attempt: Attempt = {\n id: ++record.attemptId,\n version: record.version,\n raw: { isActive: sessionIsActive(record.session), webId: record.session.webId },\n promise: Promise.resolve()\n .then(action)\n .finally(() => { if (record.attempt === attempt) record.attempt = undefined })\n }\n record.attempt = attempt\n return attempt\n}\n\n/**\n * Re-reads the session through `resync` (when given) and reports the\n * transition, bounded by RESYNC_TIMEOUT_MS so a hung restore cannot stall a\n * refocus — but keeping the outcome: a restore that only finishes later can\n * still report the session gone, and dropping it would leave this tab on the\n * old identity until some other event.\n */\nasync function resyncThenNote (record: Record): Promise<void> {\n const resync = resyncActionOf(record)\n if (typeof resync !== 'function') {\n note(record)\n return\n }\n const runResync = resync\n const started = startAttempt(record, () => runResync())\n // This attempt's outcome only applies while it is still the newest one and no\n // transition was applied since the attempt (not this caller) started.\n const stale = (): boolean => started.id !== record.attemptId || record.version !== started.version\n // A `'cleared'` answer only applies while the session still reports the\n // identity the attempt started from: `restore()` rejects with \"no session\"\n // for the identity it was started for, so if the session reports a different\n // identity now, the answer is about the previous one and must not log the new\n // one out.\n const sameRawIdentity = (): boolean =>\n sessionIsActive(record.session) === started.raw.isActive && record.session.webId === started.raw.webId\n let outcome: unknown\n let done = false\n const attempt = started.promise.then(\n (value) => { outcome = value; done = true },\n () => { done = true } // compared as it stands\n )\n const apply = (): void => {\n if (stale()) return\n if (outcome === 'cleared') {\n if (sameRawIdentity()) reportCleared(record)\n return\n }\n // Any other result may have updated the session (a cross-tab login):\n // comparing again is what turns that into `sessionChange` /\n // `identityReplaced` instead of leaving this tab on the old identity.\n note(record)\n }\n let timer: ReturnType<typeof setTimeout> | undefined\n const timeout = new Promise<void>((resolve) => { timer = setTimeout(resolve, RESYNC_TIMEOUT_MS) })\n try {\n await Promise.race([attempt, timeout])\n } finally {\n if (timer !== undefined) clearTimeout(timer)\n }\n if (done) {\n apply()\n return\n }\n note(record)\n void attempt.then(apply)\n}\n\n/**\n * The resync action lives with the subscriber that knows how to run it\n * (`authSession` maps a restore rejection to `'cleared'`); the module keeps\n * the newest one so a refocus started from any subscriber re-reads the\n * session.\n */\nfunction resyncActionOf (record: Record): (() => unknown) | undefined {\n // Subscribers iterate in insertion order, so the last match is the newest.\n let newest: (() => unknown) | undefined\n for (const subscriber of record.subscribers) {\n if (subscriber.resync) newest = subscriber.resync\n }\n return newest\n}\n\n// uvdsl's session announces only changes of `isActive`; a WebID can change\n// while both states stay active and would go unseen. Every token update goes\n// through `setTokenDetails`, so compare the identity around it. Wrapped once\n// per session.\n//\n// TEMPORARY, pending the uvdsl change: delete this wrapper when the library\n// dispatches `sessionStateChange` for an active -> active WebID change too\n// (see the module header). The state model does not depend on it: any\n// observation path reports the same transition.\nfunction watchTokenUpdates (record: Record): void {\n const session = record.session\n const original = session.setTokenDetails\n if (typeof original !== 'function' || record.watchingTokenUpdates) return\n record.watchingTokenUpdates = true\n session.setTokenDetails = (...args: unknown[]): unknown => {\n const before = snapshotOf(record)\n const changed = (): boolean => {\n const after = snapshotOf(record)\n return after.webId !== before.webId || after.isActive !== before.isActive\n }\n // Whatever the outcome — a token update can apply the new identity and\n // then fail (a failed persistence, for one) — the identity around the call\n // is what matters, so the change is reported and the failure passes on.\n let result: unknown\n try {\n result = original.apply(session, args)\n } catch (error) {\n if (changed()) note(record)\n throw error\n }\n if (result && typeof (result as Promise<unknown>).then === 'function') {\n return (result as Promise<unknown>).then(\n (value: unknown) => {\n if (changed()) note(record)\n return value\n },\n (error: unknown) => {\n if (changed()) note(record)\n throw error\n }\n )\n }\n if (changed()) note(record)\n return result\n }\n}\n\nexport type IdentitySubscription = {\n /**\n * Applies a cookie-probe result. Ignored once the subscription is released,\n * so a probe that answers after its owner was replaced cannot resurrect an\n * identity — and while the session owns the identity it must not be replaced\n * by a cookie one at all.\n */\n reportCookieIdentity: (webId: string | null) => void\n unsubscribe: () => void\n}\n\n/**\n * Observes the session for identity transitions, and for refocuses (one\n * document listener per session, removed when the last subscription is\n * released, so a replaced logic instance adds no second listener).\n *\n * `onEvent` receives the derived events; `onRefocus` runs on a refocus (the\n * cookie revalidation, in SolidAuthnLogic); `resync` re-reads the session\n * before the comparison (authSession owns it, because it knows that a restore\n * rejection means \"no session\").\n */\nexport function subscribeIdentity (\n session: SessionLike,\n options: Subscriber = {}\n): IdentitySubscription {\n const record = recordOf(session)\n if (!record) {\n return { reportCookieIdentity: () => undefined, unsubscribe: () => undefined }\n }\n const subscriber: Subscriber = { ...options }\n record.subscribers.add(subscriber)\n if (!record.listenerAttached) {\n record.listenerAttached = true\n if (typeof session.addEventListener === 'function') {\n session.addEventListener('sessionStateChange', () => note(record))\n }\n watchTokenUpdates(record)\n }\n const doc: DocumentLike | undefined = typeof document === 'undefined' ? undefined : document\n if (record.subscribers.size === 1 && doc && typeof doc.addEventListener === 'function') {\n const handler = (): void => {\n if (doc.visibilityState !== 'visible') return\n // Wake up the session first; the cookie identity is invisible to it and\n // is revalidated in parallel by the subscriber that owns the probe.\n void resyncThenNote(record)\n record.subscribers.forEach(sub => { void sub.onRefocus?.() })\n }\n doc.addEventListener('visibilitychange', handler)\n record.detachDocument = () => doc.removeEventListener?.('visibilitychange', handler)\n }\n let released = false\n return {\n reportCookieIdentity: (webId: string | null): void => {\n if (released) return\n applyCookieIdentity(record, webId)\n },\n unsubscribe: (): void => {\n if (released) return\n released = true\n record.subscribers.delete(subscriber)\n if (record.subscribers.size === 0) {\n record.detachDocument?.()\n record.detachDocument = undefined\n }\n }\n }\n}\n\n/**\n * Applies a cookie-probe result to the record. Only cookie-backed changes are\n * reported: an OIDC identity change is already emitted by `note()`, and\n * reporting it again would duplicate the events — a reload consumer would\n * reload twice. While the session owns the identity a cookie one must not\n * replace it, and `sessionChange` is emitted only when it does not (an active\n * session already reported its own transition).\n */\nfunction applyCookieIdentity (record: Record, webId: string | null): void {\n if (record.cookieWebId === webId) return\n // While the session owns the identity a cookie one must not replace it — a\n // probe only runs while the session is inactive for exactly that reason.\n if (sessionOwnsIdentity(record.session)) return\n const previousCookieBacked = record.cookieWebId !== null\n record.cookieWebId = webId\n // The replacement is owed whenever the identity being REPLACED was\n // cookie-backed — the session watcher could not see it. Adopting one is not\n // a replacement: there was nothing of a previous user to drop.\n const events: IdentityEvent[] = ['sessionChange']\n if (previousCookieBacked) events.push('identityReplaced')\n deliver(record, events)\n}\n\n/**\n * Reload the page when the identity that was active in this tab is replaced or\n * cleared — the pragmatic way to drop everything fetched under the previous\n * identity (store, panes, editability), instead of repairing every read path.\n *\n * Consumer-side on purpose: navigation is an application decision (solid-ui,\n * mashlib), and tests inject their own action.\n */\nexport function reloadOnIdentityReplaced (\n events: { on?: (event: 'identityReplaced', handler: () => void) => void } | undefined,\n reload: () => void = () => {\n if (typeof window !== 'undefined') window.location.reload()\n }\n): void {\n if (!events || typeof events.on !== 'function') return\n events.on('identityReplaced', reload)\n}\n\n/**\n * Re-reads the session and reports the transition, without a resync action\n * (a plain observation, used by tests and by callers that already know the\n * session was refreshed).\n */\nexport function observeSession (session: SessionLike): void {\n const record = recordOf(session)\n if (record) note(record)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"identityState.esm.js","names":[],"sources":["../../src/authSession/identityState.ts"],"sourcesContent":["/**\n * One identity state per session.\n *\n * The session's identity is assembled from two sources — the OIDC session\n * itself and (on NSS localhost setups) a cookie-backed fallback — and is\n * observed by consumers that must invalidate what the previous identity\n * fetched. Doing that with independent flags and predicates per call site let\n * each new async path (a slow restore, a probe answering late, a refocus)\n * reintroduce the same class of bug, so the identity lives here, once:\n *\n * TEMPORARY, pending the uvdsl change: two mechanisms here exist only because\n * `@uvdsl/solid-oidc-client-browser` announces `sessionStateChange` when\n * `isActive` changes and nothing else — it does not report a WebID change that\n * keeps the session active, and it does not push a login/logout made in\n * another tab. Until it does (issue drafted), the cookie fallback is the only\n * way to see an NSS cookie identity change at all (see\n * `reportCookieIdentity`) and `watchTokenUpdates()` is the only way to see an\n * A -> B switch. Both are isolated here so they become a small deletion, not\n * a hunt, when uvdsl reports identity changes itself.\n *\n * - `sessionIsActive()` is the single activity rule (`isActive` is\n * authoritative, an explicit `false` wins over a cached WebID);\n * - every observation (session event, token update, refocus resync, cookie\n * probe result) is applied to ONE record, so a change is detected and\n * reported once, whichever path noticed it;\n * - the record carries a `version` (bumped when a transition is applied) and\n * the newest refresh attempt, so an answer that belongs to an identity the\n * session has since left — a restore started under Alice answering after\n * Bob logged in — is dropped instead of being applied;\n * - the transition is derived from the previous/next snapshots\n * (`classifyTransition`, `identityReplaced`), not from flags: there is\n * nothing to keep in sync, and an unchanged identity costs no event.\n *\n * Reported events (same vocabulary as before):\n * 'logout' — the session went from active to inactive;\n * 'sessionChange' — any other change of active state or WebID;\n * 'identityReplaced' — an identity that WAS actively established is gone or\n * replaced, so data fetched under it must be dropped.\n * Start-up (none -> A) and a token refresh for the same identity are not\n * replacements.\n *\n * A session whose backing store no longer holds it (a cross-tab logout) is\n * marked `cleared` here: the local session object keeps reporting the previous\n * identity, so the mark is what makes the derived reads report logged out\n * until the session reports an identity again.\n *\n * DO NOT keep identity state anywhere else: `SolidAuthnLogic` reads it,\n * `authSession` publishes it (`info`, events) and `solidLogicSingleton`\n * decides from it whose credentials a request would carry.\n */\n\nexport type SessionLike = {\n isActive?: boolean\n webId?: string\n info?: { isLoggedIn?: boolean, webId?: string }\n addEventListener?: (type: string, listener: () => void) => void\n setTokenDetails?: (...args: unknown[]) => unknown\n restore?: () => Promise<unknown>\n}\n\nexport type IdentityEvent = 'logout' | 'sessionChange' | 'identityReplaced'\n\n/** The session's own view: what the raw session reports, cleared-aware. */\nexport type IdentitySnapshot = { isActive: boolean, webId?: string }\n\nexport type DocumentLike = {\n visibilityState?: string\n addEventListener?: (type: string, listener: () => void) => void\n removeEventListener?: (type: string, listener: () => void) => void\n}\n\n/**\n * Which legacy event a session transition should emit:\n * 'logout' — the session went from active to inactive;\n * 'sessionChange' — any other change of active state or WebID (a login here\n * is also announced as 'login' by SolidAuthnLogic; the\n * duplicate is harmless — consumers only invalidate);\n * null — nothing changed, so a refocused tab with the same\n * identity costs no event and no invalidation.\n */\nexport function classifyTransition (\n prev: IdentitySnapshot,\n next: IdentitySnapshot\n): 'logout' | 'sessionChange' | null {\n if (prev.isActive !== next.isActive) return next.isActive ? 'sessionChange' : 'logout'\n return next.webId !== prev.webId ? 'sessionChange' : null\n}\n\n/**\n * Whether an established identity was replaced or cleared — a session that had\n * a WebID no longer reports the same one (A -> B), or no longer reports being\n * active (A -> logged out, A -> none). Data fetched under the previous\n * identity cannot be re-validated document by document, so this is the signal\n * to discard it.\n *\n * Only the transition OUT of an ACTIVELY established identity counts: once the\n * session has gone inactive (webId possibly retained), the replacement was\n * already reported — clearing the WebID afterwards or logging in as someone\n * else is not a second replacement.\n */\nexport function identityReplaced (prev: IdentitySnapshot, next: IdentitySnapshot): boolean {\n if (prev.webId === undefined) return false\n if (!prev.isActive) return false\n return next.webId !== prev.webId || !next.isActive\n}\n\n/**\n * Whether the session counts as active. `isActive` is authoritative — an\n * explicit `false` wins even when a WebID is still cached (a logout that has\n * not cleared it yet); the WebID only fills in an undefined state.\n */\nexport const sessionIsActive = (session: SessionLike): boolean =>\n session.isActive === true || (session.isActive === undefined && Boolean(session.webId))\n\n/**\n * How long a refocus resync may delay the snapshot comparison.\n */\nconst RESYNC_TIMEOUT_MS = 2000\n\ntype Subscriber = {\n onEvent?: (event: IdentityEvent) => void\n /** The whole transition, once — for consumers that only invalidate. */\n onTransition?: (events: IdentityEvent[]) => void\n onRefocus?: () => void | Promise<void>\n resync?: () => unknown\n}\n\ntype Attempt = {\n id: number\n /** The record version the attempt started from. */\n version: number\n /** The raw identity the attempt started from. */\n raw: { isActive: boolean, webId?: string }\n promise: Promise<unknown>\n}\n\ntype Record = {\n session: SessionLike\n /** The last applied view of the session, cleared-aware. */\n raw: IdentitySnapshot\n /** The backing store no longer holds the session (a cross-tab logout). */\n cleared: boolean\n /** The cookie-probed identity, when the session does not own one. */\n cookieWebId: string | null\n /** Bumped whenever a session transition is applied. */\n version: number\n /** The newest resync attempt, or undefined when none is running. */\n attempt?: Attempt\n attemptId: number\n subscribers: Set<Subscriber>\n /** Whether the session state listener is attached (once per session). */\n listenerAttached: boolean\n /** Detaches the document listener when the last subscriber leaves. */\n detachDocument?: () => void\n /** Wrapped once per session, see watchTokenUpdates(). */\n watchingTokenUpdates: boolean\n}\n\nconst records = new WeakMap<object, Record>()\n\n// One restore at a time per session: `restore()` can mutate the session before\n// it resolves, so two overlapping restores could write an older identity back\n// over a newer one. Every caller (the refocus resync and `checkUser()`) goes\n// through this lock, and one that arrives while a restore is in flight joins\n// it instead of starting another.\nconst restoresInFlight = new WeakMap<object, Promise<unknown>>()\n\nconst recordOf = (session: SessionLike): Record | undefined => {\n if (typeof session !== 'object' || session === null) return undefined\n let record = records.get(session as object)\n if (!record) {\n record = {\n session,\n raw: { isActive: false },\n cleared: false,\n cookieWebId: null,\n version: 0,\n attemptId: 0,\n subscribers: new Set(),\n listenerAttached: false,\n watchingTokenUpdates: false\n }\n record.raw = snapshotOf(record)\n records.set(session as object, record)\n }\n return record\n}\n\n/**\n * Whether a session was reported cleared: its backing store lost the session\n * while this tab still had one, so its identity no longer holds here.\n */\nexport function sessionWasCleared (session: unknown): boolean {\n if (typeof session !== 'object' || session === null) return false\n return records.get(session as object)?.cleared === true\n}\n\n/**\n * Whether the session explicitly reports itself inactive. An explicit `false`\n * — `isActive` on the session or `isLoggedIn` on the legacy `info` shape —\n * wins over a retained WebID: a partial logout that has not cleared the cached\n * WebID must not keep identifying the previous user. Consumers that would\n * otherwise act on the WebID alone (authenticated fetch, currentUser) use this\n * to stand down.\n */\nexport function sessionExplicitlyInactive (session: SessionLike): boolean {\n if (sessionWasCleared(session)) return true\n return session?.isActive === false || session?.info?.isLoggedIn === false\n}\n\n/**\n * Whether the OIDC session currently owns the identity. A session that was\n * reported cleared does not, and neither does one that explicitly reports\n * itself logged out (a legacy shape that still carries a WebID would otherwise\n * pass `sessionIsActive()` alone).\n */\nexport function sessionOwnsIdentity (session: SessionLike): boolean {\n return !sessionExplicitlyInactive(session) && sessionIsActive(session)\n}\n\n/** The WebID the raw session publishes, or undefined when it owns none. */\nexport function sessionIdentityWebId (session: SessionLike): string | undefined {\n return sessionOwnsIdentity(session) ? session.webId : undefined\n}\n\nconst snapshotOf = (record: Record): IdentitySnapshot => {\n // A cleared session answers as logged out until it reports an identity\n // again: the local session object may still carry the previous WebID.\n if (record.cleared) return { isActive: false }\n return { isActive: sessionIsActive(record.session), webId: record.session.webId }\n}\n\n/**\n * The identity a caller should act as: the session's own when it owns one, the\n * cookie-probed one when the session is inactive (or cleared) and a probe\n * established it, and undefined when neither does. This is the last word for\n * `currentUser()`, `info` consumers and the fetch bridge.\n */\nexport function effectiveIdentity (session: SessionLike): { webId?: string, source: 'none' | 'session' | 'cookie' } {\n const record = recordOf(session)\n if (!record) return { source: 'none' }\n if (sessionOwnsIdentity(session)) {\n return session.webId === undefined ? { source: 'none' } : { webId: session.webId, source: 'session' }\n }\n if (record.cookieWebId !== null) return { webId: record.cookieWebId, source: 'cookie' }\n // A session that only reports a logout keeps no identity — the retained\n // WebID must not be used (see sessionExplicitlyInactive).\n return { source: 'none' }\n}\n\n/**\n * The legacy `info` shape is session-derived and stays derived: callers\n * snapshot and restore it, so a retained value must never answer for the\n * session. `isLoggedIn` follows `sessionIsActive` — an explicit `isActive:\n * false` reports logged out even when a WebID is still cached, or the fetch\n * bridge would keep routing anonymous requests through the authenticated\n * fetch.\n */\nexport function legacySessionInfo (session: SessionLike): { webId?: string, isLoggedIn?: boolean } {\n if (sessionWasCleared(session)) return { webId: undefined, isLoggedIn: false }\n return { webId: session.webId, isLoggedIn: sessionIsActive(session) }\n}\n\n/**\n * Runs `session.restore()`, sharing an attempt that is already in flight.\n *\n * @returns the shared promise, or undefined when the session has no restore.\n */\nexport function restoreSession (session: SessionLike | undefined): Promise<unknown> | undefined {\n const restore = session?.restore\n if (typeof restore !== 'function' || typeof session !== 'object' || session === null) {\n return undefined\n }\n const key = session as object\n const inFlight = restoresInFlight.get(key)\n if (inFlight) return inFlight\n const started = Promise.resolve()\n .then(() => restore.call(session))\n .finally(() => { restoresInFlight.delete(key) })\n restoresInFlight.set(key, started)\n return started\n}\n\n/**\n * Reports one applied transition. `onEvent` receives each event separately\n * (the legacy vocabulary); `onTransition` receives the whole transition once,\n * so a consumer that only invalidates does not do its work twice when a\n * transition carries two events (a logout that also replaces the identity).\n *\n * Every subscriber's `onTransition` runs BEFORE any subscriber's `onEvent`:\n * a consumer that hears the event can rely on the transition having been\n * applied everywhere else (the store is already invalidated when the legacy\n * listeners run), instead of depending on the order subscriptions happened to\n * be created in.\n */\nconst deliver = (record: Record, events: IdentityEvent[]): void => {\n record.subscribers.forEach(subscriber => subscriber.onTransition?.(events))\n record.subscribers.forEach(subscriber => {\n events.forEach(event => subscriber.onEvent?.(event))\n })\n}\n\n/**\n * Re-reads the session and reports what changed since the last observation.\n * The cleared mark is dropped first when the session reports an identity\n * again, otherwise it would mask the new identity forever.\n */\nfunction note (record: Record): void {\n if (record.cleared && sessionIsActive(record.session)) {\n record.cleared = false\n }\n // The session owns the identity again: a cookie identity remembered from\n // before it activated is stale — the probe only runs while the session is\n // inactive, and after a session logout the remembered cookie value must not\n // answer as if it had just been probed. Silent on purpose: the session\n // transition is what is reported, not this book-keeping.\n if (sessionOwnsIdentity(record.session)) record.cookieWebId = null\n const next = snapshotOf(record)\n const event = classifyTransition(record.raw, next)\n const replaced = identityReplaced(record.raw, next)\n const moved = event !== null || replaced\n record.raw = next\n if (!moved) return\n // The session moved on: an attempt that started before this transition is\n // answering about a state that no longer holds.\n record.version += 1\n const events: IdentityEvent[] = []\n if (event) events.push(event)\n if (replaced) events.push('identityReplaced')\n deliver(record, events)\n}\n\n/**\n * The backing store has no session while this tab still believes it is signed\n * in: report the logout and the replacement for the identity that was active,\n * once — a later refocus that still finds no session must not repeat it.\n */\nfunction reportCleared (record: Record): void {\n if (record.cleared) return\n const wasActive = record.raw.isActive\n const wasEstablished = record.raw.webId !== undefined\n // Only a session that was actually in use has to be invalidated: an\n // anonymous tab is cleared already, and marking it would make snapshotOf()\n // report the cleared state — masking a later login until some other\n // observation happens to run.\n if (!wasActive || !wasEstablished) {\n record.raw = snapshotOf(record)\n if (wasActive) {\n record.version += 1\n deliver(record, ['logout'])\n }\n return\n }\n // The local session object still reports the old identity: the mark is what\n // makes the derived reads stop answering for it, and the cleared snapshot is\n // the baseline — a later activation is a new login, and a still-cleared\n // session cannot report the logout twice.\n record.cleared = true\n record.raw = snapshotOf(record)\n record.version += 1\n deliver(record, ['logout', 'identityReplaced'])\n}\n\n/**\n * Starts (or joins) the newest resync attempt. A caller that joins an attempt\n * in flight must judge its answer against the identity the ATTEMPT started\n * from, not the one it sees now — hence the baseline is stored with the\n * attempt.\n */\nfunction startAttempt (record: Record, action: () => unknown): Attempt {\n if (record.attempt) return record.attempt\n const attempt: Attempt = {\n id: ++record.attemptId,\n version: record.version,\n raw: { isActive: sessionIsActive(record.session), webId: record.session.webId },\n promise: Promise.resolve()\n .then(action)\n .finally(() => { if (record.attempt === attempt) record.attempt = undefined })\n }\n record.attempt = attempt\n return attempt\n}\n\n/**\n * Re-reads the session through `resync` (when given) and reports the\n * transition, bounded by RESYNC_TIMEOUT_MS so a hung restore cannot stall a\n * refocus — but keeping the outcome: a restore that only finishes later can\n * still report the session gone, and dropping it would leave this tab on the\n * old identity until some other event.\n */\nasync function resyncThenNote (record: Record): Promise<void> {\n const resync = resyncActionOf(record)\n if (typeof resync !== 'function') {\n note(record)\n return\n }\n const runResync = resync\n const started = startAttempt(record, () => runResync())\n // This attempt's outcome only applies while it is still the newest one and no\n // transition was applied since the attempt (not this caller) started.\n const stale = (): boolean => started.id !== record.attemptId || record.version !== started.version\n // A `'cleared'` answer only applies while the session still reports the\n // identity the attempt started from: `restore()` rejects with \"no session\"\n // for the identity it was started for, so if the session reports a different\n // identity now, the answer is about the previous one and must not log the new\n // one out.\n const sameRawIdentity = (): boolean =>\n sessionIsActive(record.session) === started.raw.isActive && record.session.webId === started.raw.webId\n let outcome: unknown\n let done = false\n const attempt = started.promise.then(\n (value) => { outcome = value; done = true },\n () => { done = true } // compared as it stands\n )\n const apply = (): void => {\n if (stale()) return\n if (outcome === 'cleared') {\n if (sameRawIdentity()) reportCleared(record)\n return\n }\n // Any other result may have updated the session (a cross-tab login):\n // comparing again is what turns that into `sessionChange` /\n // `identityReplaced` instead of leaving this tab on the old identity.\n note(record)\n }\n let timer: ReturnType<typeof setTimeout> | undefined\n const timeout = new Promise<void>((resolve) => { timer = setTimeout(resolve, RESYNC_TIMEOUT_MS) })\n try {\n await Promise.race([attempt, timeout])\n } finally {\n if (timer !== undefined) clearTimeout(timer)\n }\n if (done) {\n apply()\n return\n }\n note(record)\n void attempt.then(apply)\n}\n\n/**\n * The resync action lives with the subscriber that knows how to run it\n * (`authSession` maps a restore rejection to `'cleared'`); the module keeps\n * the newest one so a refocus started from any subscriber re-reads the\n * session.\n */\nfunction resyncActionOf (record: Record): (() => unknown) | undefined {\n // Subscribers iterate in insertion order, so the last match is the newest.\n let newest: (() => unknown) | undefined\n for (const subscriber of record.subscribers) {\n if (subscriber.resync) newest = subscriber.resync\n }\n return newest\n}\n\n// uvdsl's session announces only changes of `isActive`; a WebID can change\n// while both states stay active and would go unseen. Every token update goes\n// through `setTokenDetails`, so compare the identity around it. Wrapped once\n// per session.\n//\n// TEMPORARY, pending the uvdsl change: delete this wrapper when the library\n// dispatches `sessionStateChange` for an active -> active WebID change too\n// (see the module header). The state model does not depend on it: any\n// observation path reports the same transition.\nfunction watchTokenUpdates (record: Record): void {\n const session = record.session\n const original = session.setTokenDetails\n if (typeof original !== 'function' || record.watchingTokenUpdates) return\n record.watchingTokenUpdates = true\n session.setTokenDetails = (...args: unknown[]): unknown => {\n const before = snapshotOf(record)\n const changed = (): boolean => {\n const after = snapshotOf(record)\n return after.webId !== before.webId || after.isActive !== before.isActive\n }\n // Whatever the outcome — a token update can apply the new identity and\n // then fail (a failed persistence, for one) — the identity around the call\n // is what matters, so the change is reported and the failure passes on.\n let result: unknown\n try {\n result = original.apply(session, args)\n } catch (error) {\n if (changed()) note(record)\n throw error\n }\n if (result && typeof (result as Promise<unknown>).then === 'function') {\n return (result as Promise<unknown>).then(\n (value: unknown) => {\n if (changed()) note(record)\n return value\n },\n (error: unknown) => {\n if (changed()) note(record)\n throw error\n }\n )\n }\n if (changed()) note(record)\n return result\n }\n}\n\nexport type IdentitySubscription = {\n /**\n * Applies a cookie-probe result. Ignored once the subscription is released,\n * so a probe that answers after its owner was replaced cannot resurrect an\n * identity — and while the session owns the identity it must not be replaced\n * by a cookie one at all.\n */\n reportCookieIdentity: (webId: string | null) => void\n unsubscribe: () => void\n}\n\n/**\n * Observes the session for identity transitions, and for refocuses (one\n * document listener per session, removed when the last subscription is\n * released, so a replaced logic instance adds no second listener).\n *\n * `onEvent` receives the derived events; `onRefocus` runs on a refocus (the\n * cookie revalidation, in SolidAuthnLogic); `resync` re-reads the session\n * before the comparison (authSession owns it, because it knows that a restore\n * rejection means \"no session\").\n */\nexport function subscribeIdentity (\n session: SessionLike,\n options: Subscriber = {}\n): IdentitySubscription {\n const record = recordOf(session)\n if (!record) {\n return { reportCookieIdentity: () => undefined, unsubscribe: () => undefined }\n }\n const subscriber: Subscriber = { ...options }\n record.subscribers.add(subscriber)\n if (!record.listenerAttached) {\n record.listenerAttached = true\n if (typeof session.addEventListener === 'function') {\n session.addEventListener('sessionStateChange', () => note(record))\n }\n watchTokenUpdates(record)\n }\n const doc: DocumentLike | undefined = typeof document === 'undefined' ? undefined : document\n if (record.subscribers.size === 1 && doc && typeof doc.addEventListener === 'function') {\n const handler = (): void => {\n if (doc.visibilityState !== 'visible') return\n // Wake up the session first; the cookie identity is invisible to it and\n // is revalidated in parallel by the subscriber that owns the probe.\n void resyncThenNote(record)\n record.subscribers.forEach(sub => { void sub.onRefocus?.() })\n }\n doc.addEventListener('visibilitychange', handler)\n record.detachDocument = () => doc.removeEventListener?.('visibilitychange', handler)\n }\n let released = false\n return {\n reportCookieIdentity: (webId: string | null): void => {\n if (released) return\n applyCookieIdentity(record, webId)\n },\n unsubscribe: (): void => {\n if (released) return\n released = true\n record.subscribers.delete(subscriber)\n if (record.subscribers.size === 0) {\n record.detachDocument?.()\n record.detachDocument = undefined\n }\n }\n }\n}\n\n/**\n * Applies a cookie-probe result to the record. Only cookie-backed changes are\n * reported: an OIDC identity change is already emitted by `note()`, and\n * reporting it again would duplicate the events — a reload consumer would\n * reload twice. While the session owns the identity a cookie one must not\n * replace it, and `sessionChange` is emitted only when it does not (an active\n * session already reported its own transition).\n */\nfunction applyCookieIdentity (record: Record, webId: string | null): void {\n if (record.cookieWebId === webId) return\n // While the session owns the identity a cookie one must not replace it — a\n // probe only runs while the session is inactive for exactly that reason.\n if (sessionOwnsIdentity(record.session)) return\n const previousCookieBacked = record.cookieWebId !== null\n record.cookieWebId = webId\n // The replacement is owed whenever the identity being REPLACED was\n // cookie-backed — the session watcher could not see it. Adopting one is not\n // a replacement: there was nothing of a previous user to drop.\n const events: IdentityEvent[] = ['sessionChange']\n if (previousCookieBacked) events.push('identityReplaced')\n deliver(record, events)\n}\n\n/**\n * Reload the page when the identity that was active in this tab is replaced or\n * cleared — the pragmatic way to drop everything fetched under the previous\n * identity (store, panes, editability), instead of repairing every read path.\n *\n * Consumer-side on purpose: navigation is an application decision (solid-ui,\n * mashlib), and tests inject their own action.\n */\nexport function reloadOnIdentityReplaced (\n events: { on?: (event: 'identityReplaced', handler: () => void) => void } | undefined,\n reload: () => void = () => {\n if (typeof window !== 'undefined') window.location.reload()\n }\n): void {\n if (!events || typeof events.on !== 'function') return\n events.on('identityReplaced', reload)\n}\n\n/**\n * Re-reads the session and reports the transition, without a resync action\n * (a plain observation, used by tests and by callers that already know the\n * session was refreshed).\n */\nexport function observeSession (session: SessionLike): void {\n const record = recordOf(session)\n if (record) note(record)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issuer discovery utilities.
|
|
3
|
+
*
|
|
4
|
+
* Resolves OIDC issuer endpoints from /.well-known/openid-configuration
|
|
5
|
+
* so that login can use the canonical issuer host.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveIssuerForLogin(issuer: string): Promise<string>;
|
|
8
|
+
//# sourceMappingURL=issuer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issuer.d.ts","sourceRoot":"","sources":["../../src/authSession/issuer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAsBH,wBAAsB,qBAAqB,CAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ5E"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../_virtual/_rolldown/runtime.cjs.js");let t=require("@uvdsl/solid-oidc-client-browser"),n=require("@uvdsl/solid-oidc-client-browser/core");n=e.__toESM(n);var r=class{constructor(){this.map=new Map}shouldPreserveExistingRefreshToken(e,t){return e===`refresh_token`&&(t==null||t===``)&&this.map.has(e)}async init(){return this}async setItem(e,t){this.shouldPreserveExistingRefreshToken(e,t)||this.map.set(e,t)}async getItem(e){return this.map.has(e)?this.map.get(e):null}async deleteItem(e){this.map.delete(e)}async clear(){this.map.clear()}close(){}},i=class{constructor(){this.db=null,this.dbName=`soidc`,this.storeName=`session`,this.dbVersion=1}async shouldPreserveExistingRefreshToken(e,t){if(e!==`refresh_token
|
|
1
|
+
const e=require("../_virtual/_rolldown/runtime.cjs.js");let t=require("@uvdsl/solid-oidc-client-browser"),n=require("@uvdsl/solid-oidc-client-browser/core");n=e.__toESM(n);var r=class{constructor(){this.map=new Map}shouldPreserveExistingRefreshToken(e,t){return e===`refresh_token`&&(t==null||t===``)&&this.map.has(e)}async init(){return this}async setItem(e,t){this.shouldPreserveExistingRefreshToken(e,t)||this.map.set(e,t)}async getItem(e){return this.map.has(e)?this.map.get(e):null}async deleteItem(e){this.map.delete(e)}async clear(){this.map.clear()}close(){}},i=class{constructor(){this.db=null,this.dbName=`soidc`,this.storeName=`session`,this.dbVersion=1}async shouldPreserveExistingRefreshToken(e,t){if(e!==`refresh_token`||t!=null&&t!==``)return!1;let n=await this.getItem(e);return n!=null&&n!==``}async init(){if(this.db)return this;if(typeof indexedDB>`u`)throw Error(`IndexedDB is not available in this environment`);return await new Promise((e,t)=>{let n=indexedDB.open(this.dbName,this.dbVersion);n.onerror=()=>t(n.error),n.onsuccess=()=>{this.db=n.result,e()},n.onupgradeneeded=()=>{let e=n.result;e.objectStoreNames.contains(this.storeName)||e.createObjectStore(this.storeName)}}),this}async setItem(e,t){await this.init(),!await this.shouldPreserveExistingRefreshToken(e,t)&&await this.withStore(`readwrite`,n=>n.put(t,e))}async getItem(e){return await this.init(),this.withStore(`readonly`,t=>t.get(e))}async deleteItem(e){await this.init(),await this.withStore(`readwrite`,t=>t.delete(e))}async clear(){await this.init(),await this.withStore(`readwrite`,e=>e.clear())}close(){this.db&&=(this.db.close(),null)}withStore(e,t){return new Promise((n,r)=>{if(!this.db){r(Error(`Session database not initialized`));return}let i=this.db.transaction(this.storeName,e),a=t(i.objectStore(this.storeName)),o=null;i.onerror=()=>r(i.error??a.error),i.onabort=()=>r(i.error??a.error??Error(`IndexedDB transaction aborted`)),i.oncomplete=()=>n(o),a.onerror=()=>r(a.error),a.onsuccess=()=>{o=a.result??null}})}};function a(){let e=n,t=e.SessionCore??e.default?.SessionCore??e.default;return typeof t==`function`?t:null}var o=a();function s(){if(typeof window<`u`&&(()=>{let e=window.location.hostname;return e===`localhost`||e===`127.0.0.1`||e.endsWith(`.localhost`)})())return o?new o(void 0,{database:new i}):new t.Session;try{return new t.Session}catch(e){console.warn(`solid-logic: falling back to non-worker auth session:`,e);try{return o?new o(void 0,{database:new i}):new t.Session}catch(e){return console.warn(`solid-logic: IndexedDB unavailable, using in-memory session database:`,e),o?new o(void 0,{database:new r}):new t.Session}}}var c=s();exports.IndexedDbSessionDatabase=i,exports.MemorySessionDatabase=r,exports._session=c;
|
|
2
2
|
//# sourceMappingURL=session.cjs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.cjs.js","names":[],"sources":["../../src/authSession/session.ts"],"sourcesContent":["/**\n * OIDC session factory.\n *\n * Everything needed to create and configure the underlying auth session:\n * - Session database backends (in-memory and IndexedDB)\n * - Session instantiation (WebSession or SessionCore, with local-dev\n * fallbacks for environments where the service worker can't load)\n * - Login compatibility shim that normalises legacy call-site signatures\n * and resolves the canonical issuer through /.well-known discovery\n *\n * The raw session instance (_session) is consumed by authSession.ts where\n * the legacy event layer and logout listener are attached.\n *\n * DO NOT import _session directly — always go through authSession.ts so\n * the event wiring is guaranteed to run.\n */\n\nimport {\n Session as WebSession,\n} from '@uvdsl/solid-oidc-client-browser'\nimport * as OidcCore from '@uvdsl/solid-oidc-client-browser/core'\nimport type { Session as OidcSession, SessionDatabase } from '@uvdsl/solid-oidc-client-browser/core'\n\n// ---------------------------------------------------------------------------\n// Session databases\n// ---------------------------------------------------------------------------\n\nexport class MemorySessionDatabase implements SessionDatabase {\n private readonly map = new Map<string, any>()\n\n private shouldPreserveExistingRefreshToken(id: string, value: any): boolean {\n return id === 'refresh_token' && (value == null || value === '') && this.map.has(id)\n }\n\n async init (): Promise<SessionDatabase> {\n return this\n }\n\n async setItem (id: string, value: any): Promise<void> {\n // Some Solid IdPs do not include refresh_token on refresh responses.\n // Keep the previous token instead of overwriting it with null/undefined.\n if (this.shouldPreserveExistingRefreshToken(id, value)) {\n return\n }\n this.map.set(id, value)\n }\n\n async getItem (id: string): Promise<any> {\n return this.map.has(id) ? this.map.get(id) : null\n }\n\n async deleteItem (id: string): Promise<void> {\n this.map.delete(id)\n }\n\n async clear (): Promise<void> {\n this.map.clear()\n }\n\n close (): void {\n // No-op for in-memory database\n }\n}\n\nexport class IndexedDbSessionDatabase implements SessionDatabase {\n private db: IDBDatabase | null = null\n private readonly dbName = 'soidc'\n private readonly storeName = 'session'\n private readonly dbVersion = 1\n\n private async shouldPreserveExistingRefreshToken(id: string, value: any): Promise<boolean> {\n if (id !== 'refresh_token' || !(value == null || value === '')) {\n return false\n }\n const existing = await this.getItem(id)\n return existing != null && existing !== ''\n }\n\n async init (): Promise<SessionDatabase> {\n if (this.db) return this\n if (typeof indexedDB === 'undefined') {\n throw new Error('IndexedDB is not available in this environment')\n }\n\n await new Promise<void>((resolve, reject) => {\n const request = indexedDB.open(this.dbName, this.dbVersion)\n\n request.onerror = () => reject(request.error)\n request.onsuccess = () => {\n this.db = request.result\n resolve()\n }\n request.onupgradeneeded = () => {\n const db = request.result\n if (!db.objectStoreNames.contains(this.storeName)) {\n db.createObjectStore(this.storeName)\n }\n }\n })\n\n return this\n }\n\n async setItem (id: string, value: any): Promise<void> {\n await this.init()\n // Some Solid IdPs do not include refresh_token on refresh responses.\n // Keep the previous token instead of overwriting it with null/undefined.\n if (await this.shouldPreserveExistingRefreshToken(id, value)) {\n return\n }\n await this.withStore('readwrite', store => store.put(value, id))\n }\n\n async getItem (id: string): Promise<any> {\n await this.init()\n return this.withStore('readonly', store => store.get(id))\n }\n\n async deleteItem (id: string): Promise<void> {\n await this.init()\n await this.withStore('readwrite', store => store.delete(id))\n }\n\n async clear (): Promise<void> {\n await this.init()\n await this.withStore('readwrite', store => store.clear())\n }\n\n close (): void {\n if (this.db) {\n this.db.close()\n this.db = null\n }\n }\n\n private withStore(mode: IDBTransactionMode, op: (store: IDBObjectStore) => IDBRequest<any>): Promise<any> {\n return new Promise<any>((resolve, reject) => {\n if (!this.db) {\n reject(new Error('Session database not initialized'))\n return\n }\n\n const tx = this.db.transaction(this.storeName, mode)\n const store = tx.objectStore(this.storeName)\n const request = op(store)\n let result: any = null\n\n tx.onerror = () => reject(tx.error ?? request.error)\n tx.onabort = () => reject(tx.error ?? request.error ?? new Error('IndexedDB transaction aborted'))\n tx.oncomplete = () => resolve(result)\n\n request.onerror = () => reject(request.error)\n request.onsuccess = () => {\n result = request.result ?? null\n }\n })\n }\n}\n\n// ---------------------------------------------------------------------------\n// Session instantiation\n// ---------------------------------------------------------------------------\n\nfunction getSessionCoreCtor (): (new (...args: any[]) => OidcSession) | null {\n const coreAny = OidcCore as any\n const candidate = coreAny.SessionCore ?? coreAny.default?.SessionCore ?? coreAny.default\n\n if (typeof candidate !== 'function') {\n return null\n }\n\n return candidate as new (...args: any[]) => OidcSession\n}\n\nconst SessionCoreCtor = getSessionCoreCtor()\n\nfunction createSession (): OidcSession {\n const shouldSkipWorkerInLocalDev = typeof window !== 'undefined' && (() => {\n const host = window.location.hostname\n // In local NSS setups (including subdomain mode like alice.localhost),\n // worker-based session storage can be brittle and lose state on reload.\n // Prefer SessionCore + IndexedDB for deterministic persistence.\n return host === 'localhost' || host === '127.0.0.1' || host.endsWith('.localhost')\n })()\n\n if (shouldSkipWorkerInLocalDev) {\n if (SessionCoreCtor) {\n return new SessionCoreCtor(undefined, { database: new IndexedDbSessionDatabase() })\n }\n return new WebSession()\n }\n\n try {\n return new WebSession()\n } catch (error) {\n // In some deployments, worker URL resolution can become file:// and fail cross-origin.\n // Fall back to SessionCore so auth still works without background refresh worker.\n // Use IndexedDB to keep refresh-token persistence across page reloads.\n console.warn('solid-logic: falling back to non-worker auth session:', error)\n try {\n if (SessionCoreCtor) {\n return new SessionCoreCtor(undefined, { database: new IndexedDbSessionDatabase() })\n }\n return new WebSession()\n } catch (dbError) {\n console.warn('solid-logic: IndexedDB unavailable, using in-memory session database:', dbError)\n if (SessionCoreCtor) {\n return new SessionCoreCtor(undefined, { database: new MemorySessionDatabase() })\n }\n return new WebSession()\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Singleton session\n// ---------------------------------------------------------------------------\n\nconst _session = createSession()\n\nexport { _session }\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"session.cjs.js","names":[],"sources":["../../src/authSession/session.ts"],"sourcesContent":["/**\n * OIDC session factory.\n *\n * Everything needed to create and configure the underlying auth session:\n * - Session database backends (in-memory and IndexedDB)\n * - Session instantiation (WebSession or SessionCore, with local-dev\n * fallbacks for environments where the service worker can't load)\n * - Login compatibility shim that normalises legacy call-site signatures\n * and resolves the canonical issuer through /.well-known discovery\n *\n * The raw session instance (_session) is consumed by authSession.ts where\n * the legacy event layer and logout listener are attached.\n *\n * DO NOT import _session directly — always go through authSession.ts so\n * the event wiring is guaranteed to run.\n */\n\nimport {\n Session as WebSession,\n} from '@uvdsl/solid-oidc-client-browser'\nimport * as OidcCore from '@uvdsl/solid-oidc-client-browser/core'\nimport type { Session as OidcSession, SessionDatabase } from '@uvdsl/solid-oidc-client-browser/core'\n\n// ---------------------------------------------------------------------------\n// Session databases\n// ---------------------------------------------------------------------------\n\nexport class MemorySessionDatabase implements SessionDatabase {\n private readonly map = new Map<string, any>()\n\n private shouldPreserveExistingRefreshToken(id: string, value: any): boolean {\n return id === 'refresh_token' && (value == null || value === '') && this.map.has(id)\n }\n\n async init (): Promise<SessionDatabase> {\n return this\n }\n\n async setItem (id: string, value: any): Promise<void> {\n // Some Solid IdPs do not include refresh_token on refresh responses.\n // Keep the previous token instead of overwriting it with null/undefined.\n if (this.shouldPreserveExistingRefreshToken(id, value)) {\n return\n }\n this.map.set(id, value)\n }\n\n async getItem (id: string): Promise<any> {\n return this.map.has(id) ? this.map.get(id) : null\n }\n\n async deleteItem (id: string): Promise<void> {\n this.map.delete(id)\n }\n\n async clear (): Promise<void> {\n this.map.clear()\n }\n\n close (): void {\n // No-op for in-memory database\n }\n}\n\nexport class IndexedDbSessionDatabase implements SessionDatabase {\n private db: IDBDatabase | null = null\n private readonly dbName = 'soidc'\n private readonly storeName = 'session'\n private readonly dbVersion = 1\n\n private async shouldPreserveExistingRefreshToken(id: string, value: any): Promise<boolean> {\n if (id !== 'refresh_token' || !(value == null || value === '')) {\n return false\n }\n const existing = await this.getItem(id)\n return existing != null && existing !== ''\n }\n\n async init (): Promise<SessionDatabase> {\n if (this.db) return this\n if (typeof indexedDB === 'undefined') {\n throw new Error('IndexedDB is not available in this environment')\n }\n\n await new Promise<void>((resolve, reject) => {\n const request = indexedDB.open(this.dbName, this.dbVersion)\n\n request.onerror = () => reject(request.error)\n request.onsuccess = () => {\n this.db = request.result\n resolve()\n }\n request.onupgradeneeded = () => {\n const db = request.result\n if (!db.objectStoreNames.contains(this.storeName)) {\n db.createObjectStore(this.storeName)\n }\n }\n })\n\n return this\n }\n\n async setItem (id: string, value: any): Promise<void> {\n await this.init()\n // Some Solid IdPs do not include refresh_token on refresh responses.\n // Keep the previous token instead of overwriting it with null/undefined.\n if (await this.shouldPreserveExistingRefreshToken(id, value)) {\n return\n }\n await this.withStore('readwrite', store => store.put(value, id))\n }\n\n async getItem (id: string): Promise<any> {\n await this.init()\n return this.withStore('readonly', store => store.get(id))\n }\n\n async deleteItem (id: string): Promise<void> {\n await this.init()\n await this.withStore('readwrite', store => store.delete(id))\n }\n\n async clear (): Promise<void> {\n await this.init()\n await this.withStore('readwrite', store => store.clear())\n }\n\n close (): void {\n if (this.db) {\n this.db.close()\n this.db = null\n }\n }\n\n private withStore(mode: IDBTransactionMode, op: (store: IDBObjectStore) => IDBRequest<any>): Promise<any> {\n return new Promise<any>((resolve, reject) => {\n if (!this.db) {\n reject(new Error('Session database not initialized'))\n return\n }\n\n const tx = this.db.transaction(this.storeName, mode)\n const store = tx.objectStore(this.storeName)\n const request = op(store)\n let result: any = null\n\n tx.onerror = () => reject(tx.error ?? request.error)\n tx.onabort = () => reject(tx.error ?? request.error ?? new Error('IndexedDB transaction aborted'))\n tx.oncomplete = () => resolve(result)\n\n request.onerror = () => reject(request.error)\n request.onsuccess = () => {\n result = request.result ?? null\n }\n })\n }\n}\n\n// ---------------------------------------------------------------------------\n// Session instantiation\n// ---------------------------------------------------------------------------\n\nfunction getSessionCoreCtor (): (new (...args: any[]) => OidcSession) | null {\n const coreAny = OidcCore as any\n const candidate = coreAny.SessionCore ?? coreAny.default?.SessionCore ?? coreAny.default\n\n if (typeof candidate !== 'function') {\n return null\n }\n\n return candidate as new (...args: any[]) => OidcSession\n}\n\nconst SessionCoreCtor = getSessionCoreCtor()\n\nfunction createSession (): OidcSession {\n const shouldSkipWorkerInLocalDev = typeof window !== 'undefined' && (() => {\n const host = window.location.hostname\n // In local NSS setups (including subdomain mode like alice.localhost),\n // worker-based session storage can be brittle and lose state on reload.\n // Prefer SessionCore + IndexedDB for deterministic persistence.\n return host === 'localhost' || host === '127.0.0.1' || host.endsWith('.localhost')\n })()\n\n if (shouldSkipWorkerInLocalDev) {\n if (SessionCoreCtor) {\n return new SessionCoreCtor(undefined, { database: new IndexedDbSessionDatabase() })\n }\n return new WebSession()\n }\n\n try {\n return new WebSession()\n } catch (error) {\n // In some deployments, worker URL resolution can become file:// and fail cross-origin.\n // Fall back to SessionCore so auth still works without background refresh worker.\n // Use IndexedDB to keep refresh-token persistence across page reloads.\n console.warn('solid-logic: falling back to non-worker auth session:', error)\n try {\n if (SessionCoreCtor) {\n return new SessionCoreCtor(undefined, { database: new IndexedDbSessionDatabase() })\n }\n return new WebSession()\n } catch (dbError) {\n console.warn('solid-logic: IndexedDB unavailable, using in-memory session database:', dbError)\n if (SessionCoreCtor) {\n return new SessionCoreCtor(undefined, { database: new MemorySessionDatabase() })\n }\n return new WebSession()\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Singleton session\n// ---------------------------------------------------------------------------\n\nconst _session = createSession()\n\nexport { _session }\n"],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OIDC session factory.
|
|
3
|
+
*
|
|
4
|
+
* Everything needed to create and configure the underlying auth session:
|
|
5
|
+
* - Session database backends (in-memory and IndexedDB)
|
|
6
|
+
* - Session instantiation (WebSession or SessionCore, with local-dev
|
|
7
|
+
* fallbacks for environments where the service worker can't load)
|
|
8
|
+
* - Login compatibility shim that normalises legacy call-site signatures
|
|
9
|
+
* and resolves the canonical issuer through /.well-known discovery
|
|
10
|
+
*
|
|
11
|
+
* The raw session instance (_session) is consumed by authSession.ts where
|
|
12
|
+
* the legacy event layer and logout listener are attached.
|
|
13
|
+
*
|
|
14
|
+
* DO NOT import _session directly — always go through authSession.ts so
|
|
15
|
+
* the event wiring is guaranteed to run.
|
|
16
|
+
*/
|
|
17
|
+
import * as OidcCore from '@uvdsl/solid-oidc-client-browser/core';
|
|
18
|
+
import type { SessionDatabase } from '@uvdsl/solid-oidc-client-browser/core';
|
|
19
|
+
export declare class MemorySessionDatabase implements SessionDatabase {
|
|
20
|
+
private readonly map;
|
|
21
|
+
private shouldPreserveExistingRefreshToken;
|
|
22
|
+
init(): Promise<SessionDatabase>;
|
|
23
|
+
setItem(id: string, value: any): Promise<void>;
|
|
24
|
+
getItem(id: string): Promise<any>;
|
|
25
|
+
deleteItem(id: string): Promise<void>;
|
|
26
|
+
clear(): Promise<void>;
|
|
27
|
+
close(): void;
|
|
28
|
+
}
|
|
29
|
+
export declare class IndexedDbSessionDatabase implements SessionDatabase {
|
|
30
|
+
private db;
|
|
31
|
+
private readonly dbName;
|
|
32
|
+
private readonly storeName;
|
|
33
|
+
private readonly dbVersion;
|
|
34
|
+
private shouldPreserveExistingRefreshToken;
|
|
35
|
+
init(): Promise<SessionDatabase>;
|
|
36
|
+
setItem(id: string, value: any): Promise<void>;
|
|
37
|
+
getItem(id: string): Promise<any>;
|
|
38
|
+
deleteItem(id: string): Promise<void>;
|
|
39
|
+
clear(): Promise<void>;
|
|
40
|
+
close(): void;
|
|
41
|
+
private withStore;
|
|
42
|
+
}
|
|
43
|
+
declare const _session: OidcCore.Session;
|
|
44
|
+
export { _session };
|
|
45
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/authSession/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,QAAQ,MAAM,uCAAuC,CAAA;AACjE,OAAO,KAAK,EAA0B,eAAe,EAAE,MAAM,uCAAuC,CAAA;AAMpG,qBAAa,qBAAsB,YAAW,eAAe;IAC3D,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAyB;IAE7C,OAAO,CAAC,kCAAkC;IAIpC,IAAI,IAAK,OAAO,CAAC,eAAe,CAAC;IAIjC,OAAO,CAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAS/C,OAAO,CAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAIlC,UAAU,CAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAItC,KAAK,IAAK,OAAO,CAAC,IAAI,CAAC;IAI7B,KAAK,IAAK,IAAI;CAGf;AAED,qBAAa,wBAAyB,YAAW,eAAe;IAC9D,OAAO,CAAC,EAAE,CAA2B;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAU;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAI;YAEhB,kCAAkC;IAQ1C,IAAI,IAAK,OAAO,CAAC,eAAe,CAAC;IAyBjC,OAAO,CAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAU/C,OAAO,CAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAKlC,UAAU,CAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKtC,KAAK,IAAK,OAAO,CAAC,IAAI,CAAC;IAK7B,KAAK,IAAK,IAAI;IAOd,OAAO,CAAC,SAAS;CAsBlB;AA6DD,QAAA,MAAM,QAAQ,kBAAkB,CAAA;AAEhC,OAAO,EAAE,QAAQ,EAAE,CAAA"}
|
|
@@ -29,7 +29,7 @@ var n = class {
|
|
|
29
29
|
this.db = null, this.dbName = "soidc", this.storeName = "session", this.dbVersion = 1;
|
|
30
30
|
}
|
|
31
31
|
async shouldPreserveExistingRefreshToken(e, t) {
|
|
32
|
-
if (e !== "refresh_token" ||
|
|
32
|
+
if (e !== "refresh_token" || t != null && t !== "") return !1;
|
|
33
33
|
let n = await this.getItem(e);
|
|
34
34
|
return n != null && n !== "";
|
|
35
35
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.esm.js","names":[],"sources":["../../src/authSession/session.ts"],"sourcesContent":["/**\n * OIDC session factory.\n *\n * Everything needed to create and configure the underlying auth session:\n * - Session database backends (in-memory and IndexedDB)\n * - Session instantiation (WebSession or SessionCore, with local-dev\n * fallbacks for environments where the service worker can't load)\n * - Login compatibility shim that normalises legacy call-site signatures\n * and resolves the canonical issuer through /.well-known discovery\n *\n * The raw session instance (_session) is consumed by authSession.ts where\n * the legacy event layer and logout listener are attached.\n *\n * DO NOT import _session directly — always go through authSession.ts so\n * the event wiring is guaranteed to run.\n */\n\nimport {\n Session as WebSession,\n} from '@uvdsl/solid-oidc-client-browser'\nimport * as OidcCore from '@uvdsl/solid-oidc-client-browser/core'\nimport type { Session as OidcSession, SessionDatabase } from '@uvdsl/solid-oidc-client-browser/core'\n\n// ---------------------------------------------------------------------------\n// Session databases\n// ---------------------------------------------------------------------------\n\nexport class MemorySessionDatabase implements SessionDatabase {\n private readonly map = new Map<string, any>()\n\n private shouldPreserveExistingRefreshToken(id: string, value: any): boolean {\n return id === 'refresh_token' && (value == null || value === '') && this.map.has(id)\n }\n\n async init (): Promise<SessionDatabase> {\n return this\n }\n\n async setItem (id: string, value: any): Promise<void> {\n // Some Solid IdPs do not include refresh_token on refresh responses.\n // Keep the previous token instead of overwriting it with null/undefined.\n if (this.shouldPreserveExistingRefreshToken(id, value)) {\n return\n }\n this.map.set(id, value)\n }\n\n async getItem (id: string): Promise<any> {\n return this.map.has(id) ? this.map.get(id) : null\n }\n\n async deleteItem (id: string): Promise<void> {\n this.map.delete(id)\n }\n\n async clear (): Promise<void> {\n this.map.clear()\n }\n\n close (): void {\n // No-op for in-memory database\n }\n}\n\nexport class IndexedDbSessionDatabase implements SessionDatabase {\n private db: IDBDatabase | null = null\n private readonly dbName = 'soidc'\n private readonly storeName = 'session'\n private readonly dbVersion = 1\n\n private async shouldPreserveExistingRefreshToken(id: string, value: any): Promise<boolean> {\n if (id !== 'refresh_token' || !(value == null || value === '')) {\n return false\n }\n const existing = await this.getItem(id)\n return existing != null && existing !== ''\n }\n\n async init (): Promise<SessionDatabase> {\n if (this.db) return this\n if (typeof indexedDB === 'undefined') {\n throw new Error('IndexedDB is not available in this environment')\n }\n\n await new Promise<void>((resolve, reject) => {\n const request = indexedDB.open(this.dbName, this.dbVersion)\n\n request.onerror = () => reject(request.error)\n request.onsuccess = () => {\n this.db = request.result\n resolve()\n }\n request.onupgradeneeded = () => {\n const db = request.result\n if (!db.objectStoreNames.contains(this.storeName)) {\n db.createObjectStore(this.storeName)\n }\n }\n })\n\n return this\n }\n\n async setItem (id: string, value: any): Promise<void> {\n await this.init()\n // Some Solid IdPs do not include refresh_token on refresh responses.\n // Keep the previous token instead of overwriting it with null/undefined.\n if (await this.shouldPreserveExistingRefreshToken(id, value)) {\n return\n }\n await this.withStore('readwrite', store => store.put(value, id))\n }\n\n async getItem (id: string): Promise<any> {\n await this.init()\n return this.withStore('readonly', store => store.get(id))\n }\n\n async deleteItem (id: string): Promise<void> {\n await this.init()\n await this.withStore('readwrite', store => store.delete(id))\n }\n\n async clear (): Promise<void> {\n await this.init()\n await this.withStore('readwrite', store => store.clear())\n }\n\n close (): void {\n if (this.db) {\n this.db.close()\n this.db = null\n }\n }\n\n private withStore(mode: IDBTransactionMode, op: (store: IDBObjectStore) => IDBRequest<any>): Promise<any> {\n return new Promise<any>((resolve, reject) => {\n if (!this.db) {\n reject(new Error('Session database not initialized'))\n return\n }\n\n const tx = this.db.transaction(this.storeName, mode)\n const store = tx.objectStore(this.storeName)\n const request = op(store)\n let result: any = null\n\n tx.onerror = () => reject(tx.error ?? request.error)\n tx.onabort = () => reject(tx.error ?? request.error ?? new Error('IndexedDB transaction aborted'))\n tx.oncomplete = () => resolve(result)\n\n request.onerror = () => reject(request.error)\n request.onsuccess = () => {\n result = request.result ?? null\n }\n })\n }\n}\n\n// ---------------------------------------------------------------------------\n// Session instantiation\n// ---------------------------------------------------------------------------\n\nfunction getSessionCoreCtor (): (new (...args: any[]) => OidcSession) | null {\n const coreAny = OidcCore as any\n const candidate = coreAny.SessionCore ?? coreAny.default?.SessionCore ?? coreAny.default\n\n if (typeof candidate !== 'function') {\n return null\n }\n\n return candidate as new (...args: any[]) => OidcSession\n}\n\nconst SessionCoreCtor = getSessionCoreCtor()\n\nfunction createSession (): OidcSession {\n const shouldSkipWorkerInLocalDev = typeof window !== 'undefined' && (() => {\n const host = window.location.hostname\n // In local NSS setups (including subdomain mode like alice.localhost),\n // worker-based session storage can be brittle and lose state on reload.\n // Prefer SessionCore + IndexedDB for deterministic persistence.\n return host === 'localhost' || host === '127.0.0.1' || host.endsWith('.localhost')\n })()\n\n if (shouldSkipWorkerInLocalDev) {\n if (SessionCoreCtor) {\n return new SessionCoreCtor(undefined, { database: new IndexedDbSessionDatabase() })\n }\n return new WebSession()\n }\n\n try {\n return new WebSession()\n } catch (error) {\n // In some deployments, worker URL resolution can become file:// and fail cross-origin.\n // Fall back to SessionCore so auth still works without background refresh worker.\n // Use IndexedDB to keep refresh-token persistence across page reloads.\n console.warn('solid-logic: falling back to non-worker auth session:', error)\n try {\n if (SessionCoreCtor) {\n return new SessionCoreCtor(undefined, { database: new IndexedDbSessionDatabase() })\n }\n return new WebSession()\n } catch (dbError) {\n console.warn('solid-logic: IndexedDB unavailable, using in-memory session database:', dbError)\n if (SessionCoreCtor) {\n return new SessionCoreCtor(undefined, { database: new MemorySessionDatabase() })\n }\n return new WebSession()\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Singleton session\n// ---------------------------------------------------------------------------\n\nconst _session = createSession()\n\nexport { _session }\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"session.esm.js","names":[],"sources":["../../src/authSession/session.ts"],"sourcesContent":["/**\n * OIDC session factory.\n *\n * Everything needed to create and configure the underlying auth session:\n * - Session database backends (in-memory and IndexedDB)\n * - Session instantiation (WebSession or SessionCore, with local-dev\n * fallbacks for environments where the service worker can't load)\n * - Login compatibility shim that normalises legacy call-site signatures\n * and resolves the canonical issuer through /.well-known discovery\n *\n * The raw session instance (_session) is consumed by authSession.ts where\n * the legacy event layer and logout listener are attached.\n *\n * DO NOT import _session directly — always go through authSession.ts so\n * the event wiring is guaranteed to run.\n */\n\nimport {\n Session as WebSession,\n} from '@uvdsl/solid-oidc-client-browser'\nimport * as OidcCore from '@uvdsl/solid-oidc-client-browser/core'\nimport type { Session as OidcSession, SessionDatabase } from '@uvdsl/solid-oidc-client-browser/core'\n\n// ---------------------------------------------------------------------------\n// Session databases\n// ---------------------------------------------------------------------------\n\nexport class MemorySessionDatabase implements SessionDatabase {\n private readonly map = new Map<string, any>()\n\n private shouldPreserveExistingRefreshToken(id: string, value: any): boolean {\n return id === 'refresh_token' && (value == null || value === '') && this.map.has(id)\n }\n\n async init (): Promise<SessionDatabase> {\n return this\n }\n\n async setItem (id: string, value: any): Promise<void> {\n // Some Solid IdPs do not include refresh_token on refresh responses.\n // Keep the previous token instead of overwriting it with null/undefined.\n if (this.shouldPreserveExistingRefreshToken(id, value)) {\n return\n }\n this.map.set(id, value)\n }\n\n async getItem (id: string): Promise<any> {\n return this.map.has(id) ? this.map.get(id) : null\n }\n\n async deleteItem (id: string): Promise<void> {\n this.map.delete(id)\n }\n\n async clear (): Promise<void> {\n this.map.clear()\n }\n\n close (): void {\n // No-op for in-memory database\n }\n}\n\nexport class IndexedDbSessionDatabase implements SessionDatabase {\n private db: IDBDatabase | null = null\n private readonly dbName = 'soidc'\n private readonly storeName = 'session'\n private readonly dbVersion = 1\n\n private async shouldPreserveExistingRefreshToken(id: string, value: any): Promise<boolean> {\n if (id !== 'refresh_token' || !(value == null || value === '')) {\n return false\n }\n const existing = await this.getItem(id)\n return existing != null && existing !== ''\n }\n\n async init (): Promise<SessionDatabase> {\n if (this.db) return this\n if (typeof indexedDB === 'undefined') {\n throw new Error('IndexedDB is not available in this environment')\n }\n\n await new Promise<void>((resolve, reject) => {\n const request = indexedDB.open(this.dbName, this.dbVersion)\n\n request.onerror = () => reject(request.error)\n request.onsuccess = () => {\n this.db = request.result\n resolve()\n }\n request.onupgradeneeded = () => {\n const db = request.result\n if (!db.objectStoreNames.contains(this.storeName)) {\n db.createObjectStore(this.storeName)\n }\n }\n })\n\n return this\n }\n\n async setItem (id: string, value: any): Promise<void> {\n await this.init()\n // Some Solid IdPs do not include refresh_token on refresh responses.\n // Keep the previous token instead of overwriting it with null/undefined.\n if (await this.shouldPreserveExistingRefreshToken(id, value)) {\n return\n }\n await this.withStore('readwrite', store => store.put(value, id))\n }\n\n async getItem (id: string): Promise<any> {\n await this.init()\n return this.withStore('readonly', store => store.get(id))\n }\n\n async deleteItem (id: string): Promise<void> {\n await this.init()\n await this.withStore('readwrite', store => store.delete(id))\n }\n\n async clear (): Promise<void> {\n await this.init()\n await this.withStore('readwrite', store => store.clear())\n }\n\n close (): void {\n if (this.db) {\n this.db.close()\n this.db = null\n }\n }\n\n private withStore(mode: IDBTransactionMode, op: (store: IDBObjectStore) => IDBRequest<any>): Promise<any> {\n return new Promise<any>((resolve, reject) => {\n if (!this.db) {\n reject(new Error('Session database not initialized'))\n return\n }\n\n const tx = this.db.transaction(this.storeName, mode)\n const store = tx.objectStore(this.storeName)\n const request = op(store)\n let result: any = null\n\n tx.onerror = () => reject(tx.error ?? request.error)\n tx.onabort = () => reject(tx.error ?? request.error ?? new Error('IndexedDB transaction aborted'))\n tx.oncomplete = () => resolve(result)\n\n request.onerror = () => reject(request.error)\n request.onsuccess = () => {\n result = request.result ?? null\n }\n })\n }\n}\n\n// ---------------------------------------------------------------------------\n// Session instantiation\n// ---------------------------------------------------------------------------\n\nfunction getSessionCoreCtor (): (new (...args: any[]) => OidcSession) | null {\n const coreAny = OidcCore as any\n const candidate = coreAny.SessionCore ?? coreAny.default?.SessionCore ?? coreAny.default\n\n if (typeof candidate !== 'function') {\n return null\n }\n\n return candidate as new (...args: any[]) => OidcSession\n}\n\nconst SessionCoreCtor = getSessionCoreCtor()\n\nfunction createSession (): OidcSession {\n const shouldSkipWorkerInLocalDev = typeof window !== 'undefined' && (() => {\n const host = window.location.hostname\n // In local NSS setups (including subdomain mode like alice.localhost),\n // worker-based session storage can be brittle and lose state on reload.\n // Prefer SessionCore + IndexedDB for deterministic persistence.\n return host === 'localhost' || host === '127.0.0.1' || host.endsWith('.localhost')\n })()\n\n if (shouldSkipWorkerInLocalDev) {\n if (SessionCoreCtor) {\n return new SessionCoreCtor(undefined, { database: new IndexedDbSessionDatabase() })\n }\n return new WebSession()\n }\n\n try {\n return new WebSession()\n } catch (error) {\n // In some deployments, worker URL resolution can become file:// and fail cross-origin.\n // Fall back to SessionCore so auth still works without background refresh worker.\n // Use IndexedDB to keep refresh-token persistence across page reloads.\n console.warn('solid-logic: falling back to non-worker auth session:', error)\n try {\n if (SessionCoreCtor) {\n return new SessionCoreCtor(undefined, { database: new IndexedDbSessionDatabase() })\n }\n return new WebSession()\n } catch (dbError) {\n console.warn('solid-logic: IndexedDB unavailable, using in-memory session database:', dbError)\n if (SessionCoreCtor) {\n return new SessionCoreCtor(undefined, { database: new MemorySessionDatabase() })\n }\n return new WebSession()\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Singleton session\n// ---------------------------------------------------------------------------\n\nconst _session = createSession()\n\nexport { _session }\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SolidAuthnLogic.cjs.js","names":[],"sources":["../../src/authn/SolidAuthnLogic.ts"],"sourcesContent":["import { namedNode, NamedNode, sym } from 'rdflib'\nimport { appContext, offlineTestID } from './authUtil'\nimport * as debug from '../util/debug'\nimport {\n effectiveIdentity,\n restoreSession,\n sessionOwnsIdentity,\n sessionWasCleared,\n subscribeIdentity,\n type IdentitySubscription,\n type SessionLike\n} from '../authSession/identityState'\nimport type { SessionWithLegacyEvents } from '../authSession/authSession'\nimport type { AuthenticationContext, AuthnLogic } from '../types'\n\n// Some auth clients (uvdsl worker-backed session) only settle restore() on\n// a worker message; a missing/unreachable RefreshWorker asset makes it hang\n// forever. This caps the wait so the login UI can never spin indefinitely.\nconst SESSION_RESTORE_TIMEOUT_MS = 5000\n\n/**\n * Await a session restore promise, but give up after\n * SESSION_RESTORE_TIMEOUT_MS and resolve with undefined so callers can\n * treat a stalled restore as \"no previous session\".\n */\nasync function withRestoreTimeout<T> (promise: Promise<T> | null): Promise<T | undefined> {\n if (promise === null) return undefined\n let timer: ReturnType<typeof setTimeout> | undefined\n const timeout = new Promise<undefined>(resolve => {\n timer = setTimeout(() => resolve(undefined), SESSION_RESTORE_TIMEOUT_MS)\n })\n try {\n return await Promise.race([promise, timeout])\n } finally {\n if (timer !== undefined) clearTimeout(timer)\n }\n}\n\nexport class SolidAuthnLogic implements AuthnLogic {\n private session: SessionWithLegacyEvents\n private checkUserInFlight: Promise<NamedNode | null> | null = null\n private sessionRestoreHookAttached = false\n /**\n * This instance's subscription to the session's identity state. Its lifetime\n * IS this instance's: a cookie probe that answers after `dispose()` is\n * ignored, and the refocus listener is removed with the last instance using\n * the session (see identityState.ts).\n */\n private identity: IdentitySubscription\n\n constructor (solidAuthSession: SessionWithLegacyEvents) {\n this.session = solidAuthSession\n // The cookie-backed identity is invisible to the session (it stays inactive\n // and WebID-less), so it is re-probed when the tab regains focus: another\n // tab may have logged out or switched identity while this one was\n // backgrounded. Only meaningful where the probe applies (*.localhost NSS).\n //\n // TEMPORARY, pending the uvdsl change: this subscription exists because the\n // library does not report a cookie identity change (nor a WebID change that\n // keeps the session active). See the identityState header.\n this.identity = subscribeIdentity(solidAuthSession as unknown as SessionLike, {\n onRefocus: () => this.refreshCookieBackedFallback()\n })\n }\n\n /**\n * Detaches this instance from the session's identity state: the refocus\n * listener is removed with the last instance using the session, and a probe\n * that is still in flight can no longer apply its result.\n */\n dispose (): void {\n this.identity.unsubscribe()\n }\n\n /**\n * Re-probes the NSS cookie-backed identity. Skipped while the session owns\n * the identity — the probe only exists for the case where it does not — and\n * the state drops the result if the session takes ownership while the probe\n * is in flight.\n */\n private async refreshCookieBackedFallback (): Promise<void> {\n if (sessionOwnsIdentity(this.session as unknown as SessionLike)) return\n this.identity.reportCookieIdentity(await this.probeNssCookieBackedWebId())\n }\n\n // we created authSession getter because we want to access it as authn.authSession externally\n get authSession (): SessionWithLegacyEvents { return this.session }\n\n currentUser (): NamedNode | null {\n const app = appContext()\n if (app.viewingNoAuthPage) {\n return sym(app.webId)\n }\n // The state answers with the session's identity when it owns one, the\n // probed cookie identity when the session is inactive (or cleared), and\n // nothing when neither does — a logout that retains the cached WebID must\n // not keep answering for the previous user.\n const { webId } = effectiveIdentity(this.session as unknown as SessionLike)\n return webId ? sym(webId) : offlineTestID() // null unless testing\n }\n\n /**\n * Retrieves currently logged in webId from either\n * defaultTestUser or SolidAuth\n * Also activates a session after login\n * @param [setUserCallback] Optional callback\n * @returns Resolves with webId uri, if no callback provided\n */\n async checkUser<T> (\n setUserCallback?: (me: NamedNode | null) => T\n ): Promise<NamedNode | T | null> {\n // Save hash for \"restorePreviousSession\"\n const preLoginRedirectHash = new URL(window.location.href).hash\n if (preLoginRedirectHash) {\n window.localStorage.setItem('preLoginRedirectHash', preLoginRedirectHash)\n }\n const sessionAny = this.session as any\n if (!this.sessionRestoreHookAttached && typeof sessionAny?.events?.on === 'function') {\n // Backward-compatible hook for auth clients exposing an EventEmitter-style API.\n sessionAny.events.on('sessionRestore', (url: string) => {\n debug.log(`Session restored to ${url}`)\n if (document.location.toString() !== url) history.replaceState(null, '', url)\n })\n this.sessionRestoreHookAttached = true\n }\n\n if (!this.checkUserInFlight) {\n this.checkUserInFlight = this.resolveCurrentUser()\n }\n\n const inFlight = this.checkUserInFlight\n let me: NamedNode | null\n try {\n me = await inFlight\n } finally {\n if (this.checkUserInFlight === inFlight) {\n this.checkUserInFlight = null\n }\n }\n\n return Promise.resolve(setUserCallback ? setUserCallback(me) : me)\n }\n\n private async resolveCurrentUser (): Promise<NamedNode | null> {\n const sessionAny = this.session as any\n\n /**\n * Handle a successful authentication redirect\n */\n const redirectUrl = new URL(window.location.href)\n redirectUrl.hash = ''\n if (typeof sessionAny?.handleIncomingRedirect === 'function') {\n await sessionAny.handleIncomingRedirect({\n restorePreviousSession: true,\n url: redirectUrl.href\n })\n } else {\n // uvdsl-style session (no handleIncomingRedirect): restore then handle redirect.\n //\n // The worker-backed session (WebWorkerSession) resolves restore() ONLY\n // when the SharedWorker posts a message back. If the worker asset can't\n // be fetched — local/dev servers that don't serve the RefreshWorker\n // chunk at the resolved URL, wrong MIME type, CSP, or a worker that\n // fails before `onconnect` — the promise never settles and the login\n // UI would spin forever. Race it against a timeout and treat a stall\n // as \"no previous session\" so the page can render the login button.\n const wasActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId)\n // The shared restore lock also covers this call: a refocus resync can be\n // in flight at the same time, and two overlapping restores could write an\n // older identity back over a newer one.\n const restoring = restoreSession(sessionAny)\n if (restoring) {\n try {\n await withRestoreTimeout(restoring)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n // A failed restore on an inactive session just means \"no usable\n // session to restore\" — whether that's \"No session to restore.\",\n // a stale refresh token / dead client_id returning HTTP 400, or a\n // missing session database. Never let it block the login UI: log\n // and continue as logged-out so the page renders the login button.\n // Only re-throw when the session actually became active, which is\n // an unexpected refresh failure worth surfacing.\n const isNowActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId)\n if (isNowActive && !/no session to restore/i.test(message)) {\n throw error\n }\n debug.log(`Session restore failed, continuing logged-out: ${message}`)\n }\n const isNowActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId)\n if (!wasActive && isNowActive) {\n sessionAny.events?.emit('sessionRestore', window.location.href)\n }\n }\n if (typeof sessionAny?.handleRedirectFromLogin === 'function') {\n const wasActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId)\n await sessionAny.handleRedirectFromLogin()\n const isNowActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId)\n if (!wasActive && isNowActive) {\n sessionAny.events?.emit('login')\n }\n }\n }\n\n // Check to see if a hash was stored in local storage\n const postLoginRedirectHash = window.localStorage.getItem('preLoginRedirectHash')\n if (postLoginRedirectHash) {\n const curUrl = new URL(window.location.href)\n if (curUrl.hash !== postLoginRedirectHash) {\n if (history.pushState) {\n // debug.log('Setting window.location.has using pushState')\n history.pushState(null, document.title, postLoginRedirectHash)\n } else {\n // debug.warn('Setting window.location.has using location.hash')\n location.hash = postLoginRedirectHash\n }\n curUrl.hash = postLoginRedirectHash\n }\n // See https://stackoverflow.com/questions/3870057/how-can-i-update-window-location-hash-without-jumping-the-document\n // window.location.href = curUrl.toString()// @@ See https://developer.mozilla.org/en-US/docs/Web/API/Window/location\n window.localStorage.setItem('preLoginRedirectHash', '')\n }\n\n // Check to see if already logged in / have the WebID\n let me = offlineTestID()\n if (me) {\n return me\n }\n\n let webId = this.webIdFromSession(sessionAny?.info, sessionAny)\n if (!webId) {\n // NSS-specific fallback: recover the WebID from the NSS cookie session\n // when the client restore is empty. The result goes through the identity\n // state, which drops it if the session took ownership while the probe was\n // in flight (or if this instance was disposed meanwhile) — and reports the\n // change like any other transition.\n this.identity.reportCookieIdentity(await this.probeNssCookieBackedWebId())\n webId = effectiveIdentity(sessionAny).webId ?? null\n }\n\n if (webId) {\n me = this.saveUser(webId)\n }\n\n if (me) {\n debug.log(`(Logged in as ${me} by authentication)`)\n }\n\n return me\n }\n\n private async probeNssCookieBackedWebId (): Promise<string | null> {\n if (typeof window === 'undefined') {\n return null\n }\n\n const { hostname, port, protocol } = window.location\n const localhostSuffix = '.localhost'\n // NSS local pods use subdomains like alice.localhost.\n if (!hostname.endsWith(localhostSuffix)) {\n return null\n }\n\n const podName = hostname.slice(0, -localhostSuffix.length)\n if (!podName || podName === 'localhost' || podName.includes('.')) {\n return null\n }\n\n try {\n // NSS returns 403 on this account page when the cookie session is valid.\n const probeResponse = await fetch('/account/password/change', {\n credentials: 'include',\n redirect: 'manual',\n cache: 'no-store'\n })\n if (probeResponse.status !== 403) {\n return null\n }\n const origin = `${protocol}//${hostname}${port ? `:${port}` : ''}`\n return `${origin}/profile/card#me`\n } catch (_error) {\n return null\n }\n }\n\n /**\n * Saves `webId` in `context.me`\n * @param webId\n * @param context\n *\n * @returns Returns the WebID, after setting it\n */\n saveUser (\n webId: NamedNode | string | null,\n context?: AuthenticationContext\n ): NamedNode | null {\n let webIdUri: string\n if (webId) {\n webIdUri = (typeof webId === 'string') ? webId : webId.uri\n const me = namedNode(webIdUri)\n if (context) {\n context.me = me\n }\n return me\n }\n return null\n }\n\n /**\n * @returns {Promise<string|null>} Resolves with WebID URI or null\n */\n webIdFromSession (\n sessionInfo?: { webId?: string, isLoggedIn?: boolean },\n sessionRoot?: { webId?: string, isLoggedIn?: boolean, isActive?: boolean }\n ): string | null {\n const webId = sessionInfo?.webId || sessionRoot?.webId\n if (!webId) {\n return null\n }\n const infoLoggedIn = sessionInfo?.isLoggedIn\n const rootLoggedIn = sessionRoot?.isLoggedIn\n const rootActive = sessionRoot?.isActive\n // An explicit inactive/not-logged-in flag wins over a cached WebID and\n // over a positive flag in another source — the same rule the identity\n // state uses (see identityState.ts). The session root has no `isLoggedIn`\n // property, so requiring every source to be false kept a cached WebID\n // alive across a logout; a mixed snapshot must not resurrect one either. A\n // session whose backing store lost it is inactive as well, however\n // positive its own fields still look.\n if (sessionWasCleared(sessionRoot) ||\n infoLoggedIn === false || rootLoggedIn === false || rootActive === false) {\n return null\n }\n // Active, or a legacy session that reports no state at all.\n return webId\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SolidAuthnLogic.cjs.js","names":[],"sources":["../../src/authn/SolidAuthnLogic.ts"],"sourcesContent":["import { namedNode, NamedNode, sym } from 'rdflib'\nimport { appContext, offlineTestID } from './authUtil'\nimport * as debug from '../util/debug'\nimport {\n effectiveIdentity,\n restoreSession,\n sessionOwnsIdentity,\n sessionWasCleared,\n subscribeIdentity,\n type IdentitySubscription,\n type SessionLike\n} from '../authSession/identityState'\nimport type { SessionWithLegacyEvents } from '../authSession/authSession'\nimport type { AuthenticationContext, AuthnLogic } from '../types'\n\n// Some auth clients (uvdsl worker-backed session) only settle restore() on\n// a worker message; a missing/unreachable RefreshWorker asset makes it hang\n// forever. This caps the wait so the login UI can never spin indefinitely.\nconst SESSION_RESTORE_TIMEOUT_MS = 5000\n\n/**\n * Await a session restore promise, but give up after\n * SESSION_RESTORE_TIMEOUT_MS and resolve with undefined so callers can\n * treat a stalled restore as \"no previous session\".\n */\nasync function withRestoreTimeout<T> (promise: Promise<T> | null): Promise<T | undefined> {\n if (promise === null) return undefined\n let timer: ReturnType<typeof setTimeout> | undefined\n const timeout = new Promise<undefined>(resolve => {\n timer = setTimeout(() => resolve(undefined), SESSION_RESTORE_TIMEOUT_MS)\n })\n try {\n return await Promise.race([promise, timeout])\n } finally {\n if (timer !== undefined) clearTimeout(timer)\n }\n}\n\nexport class SolidAuthnLogic implements AuthnLogic {\n private session: SessionWithLegacyEvents\n private checkUserInFlight: Promise<NamedNode | null> | null = null\n private sessionRestoreHookAttached = false\n /**\n * This instance's subscription to the session's identity state. Its lifetime\n * IS this instance's: a cookie probe that answers after `dispose()` is\n * ignored, and the refocus listener is removed with the last instance using\n * the session (see identityState.ts).\n */\n private identity: IdentitySubscription\n\n constructor (solidAuthSession: SessionWithLegacyEvents) {\n this.session = solidAuthSession\n // The cookie-backed identity is invisible to the session (it stays inactive\n // and WebID-less), so it is re-probed when the tab regains focus: another\n // tab may have logged out or switched identity while this one was\n // backgrounded. Only meaningful where the probe applies (*.localhost NSS).\n //\n // TEMPORARY, pending the uvdsl change: this subscription exists because the\n // library does not report a cookie identity change (nor a WebID change that\n // keeps the session active). See the identityState header.\n this.identity = subscribeIdentity(solidAuthSession as unknown as SessionLike, {\n onRefocus: () => this.refreshCookieBackedFallback()\n })\n }\n\n /**\n * Detaches this instance from the session's identity state: the refocus\n * listener is removed with the last instance using the session, and a probe\n * that is still in flight can no longer apply its result.\n */\n dispose (): void {\n this.identity.unsubscribe()\n }\n\n /**\n * Re-probes the NSS cookie-backed identity. Skipped while the session owns\n * the identity — the probe only exists for the case where it does not — and\n * the state drops the result if the session takes ownership while the probe\n * is in flight.\n */\n private async refreshCookieBackedFallback (): Promise<void> {\n if (sessionOwnsIdentity(this.session as unknown as SessionLike)) return\n this.identity.reportCookieIdentity(await this.probeNssCookieBackedWebId())\n }\n\n // we created authSession getter because we want to access it as authn.authSession externally\n get authSession (): SessionWithLegacyEvents { return this.session }\n\n currentUser (): NamedNode | null {\n const app = appContext()\n if (app.viewingNoAuthPage) {\n return sym(app.webId)\n }\n // The state answers with the session's identity when it owns one, the\n // probed cookie identity when the session is inactive (or cleared), and\n // nothing when neither does — a logout that retains the cached WebID must\n // not keep answering for the previous user.\n const { webId } = effectiveIdentity(this.session as unknown as SessionLike)\n return webId ? sym(webId) : offlineTestID() // null unless testing\n }\n\n /**\n * Retrieves currently logged in webId from either\n * defaultTestUser or SolidAuth\n * Also activates a session after login\n * @param [setUserCallback] Optional callback\n * @returns Resolves with webId uri, if no callback provided\n */\n async checkUser<T> (\n setUserCallback?: (me: NamedNode | null) => T\n ): Promise<NamedNode | T | null> {\n // Save hash for \"restorePreviousSession\"\n const preLoginRedirectHash = new URL(window.location.href).hash\n if (preLoginRedirectHash) {\n window.localStorage.setItem('preLoginRedirectHash', preLoginRedirectHash)\n }\n const sessionAny = this.session as any\n if (!this.sessionRestoreHookAttached && typeof sessionAny?.events?.on === 'function') {\n // Backward-compatible hook for auth clients exposing an EventEmitter-style API.\n sessionAny.events.on('sessionRestore', (url: string) => {\n debug.log(`Session restored to ${url}`)\n if (document.location.toString() !== url) history.replaceState(null, '', url)\n })\n this.sessionRestoreHookAttached = true\n }\n\n if (!this.checkUserInFlight) {\n this.checkUserInFlight = this.resolveCurrentUser()\n }\n\n const inFlight = this.checkUserInFlight\n let me: NamedNode | null\n try {\n me = await inFlight\n } finally {\n if (this.checkUserInFlight === inFlight) {\n this.checkUserInFlight = null\n }\n }\n\n return Promise.resolve(setUserCallback ? setUserCallback(me) : me)\n }\n\n private async resolveCurrentUser (): Promise<NamedNode | null> {\n const sessionAny = this.session as any\n\n /**\n * Handle a successful authentication redirect\n */\n const redirectUrl = new URL(window.location.href)\n redirectUrl.hash = ''\n if (typeof sessionAny?.handleIncomingRedirect === 'function') {\n await sessionAny.handleIncomingRedirect({\n restorePreviousSession: true,\n url: redirectUrl.href\n })\n } else {\n // uvdsl-style session (no handleIncomingRedirect): restore then handle redirect.\n //\n // The worker-backed session (WebWorkerSession) resolves restore() ONLY\n // when the SharedWorker posts a message back. If the worker asset can't\n // be fetched — local/dev servers that don't serve the RefreshWorker\n // chunk at the resolved URL, wrong MIME type, CSP, or a worker that\n // fails before `onconnect` — the promise never settles and the login\n // UI would spin forever. Race it against a timeout and treat a stall\n // as \"no previous session\" so the page can render the login button.\n const wasActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId)\n // The shared restore lock also covers this call: a refocus resync can be\n // in flight at the same time, and two overlapping restores could write an\n // older identity back over a newer one.\n const restoring = restoreSession(sessionAny)\n if (restoring) {\n try {\n await withRestoreTimeout(restoring)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n // A failed restore on an inactive session just means \"no usable\n // session to restore\" — whether that's \"No session to restore.\",\n // a stale refresh token / dead client_id returning HTTP 400, or a\n // missing session database. Never let it block the login UI: log\n // and continue as logged-out so the page renders the login button.\n // Only re-throw when the session actually became active, which is\n // an unexpected refresh failure worth surfacing.\n const isNowActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId)\n if (isNowActive && !/no session to restore/i.test(message)) {\n throw error\n }\n debug.log(`Session restore failed, continuing logged-out: ${message}`)\n }\n const isNowActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId)\n if (!wasActive && isNowActive) {\n sessionAny.events?.emit('sessionRestore', window.location.href)\n }\n }\n if (typeof sessionAny?.handleRedirectFromLogin === 'function') {\n const wasActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId)\n await sessionAny.handleRedirectFromLogin()\n const isNowActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId)\n if (!wasActive && isNowActive) {\n sessionAny.events?.emit('login')\n }\n }\n }\n\n // Check to see if a hash was stored in local storage\n const postLoginRedirectHash = window.localStorage.getItem('preLoginRedirectHash')\n if (postLoginRedirectHash) {\n const curUrl = new URL(window.location.href)\n if (curUrl.hash !== postLoginRedirectHash) {\n if (history.pushState) {\n // debug.log('Setting window.location.has using pushState')\n history.pushState(null, document.title, postLoginRedirectHash)\n } else {\n // debug.warn('Setting window.location.has using location.hash')\n location.hash = postLoginRedirectHash\n }\n curUrl.hash = postLoginRedirectHash\n }\n // See https://stackoverflow.com/questions/3870057/how-can-i-update-window-location-hash-without-jumping-the-document\n // window.location.href = curUrl.toString()// @@ See https://developer.mozilla.org/en-US/docs/Web/API/Window/location\n window.localStorage.setItem('preLoginRedirectHash', '')\n }\n\n // Check to see if already logged in / have the WebID\n let me = offlineTestID()\n if (me) {\n return me\n }\n\n let webId = this.webIdFromSession(sessionAny?.info, sessionAny)\n if (!webId) {\n // NSS-specific fallback: recover the WebID from the NSS cookie session\n // when the client restore is empty. The result goes through the identity\n // state, which drops it if the session took ownership while the probe was\n // in flight (or if this instance was disposed meanwhile) — and reports the\n // change like any other transition.\n this.identity.reportCookieIdentity(await this.probeNssCookieBackedWebId())\n webId = effectiveIdentity(sessionAny).webId ?? null\n }\n\n if (webId) {\n me = this.saveUser(webId)\n }\n\n if (me) {\n debug.log(`(Logged in as ${me} by authentication)`)\n }\n\n return me\n }\n\n private async probeNssCookieBackedWebId (): Promise<string | null> {\n if (typeof window === 'undefined') {\n return null\n }\n\n const { hostname, port, protocol } = window.location\n const localhostSuffix = '.localhost'\n // NSS local pods use subdomains like alice.localhost.\n if (!hostname.endsWith(localhostSuffix)) {\n return null\n }\n\n const podName = hostname.slice(0, -localhostSuffix.length)\n if (!podName || podName === 'localhost' || podName.includes('.')) {\n return null\n }\n\n try {\n // NSS returns 403 on this account page when the cookie session is valid.\n const probeResponse = await fetch('/account/password/change', {\n credentials: 'include',\n redirect: 'manual',\n cache: 'no-store'\n })\n if (probeResponse.status !== 403) {\n return null\n }\n const origin = `${protocol}//${hostname}${port ? `:${port}` : ''}`\n return `${origin}/profile/card#me`\n } catch (_error) {\n return null\n }\n }\n\n /**\n * Saves `webId` in `context.me`\n * @param webId\n * @param context\n *\n * @returns Returns the WebID, after setting it\n */\n saveUser (\n webId: NamedNode | string | null,\n context?: AuthenticationContext\n ): NamedNode | null {\n let webIdUri: string\n if (webId) {\n webIdUri = (typeof webId === 'string') ? webId : webId.uri\n const me = namedNode(webIdUri)\n if (context) {\n context.me = me\n }\n return me\n }\n return null\n }\n\n /**\n * @returns {Promise<string|null>} Resolves with WebID URI or null\n */\n webIdFromSession (\n sessionInfo?: { webId?: string, isLoggedIn?: boolean },\n sessionRoot?: { webId?: string, isLoggedIn?: boolean, isActive?: boolean }\n ): string | null {\n const webId = sessionInfo?.webId || sessionRoot?.webId\n if (!webId) {\n return null\n }\n const infoLoggedIn = sessionInfo?.isLoggedIn\n const rootLoggedIn = sessionRoot?.isLoggedIn\n const rootActive = sessionRoot?.isActive\n // An explicit inactive/not-logged-in flag wins over a cached WebID and\n // over a positive flag in another source — the same rule the identity\n // state uses (see identityState.ts). The session root has no `isLoggedIn`\n // property, so requiring every source to be false kept a cached WebID\n // alive across a logout; a mixed snapshot must not resurrect one either. A\n // session whose backing store lost it is inactive as well, however\n // positive its own fields still look.\n if (sessionWasCleared(sessionRoot) ||\n infoLoggedIn === false || rootLoggedIn === false || rootActive === false) {\n return null\n }\n // Active, or a legacy session that reports no state at all.\n return webId\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { NamedNode } from 'rdflib';
|
|
2
|
+
import type { SessionWithLegacyEvents } from '../authSession/authSession';
|
|
3
|
+
import type { AuthenticationContext, AuthnLogic } from '../types';
|
|
4
|
+
export declare class SolidAuthnLogic implements AuthnLogic {
|
|
5
|
+
private session;
|
|
6
|
+
private checkUserInFlight;
|
|
7
|
+
private sessionRestoreHookAttached;
|
|
8
|
+
/**
|
|
9
|
+
* This instance's subscription to the session's identity state. Its lifetime
|
|
10
|
+
* IS this instance's: a cookie probe that answers after `dispose()` is
|
|
11
|
+
* ignored, and the refocus listener is removed with the last instance using
|
|
12
|
+
* the session (see identityState.ts).
|
|
13
|
+
*/
|
|
14
|
+
private identity;
|
|
15
|
+
constructor(solidAuthSession: SessionWithLegacyEvents);
|
|
16
|
+
/**
|
|
17
|
+
* Detaches this instance from the session's identity state: the refocus
|
|
18
|
+
* listener is removed with the last instance using the session, and a probe
|
|
19
|
+
* that is still in flight can no longer apply its result.
|
|
20
|
+
*/
|
|
21
|
+
dispose(): void;
|
|
22
|
+
/**
|
|
23
|
+
* Re-probes the NSS cookie-backed identity. Skipped while the session owns
|
|
24
|
+
* the identity — the probe only exists for the case where it does not — and
|
|
25
|
+
* the state drops the result if the session takes ownership while the probe
|
|
26
|
+
* is in flight.
|
|
27
|
+
*/
|
|
28
|
+
private refreshCookieBackedFallback;
|
|
29
|
+
get authSession(): SessionWithLegacyEvents;
|
|
30
|
+
currentUser(): NamedNode | null;
|
|
31
|
+
/**
|
|
32
|
+
* Retrieves currently logged in webId from either
|
|
33
|
+
* defaultTestUser or SolidAuth
|
|
34
|
+
* Also activates a session after login
|
|
35
|
+
* @param [setUserCallback] Optional callback
|
|
36
|
+
* @returns Resolves with webId uri, if no callback provided
|
|
37
|
+
*/
|
|
38
|
+
checkUser<T>(setUserCallback?: (me: NamedNode | null) => T): Promise<NamedNode | T | null>;
|
|
39
|
+
private resolveCurrentUser;
|
|
40
|
+
private probeNssCookieBackedWebId;
|
|
41
|
+
/**
|
|
42
|
+
* Saves `webId` in `context.me`
|
|
43
|
+
* @param webId
|
|
44
|
+
* @param context
|
|
45
|
+
*
|
|
46
|
+
* @returns Returns the WebID, after setting it
|
|
47
|
+
*/
|
|
48
|
+
saveUser(webId: NamedNode | string | null, context?: AuthenticationContext): NamedNode | null;
|
|
49
|
+
/**
|
|
50
|
+
* @returns {Promise<string|null>} Resolves with WebID URI or null
|
|
51
|
+
*/
|
|
52
|
+
webIdFromSession(sessionInfo?: {
|
|
53
|
+
webId?: string;
|
|
54
|
+
isLoggedIn?: boolean;
|
|
55
|
+
}, sessionRoot?: {
|
|
56
|
+
webId?: string;
|
|
57
|
+
isLoggedIn?: boolean;
|
|
58
|
+
isActive?: boolean;
|
|
59
|
+
}): string | null;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=SolidAuthnLogic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SolidAuthnLogic.d.ts","sourceRoot":"","sources":["../../src/authn/SolidAuthnLogic.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,SAAS,EAAO,MAAM,QAAQ,CAAA;AAYlD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAA;AACzE,OAAO,KAAK,EAAE,qBAAqB,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAyBjE,qBAAa,eAAgB,YAAW,UAAU;IAChD,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,iBAAiB,CAAyC;IAClE,OAAO,CAAC,0BAA0B,CAAQ;IAC1C;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAsB;gBAEzB,gBAAgB,EAAE,uBAAuB;IAetD;;;;OAIG;IACH,OAAO,IAAK,IAAI;IAIhB;;;;;OAKG;YACW,2BAA2B;IAMzC,IAAI,WAAW,IAAK,uBAAuB,CAAwB;IAEnE,WAAW,IAAK,SAAS,GAAG,IAAI;IAahC;;;;;;OAMG;IACG,SAAS,CAAC,CAAC,EACf,eAAe,CAAC,EAAE,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI,KAAK,CAAC,GAC5C,OAAO,CAAC,SAAS,GAAG,CAAC,GAAG,IAAI,CAAC;YAiClB,kBAAkB;YA4GlB,yBAAyB;IAkCvC;;;;;;OAMG;IACH,QAAQ,CACN,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,IAAI,EAChC,OAAO,CAAC,EAAE,qBAAqB,GAC9B,SAAS,GAAG,IAAI;IAanB;;OAEG;IACH,gBAAgB,CACd,WAAW,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,EACtD,WAAW,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GACzE,MAAM,GAAG,IAAI;CAsBjB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SolidAuthnLogic.esm.js","names":[],"sources":["../../src/authn/SolidAuthnLogic.ts"],"sourcesContent":["import { namedNode, NamedNode, sym } from 'rdflib'\nimport { appContext, offlineTestID } from './authUtil'\nimport * as debug from '../util/debug'\nimport {\n effectiveIdentity,\n restoreSession,\n sessionOwnsIdentity,\n sessionWasCleared,\n subscribeIdentity,\n type IdentitySubscription,\n type SessionLike\n} from '../authSession/identityState'\nimport type { SessionWithLegacyEvents } from '../authSession/authSession'\nimport type { AuthenticationContext, AuthnLogic } from '../types'\n\n// Some auth clients (uvdsl worker-backed session) only settle restore() on\n// a worker message; a missing/unreachable RefreshWorker asset makes it hang\n// forever. This caps the wait so the login UI can never spin indefinitely.\nconst SESSION_RESTORE_TIMEOUT_MS = 5000\n\n/**\n * Await a session restore promise, but give up after\n * SESSION_RESTORE_TIMEOUT_MS and resolve with undefined so callers can\n * treat a stalled restore as \"no previous session\".\n */\nasync function withRestoreTimeout<T> (promise: Promise<T> | null): Promise<T | undefined> {\n if (promise === null) return undefined\n let timer: ReturnType<typeof setTimeout> | undefined\n const timeout = new Promise<undefined>(resolve => {\n timer = setTimeout(() => resolve(undefined), SESSION_RESTORE_TIMEOUT_MS)\n })\n try {\n return await Promise.race([promise, timeout])\n } finally {\n if (timer !== undefined) clearTimeout(timer)\n }\n}\n\nexport class SolidAuthnLogic implements AuthnLogic {\n private session: SessionWithLegacyEvents\n private checkUserInFlight: Promise<NamedNode | null> | null = null\n private sessionRestoreHookAttached = false\n /**\n * This instance's subscription to the session's identity state. Its lifetime\n * IS this instance's: a cookie probe that answers after `dispose()` is\n * ignored, and the refocus listener is removed with the last instance using\n * the session (see identityState.ts).\n */\n private identity: IdentitySubscription\n\n constructor (solidAuthSession: SessionWithLegacyEvents) {\n this.session = solidAuthSession\n // The cookie-backed identity is invisible to the session (it stays inactive\n // and WebID-less), so it is re-probed when the tab regains focus: another\n // tab may have logged out or switched identity while this one was\n // backgrounded. Only meaningful where the probe applies (*.localhost NSS).\n //\n // TEMPORARY, pending the uvdsl change: this subscription exists because the\n // library does not report a cookie identity change (nor a WebID change that\n // keeps the session active). See the identityState header.\n this.identity = subscribeIdentity(solidAuthSession as unknown as SessionLike, {\n onRefocus: () => this.refreshCookieBackedFallback()\n })\n }\n\n /**\n * Detaches this instance from the session's identity state: the refocus\n * listener is removed with the last instance using the session, and a probe\n * that is still in flight can no longer apply its result.\n */\n dispose (): void {\n this.identity.unsubscribe()\n }\n\n /**\n * Re-probes the NSS cookie-backed identity. Skipped while the session owns\n * the identity — the probe only exists for the case where it does not — and\n * the state drops the result if the session takes ownership while the probe\n * is in flight.\n */\n private async refreshCookieBackedFallback (): Promise<void> {\n if (sessionOwnsIdentity(this.session as unknown as SessionLike)) return\n this.identity.reportCookieIdentity(await this.probeNssCookieBackedWebId())\n }\n\n // we created authSession getter because we want to access it as authn.authSession externally\n get authSession (): SessionWithLegacyEvents { return this.session }\n\n currentUser (): NamedNode | null {\n const app = appContext()\n if (app.viewingNoAuthPage) {\n return sym(app.webId)\n }\n // The state answers with the session's identity when it owns one, the\n // probed cookie identity when the session is inactive (or cleared), and\n // nothing when neither does — a logout that retains the cached WebID must\n // not keep answering for the previous user.\n const { webId } = effectiveIdentity(this.session as unknown as SessionLike)\n return webId ? sym(webId) : offlineTestID() // null unless testing\n }\n\n /**\n * Retrieves currently logged in webId from either\n * defaultTestUser or SolidAuth\n * Also activates a session after login\n * @param [setUserCallback] Optional callback\n * @returns Resolves with webId uri, if no callback provided\n */\n async checkUser<T> (\n setUserCallback?: (me: NamedNode | null) => T\n ): Promise<NamedNode | T | null> {\n // Save hash for \"restorePreviousSession\"\n const preLoginRedirectHash = new URL(window.location.href).hash\n if (preLoginRedirectHash) {\n window.localStorage.setItem('preLoginRedirectHash', preLoginRedirectHash)\n }\n const sessionAny = this.session as any\n if (!this.sessionRestoreHookAttached && typeof sessionAny?.events?.on === 'function') {\n // Backward-compatible hook for auth clients exposing an EventEmitter-style API.\n sessionAny.events.on('sessionRestore', (url: string) => {\n debug.log(`Session restored to ${url}`)\n if (document.location.toString() !== url) history.replaceState(null, '', url)\n })\n this.sessionRestoreHookAttached = true\n }\n\n if (!this.checkUserInFlight) {\n this.checkUserInFlight = this.resolveCurrentUser()\n }\n\n const inFlight = this.checkUserInFlight\n let me: NamedNode | null\n try {\n me = await inFlight\n } finally {\n if (this.checkUserInFlight === inFlight) {\n this.checkUserInFlight = null\n }\n }\n\n return Promise.resolve(setUserCallback ? setUserCallback(me) : me)\n }\n\n private async resolveCurrentUser (): Promise<NamedNode | null> {\n const sessionAny = this.session as any\n\n /**\n * Handle a successful authentication redirect\n */\n const redirectUrl = new URL(window.location.href)\n redirectUrl.hash = ''\n if (typeof sessionAny?.handleIncomingRedirect === 'function') {\n await sessionAny.handleIncomingRedirect({\n restorePreviousSession: true,\n url: redirectUrl.href\n })\n } else {\n // uvdsl-style session (no handleIncomingRedirect): restore then handle redirect.\n //\n // The worker-backed session (WebWorkerSession) resolves restore() ONLY\n // when the SharedWorker posts a message back. If the worker asset can't\n // be fetched — local/dev servers that don't serve the RefreshWorker\n // chunk at the resolved URL, wrong MIME type, CSP, or a worker that\n // fails before `onconnect` — the promise never settles and the login\n // UI would spin forever. Race it against a timeout and treat a stall\n // as \"no previous session\" so the page can render the login button.\n const wasActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId)\n // The shared restore lock also covers this call: a refocus resync can be\n // in flight at the same time, and two overlapping restores could write an\n // older identity back over a newer one.\n const restoring = restoreSession(sessionAny)\n if (restoring) {\n try {\n await withRestoreTimeout(restoring)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n // A failed restore on an inactive session just means \"no usable\n // session to restore\" — whether that's \"No session to restore.\",\n // a stale refresh token / dead client_id returning HTTP 400, or a\n // missing session database. Never let it block the login UI: log\n // and continue as logged-out so the page renders the login button.\n // Only re-throw when the session actually became active, which is\n // an unexpected refresh failure worth surfacing.\n const isNowActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId)\n if (isNowActive && !/no session to restore/i.test(message)) {\n throw error\n }\n debug.log(`Session restore failed, continuing logged-out: ${message}`)\n }\n const isNowActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId)\n if (!wasActive && isNowActive) {\n sessionAny.events?.emit('sessionRestore', window.location.href)\n }\n }\n if (typeof sessionAny?.handleRedirectFromLogin === 'function') {\n const wasActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId)\n await sessionAny.handleRedirectFromLogin()\n const isNowActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId)\n if (!wasActive && isNowActive) {\n sessionAny.events?.emit('login')\n }\n }\n }\n\n // Check to see if a hash was stored in local storage\n const postLoginRedirectHash = window.localStorage.getItem('preLoginRedirectHash')\n if (postLoginRedirectHash) {\n const curUrl = new URL(window.location.href)\n if (curUrl.hash !== postLoginRedirectHash) {\n if (history.pushState) {\n // debug.log('Setting window.location.has using pushState')\n history.pushState(null, document.title, postLoginRedirectHash)\n } else {\n // debug.warn('Setting window.location.has using location.hash')\n location.hash = postLoginRedirectHash\n }\n curUrl.hash = postLoginRedirectHash\n }\n // See https://stackoverflow.com/questions/3870057/how-can-i-update-window-location-hash-without-jumping-the-document\n // window.location.href = curUrl.toString()// @@ See https://developer.mozilla.org/en-US/docs/Web/API/Window/location\n window.localStorage.setItem('preLoginRedirectHash', '')\n }\n\n // Check to see if already logged in / have the WebID\n let me = offlineTestID()\n if (me) {\n return me\n }\n\n let webId = this.webIdFromSession(sessionAny?.info, sessionAny)\n if (!webId) {\n // NSS-specific fallback: recover the WebID from the NSS cookie session\n // when the client restore is empty. The result goes through the identity\n // state, which drops it if the session took ownership while the probe was\n // in flight (or if this instance was disposed meanwhile) — and reports the\n // change like any other transition.\n this.identity.reportCookieIdentity(await this.probeNssCookieBackedWebId())\n webId = effectiveIdentity(sessionAny).webId ?? null\n }\n\n if (webId) {\n me = this.saveUser(webId)\n }\n\n if (me) {\n debug.log(`(Logged in as ${me} by authentication)`)\n }\n\n return me\n }\n\n private async probeNssCookieBackedWebId (): Promise<string | null> {\n if (typeof window === 'undefined') {\n return null\n }\n\n const { hostname, port, protocol } = window.location\n const localhostSuffix = '.localhost'\n // NSS local pods use subdomains like alice.localhost.\n if (!hostname.endsWith(localhostSuffix)) {\n return null\n }\n\n const podName = hostname.slice(0, -localhostSuffix.length)\n if (!podName || podName === 'localhost' || podName.includes('.')) {\n return null\n }\n\n try {\n // NSS returns 403 on this account page when the cookie session is valid.\n const probeResponse = await fetch('/account/password/change', {\n credentials: 'include',\n redirect: 'manual',\n cache: 'no-store'\n })\n if (probeResponse.status !== 403) {\n return null\n }\n const origin = `${protocol}//${hostname}${port ? `:${port}` : ''}`\n return `${origin}/profile/card#me`\n } catch (_error) {\n return null\n }\n }\n\n /**\n * Saves `webId` in `context.me`\n * @param webId\n * @param context\n *\n * @returns Returns the WebID, after setting it\n */\n saveUser (\n webId: NamedNode | string | null,\n context?: AuthenticationContext\n ): NamedNode | null {\n let webIdUri: string\n if (webId) {\n webIdUri = (typeof webId === 'string') ? webId : webId.uri\n const me = namedNode(webIdUri)\n if (context) {\n context.me = me\n }\n return me\n }\n return null\n }\n\n /**\n * @returns {Promise<string|null>} Resolves with WebID URI or null\n */\n webIdFromSession (\n sessionInfo?: { webId?: string, isLoggedIn?: boolean },\n sessionRoot?: { webId?: string, isLoggedIn?: boolean, isActive?: boolean }\n ): string | null {\n const webId = sessionInfo?.webId || sessionRoot?.webId\n if (!webId) {\n return null\n }\n const infoLoggedIn = sessionInfo?.isLoggedIn\n const rootLoggedIn = sessionRoot?.isLoggedIn\n const rootActive = sessionRoot?.isActive\n // An explicit inactive/not-logged-in flag wins over a cached WebID and\n // over a positive flag in another source — the same rule the identity\n // state uses (see identityState.ts). The session root has no `isLoggedIn`\n // property, so requiring every source to be false kept a cached WebID\n // alive across a logout; a mixed snapshot must not resurrect one either. A\n // session whose backing store lost it is inactive as well, however\n // positive its own fields still look.\n if (sessionWasCleared(sessionRoot) ||\n infoLoggedIn === false || rootLoggedIn === false || rootActive === false) {\n return null\n }\n // Active, or a legacy session that reports no state at all.\n return webId\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SolidAuthnLogic.esm.js","names":[],"sources":["../../src/authn/SolidAuthnLogic.ts"],"sourcesContent":["import { namedNode, NamedNode, sym } from 'rdflib'\nimport { appContext, offlineTestID } from './authUtil'\nimport * as debug from '../util/debug'\nimport {\n effectiveIdentity,\n restoreSession,\n sessionOwnsIdentity,\n sessionWasCleared,\n subscribeIdentity,\n type IdentitySubscription,\n type SessionLike\n} from '../authSession/identityState'\nimport type { SessionWithLegacyEvents } from '../authSession/authSession'\nimport type { AuthenticationContext, AuthnLogic } from '../types'\n\n// Some auth clients (uvdsl worker-backed session) only settle restore() on\n// a worker message; a missing/unreachable RefreshWorker asset makes it hang\n// forever. This caps the wait so the login UI can never spin indefinitely.\nconst SESSION_RESTORE_TIMEOUT_MS = 5000\n\n/**\n * Await a session restore promise, but give up after\n * SESSION_RESTORE_TIMEOUT_MS and resolve with undefined so callers can\n * treat a stalled restore as \"no previous session\".\n */\nasync function withRestoreTimeout<T> (promise: Promise<T> | null): Promise<T | undefined> {\n if (promise === null) return undefined\n let timer: ReturnType<typeof setTimeout> | undefined\n const timeout = new Promise<undefined>(resolve => {\n timer = setTimeout(() => resolve(undefined), SESSION_RESTORE_TIMEOUT_MS)\n })\n try {\n return await Promise.race([promise, timeout])\n } finally {\n if (timer !== undefined) clearTimeout(timer)\n }\n}\n\nexport class SolidAuthnLogic implements AuthnLogic {\n private session: SessionWithLegacyEvents\n private checkUserInFlight: Promise<NamedNode | null> | null = null\n private sessionRestoreHookAttached = false\n /**\n * This instance's subscription to the session's identity state. Its lifetime\n * IS this instance's: a cookie probe that answers after `dispose()` is\n * ignored, and the refocus listener is removed with the last instance using\n * the session (see identityState.ts).\n */\n private identity: IdentitySubscription\n\n constructor (solidAuthSession: SessionWithLegacyEvents) {\n this.session = solidAuthSession\n // The cookie-backed identity is invisible to the session (it stays inactive\n // and WebID-less), so it is re-probed when the tab regains focus: another\n // tab may have logged out or switched identity while this one was\n // backgrounded. Only meaningful where the probe applies (*.localhost NSS).\n //\n // TEMPORARY, pending the uvdsl change: this subscription exists because the\n // library does not report a cookie identity change (nor a WebID change that\n // keeps the session active). See the identityState header.\n this.identity = subscribeIdentity(solidAuthSession as unknown as SessionLike, {\n onRefocus: () => this.refreshCookieBackedFallback()\n })\n }\n\n /**\n * Detaches this instance from the session's identity state: the refocus\n * listener is removed with the last instance using the session, and a probe\n * that is still in flight can no longer apply its result.\n */\n dispose (): void {\n this.identity.unsubscribe()\n }\n\n /**\n * Re-probes the NSS cookie-backed identity. Skipped while the session owns\n * the identity — the probe only exists for the case where it does not — and\n * the state drops the result if the session takes ownership while the probe\n * is in flight.\n */\n private async refreshCookieBackedFallback (): Promise<void> {\n if (sessionOwnsIdentity(this.session as unknown as SessionLike)) return\n this.identity.reportCookieIdentity(await this.probeNssCookieBackedWebId())\n }\n\n // we created authSession getter because we want to access it as authn.authSession externally\n get authSession (): SessionWithLegacyEvents { return this.session }\n\n currentUser (): NamedNode | null {\n const app = appContext()\n if (app.viewingNoAuthPage) {\n return sym(app.webId)\n }\n // The state answers with the session's identity when it owns one, the\n // probed cookie identity when the session is inactive (or cleared), and\n // nothing when neither does — a logout that retains the cached WebID must\n // not keep answering for the previous user.\n const { webId } = effectiveIdentity(this.session as unknown as SessionLike)\n return webId ? sym(webId) : offlineTestID() // null unless testing\n }\n\n /**\n * Retrieves currently logged in webId from either\n * defaultTestUser or SolidAuth\n * Also activates a session after login\n * @param [setUserCallback] Optional callback\n * @returns Resolves with webId uri, if no callback provided\n */\n async checkUser<T> (\n setUserCallback?: (me: NamedNode | null) => T\n ): Promise<NamedNode | T | null> {\n // Save hash for \"restorePreviousSession\"\n const preLoginRedirectHash = new URL(window.location.href).hash\n if (preLoginRedirectHash) {\n window.localStorage.setItem('preLoginRedirectHash', preLoginRedirectHash)\n }\n const sessionAny = this.session as any\n if (!this.sessionRestoreHookAttached && typeof sessionAny?.events?.on === 'function') {\n // Backward-compatible hook for auth clients exposing an EventEmitter-style API.\n sessionAny.events.on('sessionRestore', (url: string) => {\n debug.log(`Session restored to ${url}`)\n if (document.location.toString() !== url) history.replaceState(null, '', url)\n })\n this.sessionRestoreHookAttached = true\n }\n\n if (!this.checkUserInFlight) {\n this.checkUserInFlight = this.resolveCurrentUser()\n }\n\n const inFlight = this.checkUserInFlight\n let me: NamedNode | null\n try {\n me = await inFlight\n } finally {\n if (this.checkUserInFlight === inFlight) {\n this.checkUserInFlight = null\n }\n }\n\n return Promise.resolve(setUserCallback ? setUserCallback(me) : me)\n }\n\n private async resolveCurrentUser (): Promise<NamedNode | null> {\n const sessionAny = this.session as any\n\n /**\n * Handle a successful authentication redirect\n */\n const redirectUrl = new URL(window.location.href)\n redirectUrl.hash = ''\n if (typeof sessionAny?.handleIncomingRedirect === 'function') {\n await sessionAny.handleIncomingRedirect({\n restorePreviousSession: true,\n url: redirectUrl.href\n })\n } else {\n // uvdsl-style session (no handleIncomingRedirect): restore then handle redirect.\n //\n // The worker-backed session (WebWorkerSession) resolves restore() ONLY\n // when the SharedWorker posts a message back. If the worker asset can't\n // be fetched — local/dev servers that don't serve the RefreshWorker\n // chunk at the resolved URL, wrong MIME type, CSP, or a worker that\n // fails before `onconnect` — the promise never settles and the login\n // UI would spin forever. Race it against a timeout and treat a stall\n // as \"no previous session\" so the page can render the login button.\n const wasActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId)\n // The shared restore lock also covers this call: a refocus resync can be\n // in flight at the same time, and two overlapping restores could write an\n // older identity back over a newer one.\n const restoring = restoreSession(sessionAny)\n if (restoring) {\n try {\n await withRestoreTimeout(restoring)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n // A failed restore on an inactive session just means \"no usable\n // session to restore\" — whether that's \"No session to restore.\",\n // a stale refresh token / dead client_id returning HTTP 400, or a\n // missing session database. Never let it block the login UI: log\n // and continue as logged-out so the page renders the login button.\n // Only re-throw when the session actually became active, which is\n // an unexpected refresh failure worth surfacing.\n const isNowActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId)\n if (isNowActive && !/no session to restore/i.test(message)) {\n throw error\n }\n debug.log(`Session restore failed, continuing logged-out: ${message}`)\n }\n const isNowActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId)\n if (!wasActive && isNowActive) {\n sessionAny.events?.emit('sessionRestore', window.location.href)\n }\n }\n if (typeof sessionAny?.handleRedirectFromLogin === 'function') {\n const wasActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId)\n await sessionAny.handleRedirectFromLogin()\n const isNowActive = sessionAny?.isActive ?? Boolean(sessionAny?.webId)\n if (!wasActive && isNowActive) {\n sessionAny.events?.emit('login')\n }\n }\n }\n\n // Check to see if a hash was stored in local storage\n const postLoginRedirectHash = window.localStorage.getItem('preLoginRedirectHash')\n if (postLoginRedirectHash) {\n const curUrl = new URL(window.location.href)\n if (curUrl.hash !== postLoginRedirectHash) {\n if (history.pushState) {\n // debug.log('Setting window.location.has using pushState')\n history.pushState(null, document.title, postLoginRedirectHash)\n } else {\n // debug.warn('Setting window.location.has using location.hash')\n location.hash = postLoginRedirectHash\n }\n curUrl.hash = postLoginRedirectHash\n }\n // See https://stackoverflow.com/questions/3870057/how-can-i-update-window-location-hash-without-jumping-the-document\n // window.location.href = curUrl.toString()// @@ See https://developer.mozilla.org/en-US/docs/Web/API/Window/location\n window.localStorage.setItem('preLoginRedirectHash', '')\n }\n\n // Check to see if already logged in / have the WebID\n let me = offlineTestID()\n if (me) {\n return me\n }\n\n let webId = this.webIdFromSession(sessionAny?.info, sessionAny)\n if (!webId) {\n // NSS-specific fallback: recover the WebID from the NSS cookie session\n // when the client restore is empty. The result goes through the identity\n // state, which drops it if the session took ownership while the probe was\n // in flight (or if this instance was disposed meanwhile) — and reports the\n // change like any other transition.\n this.identity.reportCookieIdentity(await this.probeNssCookieBackedWebId())\n webId = effectiveIdentity(sessionAny).webId ?? null\n }\n\n if (webId) {\n me = this.saveUser(webId)\n }\n\n if (me) {\n debug.log(`(Logged in as ${me} by authentication)`)\n }\n\n return me\n }\n\n private async probeNssCookieBackedWebId (): Promise<string | null> {\n if (typeof window === 'undefined') {\n return null\n }\n\n const { hostname, port, protocol } = window.location\n const localhostSuffix = '.localhost'\n // NSS local pods use subdomains like alice.localhost.\n if (!hostname.endsWith(localhostSuffix)) {\n return null\n }\n\n const podName = hostname.slice(0, -localhostSuffix.length)\n if (!podName || podName === 'localhost' || podName.includes('.')) {\n return null\n }\n\n try {\n // NSS returns 403 on this account page when the cookie session is valid.\n const probeResponse = await fetch('/account/password/change', {\n credentials: 'include',\n redirect: 'manual',\n cache: 'no-store'\n })\n if (probeResponse.status !== 403) {\n return null\n }\n const origin = `${protocol}//${hostname}${port ? `:${port}` : ''}`\n return `${origin}/profile/card#me`\n } catch (_error) {\n return null\n }\n }\n\n /**\n * Saves `webId` in `context.me`\n * @param webId\n * @param context\n *\n * @returns Returns the WebID, after setting it\n */\n saveUser (\n webId: NamedNode | string | null,\n context?: AuthenticationContext\n ): NamedNode | null {\n let webIdUri: string\n if (webId) {\n webIdUri = (typeof webId === 'string') ? webId : webId.uri\n const me = namedNode(webIdUri)\n if (context) {\n context.me = me\n }\n return me\n }\n return null\n }\n\n /**\n * @returns {Promise<string|null>} Resolves with WebID URI or null\n */\n webIdFromSession (\n sessionInfo?: { webId?: string, isLoggedIn?: boolean },\n sessionRoot?: { webId?: string, isLoggedIn?: boolean, isActive?: boolean }\n ): string | null {\n const webId = sessionInfo?.webId || sessionRoot?.webId\n if (!webId) {\n return null\n }\n const infoLoggedIn = sessionInfo?.isLoggedIn\n const rootLoggedIn = sessionRoot?.isLoggedIn\n const rootActive = sessionRoot?.isActive\n // An explicit inactive/not-logged-in flag wins over a cached WebID and\n // over a positive flag in another source — the same rule the identity\n // state uses (see identityState.ts). The session root has no `isLoggedIn`\n // property, so requiring every source to be false kept a cached WebID\n // alive across a logout; a mixed snapshot must not resurrect one either. A\n // session whose backing store lost it is inactive as well, however\n // positive its own fields still look.\n if (sessionWasCleared(sessionRoot) ||\n infoLoggedIn === false || rootLoggedIn === false || rootActive === false) {\n return null\n }\n // Active, or a legacy session that reports no state at all.\n return webId\n }\n}\n"],"mappings":"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"}
|