@securitydept/session-context-client 0.3.0-beta.1 → 0.3.0-beta.3

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.
@@ -1,4 +1,4 @@
1
- import { AuthenticatedPrincipal, CancellationTokenTrait, ClientRuntime, HttpTransport } from "@securitydept/client";
1
+ import { AuthenticatedPrincipal, CancellationTokenTrait, ClientEnvironment, HttpTransport } from "@securitydept/client";
2
2
 
3
3
  //#region src/types.d.ts
4
4
  declare const SessionContextSource: {
@@ -25,7 +25,7 @@ interface SessionContextClientConfig {
25
25
  userInfoPath?: string;
26
26
  /** Name of the query parameter for post-auth redirect (default: "post_auth_redirect_uri"). */
27
27
  postAuthRedirectParam?: string;
28
- /** Optional key used with `runtime.sessionStore` for pending login redirect state. */
28
+ /** Optional key used with `environment.sessionStore` for pending login redirect state. */
29
29
  loginRedirectStateKey?: string;
30
30
  }
31
31
  //#endregion
@@ -44,7 +44,7 @@ declare class SessionContextClient {
44
44
  private readonly _postAuthRedirectParam;
45
45
  private readonly _pendingLoginRedirectStore?;
46
46
  private readonly _loginRedirectStateKey;
47
- constructor(config: SessionContextClientConfig, runtime?: Pick<ClientRuntime, "sessionStore">);
47
+ constructor(config: SessionContextClientConfig, environment?: Pick<ClientEnvironment, "sessionStore">);
48
48
  /** Build the login URL, optionally with a post-auth redirect. */
49
49
  loginUrl(postAuthRedirectUri?: string): string;
50
50
  /** Build the logout URL. */
@@ -80,4 +80,4 @@ declare class SessionContextClient {
80
80
  }
81
81
  //#endregion
82
82
  export { SessionPrincipal as a, SessionInfo as i, SessionContextClientConfig as n, SessionContextSource as r, SessionContextClient as t };
83
- //# sourceMappingURL=client-Dzfx3BXp.d.mts.map
83
+ //# sourceMappingURL=client-BRqKhOuj.d.mts.map
@@ -55,15 +55,15 @@ var SessionContextClient = class {
55
55
  _postAuthRedirectParam;
56
56
  _pendingLoginRedirectStore;
57
57
  _loginRedirectStateKey;
58
- constructor(config, runtime = {}) {
58
+ constructor(config, environment = {}) {
59
59
  this._baseUrl = config.baseUrl.replace(/\/+$/, "");
60
60
  this._loginPath = config.loginPath ?? DEFAULT_LOGIN_PATH;
61
61
  this._logoutPath = config.logoutPath ?? DEFAULT_LOGOUT_PATH;
62
62
  this._userInfoPath = config.userInfoPath ?? DEFAULT_USER_INFO_PATH;
63
63
  this._postAuthRedirectParam = config.postAuthRedirectParam ?? DEFAULT_POST_AUTH_REDIRECT_PARAM;
64
64
  this._loginRedirectStateKey = config.loginRedirectStateKey ?? `${DEFAULT_LOGIN_REDIRECT_STATE_KEY}:${this._baseUrl || "relative"}`;
65
- this._pendingLoginRedirectStore = runtime.sessionStore ? createEphemeralFlowStore({
66
- store: runtime.sessionStore,
65
+ this._pendingLoginRedirectStore = environment.sessionStore ? createEphemeralFlowStore({
66
+ store: environment.sessionStore,
67
67
  key: this._loginRedirectStateKey,
68
68
  codec: {
69
69
  encode(value) {
@@ -180,4 +180,4 @@ function normalizeSessionInfo(body) {
180
180
  //#endregion
181
181
  export { SessionUserInfoResponseSchema as i, SessionContextSource as n, SessionInfoSchema as r, SessionContextClient as t };
182
182
 
183
- //# sourceMappingURL=client-BcZv4X8b.mjs.map
183
+ //# sourceMappingURL=client-CTJyzp37.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-CTJyzp37.mjs","names":["raw"],"sources":["../src/schemas.ts","../src/types.ts","../src/client.ts"],"sourcesContent":["// Session /auth/session/user-info response schemas — shared principal validation\n//\n// These schemas validate the two accepted shapes of the session user-info\n// response:\n// 1. Internal (camelCase):\n// { principal: { subject, displayName, picture?, issuer?, claims? } }\n// 2. Rust server wire (snake_case):\n// { subject, display_name, picture?, issuer?, claims? }\n\nimport {\n\tcreateSchema,\n\tnormalizeAuthenticatedPrincipal,\n\tnormalizeAuthenticatedPrincipalWire,\n} from \"@securitydept/client\";\nimport type { SessionInfo, SessionPrincipal } from \"./types\";\n\n/**\n * Schema for the canonical SessionInfo shape (camelCase principal).\n *\n * This is the preferred shape once normalized.\n */\nexport const SessionInfoSchema = createSchema<SessionInfo>({\n\tvalidate(input: unknown) {\n\t\tif (\n\t\t\ttypeof input === \"object\" &&\n\t\t\tinput !== null &&\n\t\t\t\"principal\" in input &&\n\t\t\ttypeof (input as Record<string, unknown>).principal === \"object\" &&\n\t\t\t(input as Record<string, unknown>).principal !== null\n\t\t) {\n\t\t\tconst principal = normalizeAuthenticatedPrincipal(\n\t\t\t\t(input as { principal: Record<string, unknown> }).principal,\n\t\t\t);\n\t\t\tif (principal) {\n\t\t\t\treturn { value: { principal } };\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\tissues: [\n\t\t\t\t{\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t\"Expected SessionInfo with principal.subject and principal.displayName\",\n\t\t\t\t\tpath: [\"principal\", \"subject\"],\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t},\n});\n\n/**\n * Schema for the Rust server session user-info response shape.\n *\n * Validates the real `/auth/session/user-info` wire payload and normalizes it\n * into SessionInfo in one step.\n */\nexport const SessionUserInfoResponseSchema = createSchema<SessionInfo>({\n\tvalidate(input: unknown) {\n\t\tif (\n\t\t\ttypeof input === \"object\" &&\n\t\t\tinput !== null &&\n\t\t\t\"subject\" in input &&\n\t\t\ttypeof (input as Record<string, unknown>).subject === \"string\"\n\t\t) {\n\t\t\tconst raw = input as Record<string, unknown>;\n\t\t\tconst principal = normalizeAuthenticatedPrincipalWire(raw);\n\t\t\tif (principal) {\n\t\t\t\treturn { value: { principal: principal as SessionPrincipal } };\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\tissues: [\n\t\t\t\t{\n\t\t\t\t\tmessage: \"Expected server UserInfo response with subject (string)\",\n\t\t\t\t\tpath: [\"subject\"],\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t},\n});\n","import type { AuthenticatedPrincipal } from \"@securitydept/client\";\n\n// --- Session Context Client types ---\n\nexport const SessionContextSource = {\n\tSessionContext: \"session-context\",\n} as const;\n\nexport type SessionContextSource =\n\t(typeof SessionContextSource)[keyof typeof SessionContextSource];\n\n/** Session principal — shared authenticated principal semantics via @securitydept/client. */\nexport type SessionPrincipal = AuthenticatedPrincipal;\n\n/** Session info returned from the server. */\nexport interface SessionInfo {\n\tprincipal: SessionPrincipal;\n\tattributes?: Record<string, unknown>;\n\textra?: Record<string, unknown>;\n}\n\n/** Configuration for the Session Context Client. */\nexport interface SessionContextClientConfig {\n\t/** Base URL of the SecurityDept server. */\n\tbaseUrl: string;\n\t/** Login path (default: \"/login\"). */\n\tloginPath?: string;\n\t/** Logout path (default: \"/logout\"). */\n\tlogoutPath?: string;\n\t/** User info endpoint path (default: \"/auth/session/user-info\"). */\n\tuserInfoPath?: string;\n\t/** Name of the query parameter for post-auth redirect (default: \"post_auth_redirect_uri\"). */\n\tpostAuthRedirectParam?: string;\n\t/** Optional key used with `environment.sessionStore` for pending login redirect state. */\n\tloginRedirectStateKey?: string;\n}\n","import type {\n\tCancellationTokenTrait,\n\tClientEnvironment,\n\tEphemeralFlowStore,\n\tHttpTransport,\n} from \"@securitydept/client\";\nimport {\n\tClientError,\n\tClientErrorKind,\n\tcreateEphemeralFlowStore,\n\tvalidateWithSchemaSync,\n} from \"@securitydept/client\";\nimport { SessionInfoSchema, SessionUserInfoResponseSchema } from \"./schemas\";\nimport {\n\ttype SessionContextClientConfig,\n\tSessionContextSource,\n\ttype SessionInfo,\n} from \"./types\";\n\nconst DEFAULT_LOGIN_PATH = \"/auth/session/login\";\nconst DEFAULT_LOGOUT_PATH = \"/auth/session/logout\";\nconst DEFAULT_USER_INFO_PATH = \"/auth/session/user-info\";\nconst DEFAULT_POST_AUTH_REDIRECT_PARAM = \"post_auth_redirect_uri\";\nconst DEFAULT_LOGIN_REDIRECT_STATE_KEY =\n\t\"securitydept.session_context.pending_login_redirect\";\n\n/**\n * Session Context Client.\n *\n * Provides login/logout URL construction, session probing via `/user-info`, and\n * minimal session-scoped flow state for login redirect intent.\n */\nexport class SessionContextClient {\n\tprivate readonly _baseUrl: string;\n\tprivate readonly _loginPath: string;\n\tprivate readonly _logoutPath: string;\n\tprivate readonly _userInfoPath: string;\n\tprivate readonly _postAuthRedirectParam: string;\n\tprivate readonly _pendingLoginRedirectStore?: EphemeralFlowStore<string>;\n\tprivate readonly _loginRedirectStateKey: string;\n\n\tconstructor(\n\t\tconfig: SessionContextClientConfig,\n\t\tenvironment: Pick<ClientEnvironment, \"sessionStore\"> = {},\n\t) {\n\t\tthis._baseUrl = config.baseUrl.replace(/\\/+$/, \"\");\n\t\tthis._loginPath = config.loginPath ?? DEFAULT_LOGIN_PATH;\n\t\tthis._logoutPath = config.logoutPath ?? DEFAULT_LOGOUT_PATH;\n\t\tthis._userInfoPath = config.userInfoPath ?? DEFAULT_USER_INFO_PATH;\n\t\tthis._postAuthRedirectParam =\n\t\t\tconfig.postAuthRedirectParam ?? DEFAULT_POST_AUTH_REDIRECT_PARAM;\n\t\tthis._loginRedirectStateKey =\n\t\t\tconfig.loginRedirectStateKey ??\n\t\t\t`${DEFAULT_LOGIN_REDIRECT_STATE_KEY}:${this._baseUrl || \"relative\"}`;\n\t\tthis._pendingLoginRedirectStore = environment.sessionStore\n\t\t\t? createEphemeralFlowStore<string>({\n\t\t\t\t\tstore: environment.sessionStore,\n\t\t\t\t\tkey: this._loginRedirectStateKey,\n\t\t\t\t\tcodec: {\n\t\t\t\t\t\tencode(value) {\n\t\t\t\t\t\t\treturn value;\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdecode(raw) {\n\t\t\t\t\t\t\treturn raw;\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t: undefined;\n\t}\n\n\t/** Build the login URL, optionally with a post-auth redirect. */\n\tloginUrl(postAuthRedirectUri?: string): string {\n\t\tconst base = this._baseUrl + this._loginPath;\n\t\tif (postAuthRedirectUri) {\n\t\t\tconst params = new URLSearchParams({\n\t\t\t\t[this._postAuthRedirectParam]: postAuthRedirectUri,\n\t\t\t});\n\t\t\treturn `${base}?${params.toString()}`;\n\t\t}\n\t\treturn base;\n\t}\n\n\t/** Build the logout URL. */\n\tlogoutUrl(): string {\n\t\treturn this._baseUrl + this._logoutPath;\n\t}\n\n\t/** Save the pending post-auth redirect using the canonical browser-shell vocabulary. */\n\tasync rememberPostAuthRedirect(postAuthRedirectUri: string): Promise<void> {\n\t\tawait this.savePendingLoginRedirect(postAuthRedirectUri);\n\t}\n\n\t/** Save the pending post-auth redirect in `sessionStore` when available. */\n\tasync savePendingLoginRedirect(postAuthRedirectUri: string): Promise<void> {\n\t\tif (!this._pendingLoginRedirectStore) {\n\t\t\treturn;\n\t\t}\n\n\t\tawait this._pendingLoginRedirectStore.save(postAuthRedirectUri);\n\t}\n\n\t/** Load the pending post-auth redirect from `sessionStore`. */\n\tasync loadPendingLoginRedirect(): Promise<string | null> {\n\t\tif (!this._pendingLoginRedirectStore) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn await this._pendingLoginRedirectStore.load();\n\t}\n\n\t/** Load and consume the pending post-auth redirect from `sessionStore`. */\n\tasync consumePendingLoginRedirect(): Promise<string | null> {\n\t\tif (!this._pendingLoginRedirectStore) {\n\t\t\treturn null;\n\t\t}\n\t\treturn await this._pendingLoginRedirectStore.consume();\n\t}\n\n\t/** Clear the pending post-auth redirect from `sessionStore`. */\n\tasync clearPendingLoginRedirect(): Promise<void> {\n\t\tif (!this._pendingLoginRedirectStore) {\n\t\t\treturn;\n\t\t}\n\n\t\tawait this._pendingLoginRedirectStore.clear();\n\t}\n\n\t/** Clear the pending post-auth redirect using the canonical browser-shell vocabulary. */\n\tasync clearPostAuthRedirect(): Promise<void> {\n\t\tawait this.clearPendingLoginRedirect();\n\t}\n\n\t/** Resolve the next login URL by consuming any pending post-auth redirect intent. */\n\tasync resolveLoginUrl(): Promise<string> {\n\t\tconst pendingRedirect = await this.consumePendingLoginRedirect();\n\t\treturn this.loginUrl(pendingRedirect ?? undefined);\n\t}\n\n\t/**\n\t * Fetch the current session info from `/user-info`.\n\t *\n\t * Returns `null` only for 401/403 (unauthenticated).\n\t * Throws `ClientError` for any other non-2xx response to distinguish\n\t * server failures from unauthenticated state.\n\t */\n\tasync fetchUserInfo(\n\t\ttransport: HttpTransport,\n\t\tcancellationToken?: CancellationTokenTrait,\n\t): Promise<SessionInfo | null> {\n\t\tconst response = await transport.execute({\n\t\t\turl: this._baseUrl + this._userInfoPath,\n\t\t\tmethod: \"GET\",\n\t\t\theaders: {},\n\t\t\tcancellationToken,\n\t\t});\n\n\t\tif (response.status === 401 || response.status === 403) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (response.status >= 200 && response.status < 300) {\n\t\t\treturn normalizeSessionInfo(response.body);\n\t\t}\n\n\t\t// Non-2xx, non-401/403 — this is a server error, not \"unauthenticated\".\n\t\tthrow ClientError.fromHttpResponse(response.status, response.body);\n\t}\n\n\t/** Check whether a session exists on the server. */\n\tasync isAuthenticated(\n\t\ttransport: HttpTransport,\n\t\tcancellationToken?: CancellationTokenTrait,\n\t): Promise<boolean> {\n\t\tconst me = await this.fetchUserInfo(transport, cancellationToken);\n\t\treturn me !== null;\n\t}\n\n\t/** Execute logout against the configured session logout endpoint. */\n\tasync logout(\n\t\ttransport: HttpTransport,\n\t\tcancellationToken?: CancellationTokenTrait,\n\t): Promise<void> {\n\t\tconst response = await transport.execute({\n\t\t\turl: this.logoutUrl(),\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"content-type\": \"application/json\" },\n\t\t\tbody: JSON.stringify({}),\n\t\t\tcancellationToken,\n\t\t});\n\n\t\tif (response.status >= 200 && response.status < 300) {\n\t\t\treturn;\n\t\t}\n\n\t\tthrow ClientError.fromHttpResponse(response.status, response.body);\n\t}\n\n\t/** Execute logout and clear any stale post-auth redirect intent in one canonical step. */\n\tasync logoutAndClearPendingLoginRedirect(\n\t\ttransport: HttpTransport,\n\t\tcancellationToken?: CancellationTokenTrait,\n\t): Promise<void> {\n\t\tawait this.logout(transport, cancellationToken);\n\t\tawait this.clearPendingLoginRedirect();\n\t}\n}\n\n/**\n * Normalize a /me response body using @standard-schema aligned schemas.\n *\n * Tries the canonical SessionInfo shape first, then falls back to the\n * server-side snake_case UserInfo shape. Throws a protocol-level ClientError\n * if neither schema matches.\n */\nfunction normalizeSessionInfo(body: unknown): SessionInfo {\n\t// Try canonical camelCase shape first.\n\tconst infoResult = validateWithSchemaSync(SessionInfoSchema, body);\n\tif (infoResult.success) {\n\t\treturn infoResult.value;\n\t}\n\n\t// Try server-side snake_case shape.\n\tconst userInfoResult = validateWithSchemaSync(\n\t\tSessionUserInfoResponseSchema,\n\t\tbody,\n\t);\n\tif (userInfoResult.success) {\n\t\treturn userInfoResult.value;\n\t}\n\n\tthrow new ClientError({\n\t\tkind: ClientErrorKind.Protocol,\n\t\tcode: \"session.invalid_user_info_payload\",\n\t\tmessage: \"Session /user-info payload is invalid\",\n\t\tsource: SessionContextSource.SessionContext,\n\t});\n}\n"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -1,5 +1,42 @@
1
- import { a as SessionPrincipal, i as SessionInfo, n as SessionContextClientConfig, r as SessionContextSource, t as SessionContextClient } from "./client-Dzfx3BXp.mjs";
1
+ import { a as SessionPrincipal, i as SessionInfo, n as SessionContextClientConfig, r as SessionContextSource, t as SessionContextClient } from "./client-BRqKhOuj.mjs";
2
+ import { HttpTransport, ReadableSignalTrait } from "@securitydept/client";
2
3
 
4
+ //#region src/controller.d.ts
5
+ declare const SessionContextControllerStatus: {
6
+ readonly Idle: "idle";
7
+ readonly Loading: "loading";
8
+ readonly Authenticated: "authenticated";
9
+ readonly Unauthenticated: "unauthenticated";
10
+ readonly Error: "error";
11
+ };
12
+ type SessionContextControllerStatus = (typeof SessionContextControllerStatus)[keyof typeof SessionContextControllerStatus];
13
+ interface SessionContextControllerState {
14
+ status: SessionContextControllerStatus;
15
+ session: SessionInfo | null;
16
+ error: unknown | null;
17
+ }
18
+ interface SessionContextControllerOptions {
19
+ client: SessionContextClient;
20
+ transport: HttpTransport;
21
+ }
22
+ declare class SessionContextController {
23
+ readonly client: SessionContextClient;
24
+ readonly state: ReadableSignalTrait<SessionContextControllerState>;
25
+ private readonly transport;
26
+ private readonly stateSignal;
27
+ private refreshPromise;
28
+ private disposed;
29
+ constructor(options: SessionContextControllerOptions);
30
+ getState(): SessionContextControllerState;
31
+ subscribe(listener: () => void): () => void;
32
+ refresh(): Promise<SessionInfo | null>;
33
+ rememberPostAuthRedirect(uri: string): Promise<void>;
34
+ clearPostAuthRedirect(): Promise<void>;
35
+ resolveLoginUrl(): Promise<string>;
36
+ logout(): Promise<void>;
37
+ dispose(): void;
38
+ }
39
+ //#endregion
3
40
  //#region src/schemas.d.ts
4
41
  /**
5
42
  * Schema for the canonical SessionInfo shape (camelCase principal).
@@ -15,5 +52,5 @@ declare const SessionInfoSchema: any;
15
52
  */
16
53
  declare const SessionUserInfoResponseSchema: any;
17
54
  //#endregion
18
- export { SessionContextClient, type SessionContextClientConfig, SessionContextSource, type SessionInfo, SessionInfoSchema, type SessionPrincipal, SessionUserInfoResponseSchema };
55
+ export { SessionContextClient, type SessionContextClientConfig, SessionContextController, type SessionContextControllerOptions, type SessionContextControllerState, SessionContextControllerStatus, type SessionContextControllerStatus as SessionContextControllerStatusType, SessionContextSource, type SessionInfo, SessionInfoSchema, type SessionPrincipal, SessionUserInfoResponseSchema };
19
56
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,2 +1,98 @@
1
- import { i as SessionUserInfoResponseSchema, n as SessionContextSource, r as SessionInfoSchema, t as SessionContextClient } from "./client-BcZv4X8b.mjs";
2
- export { SessionContextClient, SessionContextSource, SessionInfoSchema, SessionUserInfoResponseSchema };
1
+ import { i as SessionUserInfoResponseSchema, n as SessionContextSource, r as SessionInfoSchema, t as SessionContextClient } from "./client-CTJyzp37.mjs";
2
+ import { createSignal, readonlySignal } from "@securitydept/client";
3
+ //#region src/controller.ts
4
+ const SessionContextControllerStatus = {
5
+ Idle: "idle",
6
+ Loading: "loading",
7
+ Authenticated: "authenticated",
8
+ Unauthenticated: "unauthenticated",
9
+ Error: "error"
10
+ };
11
+ var SessionContextController = class {
12
+ client;
13
+ state;
14
+ transport;
15
+ stateSignal = createSignal({
16
+ status: SessionContextControllerStatus.Idle,
17
+ session: null,
18
+ error: null
19
+ });
20
+ refreshPromise = null;
21
+ disposed = false;
22
+ constructor(options) {
23
+ this.client = options.client;
24
+ this.transport = options.transport;
25
+ this.state = readonlySignal(this.stateSignal);
26
+ }
27
+ getState() {
28
+ return this.stateSignal.get();
29
+ }
30
+ subscribe(listener) {
31
+ return this.stateSignal.subscribe(listener);
32
+ }
33
+ refresh() {
34
+ if (this.disposed) return Promise.resolve(this.stateSignal.get().session);
35
+ if (this.refreshPromise) return this.refreshPromise;
36
+ this.stateSignal.set({
37
+ status: SessionContextControllerStatus.Loading,
38
+ session: this.stateSignal.get().session,
39
+ error: null
40
+ });
41
+ this.refreshPromise = this.client.fetchUserInfo(this.transport).then((session) => {
42
+ if (!this.disposed) this.stateSignal.set({
43
+ status: session ? SessionContextControllerStatus.Authenticated : SessionContextControllerStatus.Unauthenticated,
44
+ session,
45
+ error: null
46
+ });
47
+ return session;
48
+ }).catch((error) => {
49
+ if (!this.disposed) this.stateSignal.set({
50
+ status: SessionContextControllerStatus.Error,
51
+ session: null,
52
+ error
53
+ });
54
+ throw error;
55
+ }).finally(() => {
56
+ this.refreshPromise = null;
57
+ });
58
+ return this.refreshPromise;
59
+ }
60
+ async rememberPostAuthRedirect(uri) {
61
+ await this.client.rememberPostAuthRedirect(uri);
62
+ }
63
+ async clearPostAuthRedirect() {
64
+ await this.client.clearPostAuthRedirect();
65
+ }
66
+ async resolveLoginUrl() {
67
+ return await this.client.resolveLoginUrl();
68
+ }
69
+ async logout() {
70
+ this.stateSignal.set({
71
+ status: SessionContextControllerStatus.Loading,
72
+ session: this.stateSignal.get().session,
73
+ error: null
74
+ });
75
+ try {
76
+ await this.client.logoutAndClearPendingLoginRedirect(this.transport);
77
+ if (!this.disposed) this.stateSignal.set({
78
+ status: SessionContextControllerStatus.Unauthenticated,
79
+ session: null,
80
+ error: null
81
+ });
82
+ } catch (error) {
83
+ if (!this.disposed) this.stateSignal.set({
84
+ status: SessionContextControllerStatus.Error,
85
+ session: null,
86
+ error
87
+ });
88
+ throw error;
89
+ }
90
+ }
91
+ dispose() {
92
+ this.disposed = true;
93
+ }
94
+ };
95
+ //#endregion
96
+ export { SessionContextClient, SessionContextController, SessionContextControllerStatus, SessionContextSource, SessionInfoSchema, SessionUserInfoResponseSchema };
97
+
98
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/controller.ts"],"sourcesContent":["import {\n\tcreateSignal,\n\ttype HttpTransport,\n\ttype ReadableSignalTrait,\n\treadonlySignal,\n} from \"@securitydept/client\";\nimport type { SessionContextClient } from \"./client\";\nimport type { SessionInfo } from \"./types\";\n\nexport const SessionContextControllerStatus = {\n\tIdle: \"idle\",\n\tLoading: \"loading\",\n\tAuthenticated: \"authenticated\",\n\tUnauthenticated: \"unauthenticated\",\n\tError: \"error\",\n} as const;\n\nexport type SessionContextControllerStatus =\n\t(typeof SessionContextControllerStatus)[keyof typeof SessionContextControllerStatus];\n\nexport interface SessionContextControllerState {\n\tstatus: SessionContextControllerStatus;\n\tsession: SessionInfo | null;\n\terror: unknown | null;\n}\n\nexport interface SessionContextControllerOptions {\n\tclient: SessionContextClient;\n\ttransport: HttpTransport;\n}\n\nexport class SessionContextController {\n\treadonly client: SessionContextClient;\n\treadonly state: ReadableSignalTrait<SessionContextControllerState>;\n\n\tprivate readonly transport: HttpTransport;\n\tprivate readonly stateSignal = createSignal<SessionContextControllerState>({\n\t\tstatus: SessionContextControllerStatus.Idle,\n\t\tsession: null,\n\t\terror: null,\n\t});\n\tprivate refreshPromise: Promise<SessionInfo | null> | null = null;\n\tprivate disposed = false;\n\n\tconstructor(options: SessionContextControllerOptions) {\n\t\tthis.client = options.client;\n\t\tthis.transport = options.transport;\n\t\tthis.state = readonlySignal(this.stateSignal);\n\t}\n\n\tgetState(): SessionContextControllerState {\n\t\treturn this.stateSignal.get();\n\t}\n\n\tsubscribe(listener: () => void): () => void {\n\t\treturn this.stateSignal.subscribe(listener);\n\t}\n\n\trefresh(): Promise<SessionInfo | null> {\n\t\tif (this.disposed) {\n\t\t\treturn Promise.resolve(this.stateSignal.get().session);\n\t\t}\n\t\tif (this.refreshPromise) {\n\t\t\treturn this.refreshPromise;\n\t\t}\n\n\t\tthis.stateSignal.set({\n\t\t\tstatus: SessionContextControllerStatus.Loading,\n\t\t\tsession: this.stateSignal.get().session,\n\t\t\terror: null,\n\t\t});\n\n\t\tthis.refreshPromise = this.client\n\t\t\t.fetchUserInfo(this.transport)\n\t\t\t.then((session) => {\n\t\t\t\tif (!this.disposed) {\n\t\t\t\t\tthis.stateSignal.set({\n\t\t\t\t\t\tstatus: session\n\t\t\t\t\t\t\t? SessionContextControllerStatus.Authenticated\n\t\t\t\t\t\t\t: SessionContextControllerStatus.Unauthenticated,\n\t\t\t\t\t\tsession,\n\t\t\t\t\t\terror: null,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn session;\n\t\t\t})\n\t\t\t.catch((error: unknown) => {\n\t\t\t\tif (!this.disposed) {\n\t\t\t\t\tthis.stateSignal.set({\n\t\t\t\t\t\tstatus: SessionContextControllerStatus.Error,\n\t\t\t\t\t\tsession: null,\n\t\t\t\t\t\terror,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t})\n\t\t\t.finally(() => {\n\t\t\t\tthis.refreshPromise = null;\n\t\t\t});\n\n\t\treturn this.refreshPromise;\n\t}\n\n\tasync rememberPostAuthRedirect(uri: string): Promise<void> {\n\t\tawait this.client.rememberPostAuthRedirect(uri);\n\t}\n\n\tasync clearPostAuthRedirect(): Promise<void> {\n\t\tawait this.client.clearPostAuthRedirect();\n\t}\n\n\tasync resolveLoginUrl(): Promise<string> {\n\t\treturn await this.client.resolveLoginUrl();\n\t}\n\n\tasync logout(): Promise<void> {\n\t\tthis.stateSignal.set({\n\t\t\tstatus: SessionContextControllerStatus.Loading,\n\t\t\tsession: this.stateSignal.get().session,\n\t\t\terror: null,\n\t\t});\n\t\ttry {\n\t\t\tawait this.client.logoutAndClearPendingLoginRedirect(this.transport);\n\t\t\tif (!this.disposed) {\n\t\t\t\tthis.stateSignal.set({\n\t\t\t\t\tstatus: SessionContextControllerStatus.Unauthenticated,\n\t\t\t\t\tsession: null,\n\t\t\t\t\terror: null,\n\t\t\t\t});\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (!this.disposed) {\n\t\t\t\tthis.stateSignal.set({\n\t\t\t\t\tstatus: SessionContextControllerStatus.Error,\n\t\t\t\t\tsession: null,\n\t\t\t\t\terror,\n\t\t\t\t});\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tthis.disposed = true;\n\t}\n}\n"],"mappings":";;;AASA,MAAa,iCAAiC;CAC7C,MAAM;CACN,SAAS;CACT,eAAe;CACf,iBAAiB;CACjB,OAAO;CACP;AAgBD,IAAa,2BAAb,MAAsC;CACrC;CACA;CAEA;CACA,cAA+B,aAA4C;EAC1E,QAAQ,+BAA+B;EACvC,SAAS;EACT,OAAO;EACP,CAAC;CACF,iBAA6D;CAC7D,WAAmB;CAEnB,YAAY,SAA0C;EACrD,KAAK,SAAS,QAAQ;EACtB,KAAK,YAAY,QAAQ;EACzB,KAAK,QAAQ,eAAe,KAAK,YAAY;;CAG9C,WAA0C;EACzC,OAAO,KAAK,YAAY,KAAK;;CAG9B,UAAU,UAAkC;EAC3C,OAAO,KAAK,YAAY,UAAU,SAAS;;CAG5C,UAAuC;EACtC,IAAI,KAAK,UACR,OAAO,QAAQ,QAAQ,KAAK,YAAY,KAAK,CAAC,QAAQ;EAEvD,IAAI,KAAK,gBACR,OAAO,KAAK;EAGb,KAAK,YAAY,IAAI;GACpB,QAAQ,+BAA+B;GACvC,SAAS,KAAK,YAAY,KAAK,CAAC;GAChC,OAAO;GACP,CAAC;EAEF,KAAK,iBAAiB,KAAK,OACzB,cAAc,KAAK,UAAU,CAC7B,MAAM,YAAY;GAClB,IAAI,CAAC,KAAK,UACT,KAAK,YAAY,IAAI;IACpB,QAAQ,UACL,+BAA+B,gBAC/B,+BAA+B;IAClC;IACA,OAAO;IACP,CAAC;GAEH,OAAO;IACN,CACD,OAAO,UAAmB;GAC1B,IAAI,CAAC,KAAK,UACT,KAAK,YAAY,IAAI;IACpB,QAAQ,+BAA+B;IACvC,SAAS;IACT;IACA,CAAC;GAEH,MAAM;IACL,CACD,cAAc;GACd,KAAK,iBAAiB;IACrB;EAEH,OAAO,KAAK;;CAGb,MAAM,yBAAyB,KAA4B;EAC1D,MAAM,KAAK,OAAO,yBAAyB,IAAI;;CAGhD,MAAM,wBAAuC;EAC5C,MAAM,KAAK,OAAO,uBAAuB;;CAG1C,MAAM,kBAAmC;EACxC,OAAO,MAAM,KAAK,OAAO,iBAAiB;;CAG3C,MAAM,SAAwB;EAC7B,KAAK,YAAY,IAAI;GACpB,QAAQ,+BAA+B;GACvC,SAAS,KAAK,YAAY,KAAK,CAAC;GAChC,OAAO;GACP,CAAC;EACF,IAAI;GACH,MAAM,KAAK,OAAO,mCAAmC,KAAK,UAAU;GACpE,IAAI,CAAC,KAAK,UACT,KAAK,YAAY,IAAI;IACpB,QAAQ,+BAA+B;IACvC,SAAS;IACT,OAAO;IACP,CAAC;WAEK,OAAO;GACf,IAAI,CAAC,KAAK,UACT,KAAK,YAAY,IAAI;IACpB,QAAQ,+BAA+B;IACvC,SAAS;IACT;IACA,CAAC;GAEH,MAAM;;;CAIR,UAAgB;EACf,KAAK,WAAW"}
@@ -1,4 +1,4 @@
1
- import { i as SessionInfo, n as SessionContextClientConfig, t as SessionContextClient } from "../client-Dzfx3BXp.mjs";
1
+ import { i as SessionInfo, n as SessionContextClientConfig, t as SessionContextClient } from "../client-BRqKhOuj.mjs";
2
2
  import { HttpTransport } from "@securitydept/client";
3
3
 
4
4
  //#region src/server/helpers.d.ts
@@ -1,4 +1,4 @@
1
- import { t as SessionContextClient } from "../client-BcZv4X8b.mjs";
1
+ import { t as SessionContextClient } from "../client-CTJyzp37.mjs";
2
2
  //#region src/server/helpers.ts
3
3
  /**
4
4
  * Create a server-host helper for session-based auth.
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/server/helpers.ts"],"sourcesContent":["// Server-host helpers for session-context-client\n//\n// Provides host-neutral server helpers that wrap SessionContextClient\n// for SSR / server-render-host scenarios (Next.js, Remix, Astro,\n// plain Node request handlers, etc.).\n//\n// Key design:\n// - The host provides incoming request headers (cookies, forwarded-for, etc.)\n// - The helper creates a cookie-forwarding transport wrapper\n// - fetchUserInfo, login URL, logout URL are all server-host-safe\n\nimport type { HttpTransport } from \"@securitydept/client\";\nimport { SessionContextClient } from \"../client\";\nimport type { SessionContextClientConfig, SessionInfo } from \"../types\";\n\n// ---------------------------------------------------------------------------\n// Server request context\n// ---------------------------------------------------------------------------\n\n/** Minimal server request context — host-neutral. */\nexport interface ServerRequestContext {\n\t/**\n\t * Incoming request headers from the server host.\n\t *\n\t * The helper uses `cookie` (and optionally other headers) to build\n\t * a forwarding transport. The host controls which headers to pass.\n\t */\n\theaders: Record<string, string>;\n}\n\n// ---------------------------------------------------------------------------\n// Helper options\n// ---------------------------------------------------------------------------\n\n/** Options for {@link createSessionServerHelper}. */\nexport interface CreateSessionServerHelperOptions {\n\t/** SessionContextClient config — same shape as the browser client. */\n\tconfig: SessionContextClientConfig;\n\t/**\n\t * The underlying HTTP transport used for server-side requests.\n\t *\n\t * The helper wraps this transport to inject forwarded headers.\n\t * Typically a simple `fetch`-based transport.\n\t */\n\ttransport: HttpTransport;\n}\n\n// ---------------------------------------------------------------------------\n// Helper interface\n// ---------------------------------------------------------------------------\n\n/** Server-host helper for session-based auth in SSR contexts. */\nexport interface SessionServerHelper {\n\t/** The underlying SessionContextClient instance. */\n\treadonly client: SessionContextClient;\n\n\t/**\n\t * Fetch the current user session using forwarded request headers.\n\t *\n\t * Returns `SessionInfo` if authenticated, `null` if not.\n\t * The host's cookies are forwarded via the transport.\n\t */\n\tfetchUserInfo(context: ServerRequestContext): Promise<SessionInfo | null>;\n\n\t/** Build the login URL (server-host-safe, no browser globals). */\n\tloginUrl(postAuthRedirectUri?: string): string;\n\n\t/** Build the logout URL (server-host-safe, no browser globals). */\n\tlogoutUrl(): string;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a server-host helper for session-based auth.\n *\n * @example\n * ```ts\n * const helper = createSessionServerHelper({\n * config: { baseUrl: \"https://auth.example.com\" },\n * transport: fetchTransport, // your HTTP transport\n * });\n *\n * // In a server request handler:\n * const session = await helper.fetchUserInfo({\n * headers: { cookie: req.headers.cookie ?? \"\" },\n * });\n *\n * if (!session) {\n * return Response.redirect(helper.loginUrl(\"/protected\"), 302);\n * }\n * ```\n */\nexport function createSessionServerHelper(\n\toptions: CreateSessionServerHelperOptions,\n): SessionServerHelper {\n\tconst client = new SessionContextClient(options.config);\n\tconst baseTransport = options.transport;\n\n\treturn {\n\t\tclient,\n\n\t\tasync fetchUserInfo(\n\t\t\tcontext: ServerRequestContext,\n\t\t): Promise<SessionInfo | null> {\n\t\t\t// Create a forwarding transport that injects the request headers.\n\t\t\tconst forwardingTransport: HttpTransport = {\n\t\t\t\tasync execute(request) {\n\t\t\t\t\treturn baseTransport.execute({\n\t\t\t\t\t\t...request,\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t...context.headers,\n\t\t\t\t\t\t\t...request.headers,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t};\n\n\t\t\treturn client.fetchUserInfo(forwardingTransport);\n\t\t},\n\n\t\tloginUrl(postAuthRedirectUri?: string): string {\n\t\t\treturn client.loginUrl(postAuthRedirectUri);\n\t\t},\n\n\t\tlogoutUrl(): string {\n\t\t\treturn client.logoutUrl();\n\t\t},\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA+FA,SAAgB,0BACf,SACsB;CACtB,MAAM,SAAS,IAAI,qBAAqB,QAAQ,OAAO;CACvD,MAAM,gBAAgB,QAAQ;AAE9B,QAAO;EACN;EAEA,MAAM,cACL,SAC8B;AAc9B,UAAO,OAAO,cAZ6B,EAC1C,MAAM,QAAQ,SAAS;AACtB,WAAO,cAAc,QAAQ;KAC5B,GAAG;KACH,SAAS;MACR,GAAG,QAAQ;MACX,GAAG,QAAQ;MACX;KACD,CAAC;MAEH,CAE+C;;EAGjD,SAAS,qBAAsC;AAC9C,UAAO,OAAO,SAAS,oBAAoB;;EAG5C,YAAoB;AACnB,UAAO,OAAO,WAAW;;EAE1B"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/server/helpers.ts"],"sourcesContent":["// Server-host helpers for session-context-client\n//\n// Provides host-neutral server helpers that wrap SessionContextClient\n// for SSR / server-render-host scenarios (Next.js, Remix, Astro,\n// plain Node request handlers, etc.).\n//\n// Key design:\n// - The host provides incoming request headers (cookies, forwarded-for, etc.)\n// - The helper creates a cookie-forwarding transport wrapper\n// - fetchUserInfo, login URL, logout URL are all server-host-safe\n\nimport type { HttpTransport } from \"@securitydept/client\";\nimport { SessionContextClient } from \"../client\";\nimport type { SessionContextClientConfig, SessionInfo } from \"../types\";\n\n// ---------------------------------------------------------------------------\n// Server request context\n// ---------------------------------------------------------------------------\n\n/** Minimal server request context — host-neutral. */\nexport interface ServerRequestContext {\n\t/**\n\t * Incoming request headers from the server host.\n\t *\n\t * The helper uses `cookie` (and optionally other headers) to build\n\t * a forwarding transport. The host controls which headers to pass.\n\t */\n\theaders: Record<string, string>;\n}\n\n// ---------------------------------------------------------------------------\n// Helper options\n// ---------------------------------------------------------------------------\n\n/** Options for {@link createSessionServerHelper}. */\nexport interface CreateSessionServerHelperOptions {\n\t/** SessionContextClient config — same shape as the browser client. */\n\tconfig: SessionContextClientConfig;\n\t/**\n\t * The underlying HTTP transport used for server-side requests.\n\t *\n\t * The helper wraps this transport to inject forwarded headers.\n\t * Typically a simple `fetch`-based transport.\n\t */\n\ttransport: HttpTransport;\n}\n\n// ---------------------------------------------------------------------------\n// Helper interface\n// ---------------------------------------------------------------------------\n\n/** Server-host helper for session-based auth in SSR contexts. */\nexport interface SessionServerHelper {\n\t/** The underlying SessionContextClient instance. */\n\treadonly client: SessionContextClient;\n\n\t/**\n\t * Fetch the current user session using forwarded request headers.\n\t *\n\t * Returns `SessionInfo` if authenticated, `null` if not.\n\t * The host's cookies are forwarded via the transport.\n\t */\n\tfetchUserInfo(context: ServerRequestContext): Promise<SessionInfo | null>;\n\n\t/** Build the login URL (server-host-safe, no browser globals). */\n\tloginUrl(postAuthRedirectUri?: string): string;\n\n\t/** Build the logout URL (server-host-safe, no browser globals). */\n\tlogoutUrl(): string;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a server-host helper for session-based auth.\n *\n * @example\n * ```ts\n * const helper = createSessionServerHelper({\n * config: { baseUrl: \"https://auth.example.com\" },\n * transport: fetchTransport, // your HTTP transport\n * });\n *\n * // In a server request handler:\n * const session = await helper.fetchUserInfo({\n * headers: { cookie: req.headers.cookie ?? \"\" },\n * });\n *\n * if (!session) {\n * return Response.redirect(helper.loginUrl(\"/protected\"), 302);\n * }\n * ```\n */\nexport function createSessionServerHelper(\n\toptions: CreateSessionServerHelperOptions,\n): SessionServerHelper {\n\tconst client = new SessionContextClient(options.config);\n\tconst baseTransport = options.transport;\n\n\treturn {\n\t\tclient,\n\n\t\tasync fetchUserInfo(\n\t\t\tcontext: ServerRequestContext,\n\t\t): Promise<SessionInfo | null> {\n\t\t\t// Create a forwarding transport that injects the request headers.\n\t\t\tconst forwardingTransport: HttpTransport = {\n\t\t\t\tasync execute(request) {\n\t\t\t\t\treturn baseTransport.execute({\n\t\t\t\t\t\t...request,\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t...context.headers,\n\t\t\t\t\t\t\t...request.headers,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t};\n\n\t\t\treturn client.fetchUserInfo(forwardingTransport);\n\t\t},\n\n\t\tloginUrl(postAuthRedirectUri?: string): string {\n\t\t\treturn client.loginUrl(postAuthRedirectUri);\n\t\t},\n\n\t\tlogoutUrl(): string {\n\t\t\treturn client.logoutUrl();\n\t\t},\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA+FA,SAAgB,0BACf,SACsB;CACtB,MAAM,SAAS,IAAI,qBAAqB,QAAQ,OAAO;CACvD,MAAM,gBAAgB,QAAQ;CAE9B,OAAO;EACN;EAEA,MAAM,cACL,SAC8B;GAc9B,OAAO,OAAO,cAAc,EAX3B,MAAM,QAAQ,SAAS;IACtB,OAAO,cAAc,QAAQ;KAC5B,GAAG;KACH,SAAS;MACR,GAAG,QAAQ;MACX,GAAG,QAAQ;MACX;KACD,CAAC;MAI2C,CAAC;;EAGjD,SAAS,qBAAsC;GAC9C,OAAO,OAAO,SAAS,oBAAoB;;EAG5C,YAAoB;GACnB,OAAO,OAAO,WAAW;;EAE1B"}
@@ -1,14 +1,16 @@
1
- import { t as SessionContextClient } from "../client-Dzfx3BXp.mjs";
1
+ import { t as SessionContextClient } from "../client-BRqKhOuj.mjs";
2
+ import { PageLocationCapability } from "@securitydept/client";
2
3
 
3
4
  //#region src/web/index.d.ts
4
5
  /**
5
6
  * Options for {@link loginWithRedirect}.
6
7
  */
7
8
  interface LoginWithRedirectOptions {
9
+ environment?: PageLocationCapability;
8
10
  /**
9
11
  * Where to redirect the user after successful authentication.
10
12
  *
11
- * When omitted, `window.location.href` is used as the return URI and
13
+ * When omitted, `environment.location.href` is used as the return URI and
12
14
  * persisted via the client's pending-login-redirect store for post-auth
13
15
  * consumption.
14
16
  */
@@ -1,4 +1,6 @@
1
+ import { assertResolveEnvironment } from "@securitydept/client/web";
1
2
  //#region src/web/index.ts
3
+ const SESSION_PAGE_ENVIRONMENT_ERROR_MESSAGE = "session browser redirect helpers require an explicit page environment.\nCreate one in your composition root with createBrowserPageClientEnvironment(...).";
2
4
  /**
3
5
  * One-shot browser redirect to the session login endpoint.
4
6
  *
@@ -12,9 +14,13 @@
12
14
  * host-neutral client contract.
13
15
  */
14
16
  async function loginWithRedirect(client, options = {}) {
15
- const postAuthRedirectUri = options.postAuthRedirectUri ?? window.location.href;
17
+ const environment = assertResolveEnvironment(options.environment, failMissingPageEnvironment);
18
+ const postAuthRedirectUri = options.postAuthRedirectUri ?? environment.location.href;
16
19
  await client.savePendingLoginRedirect(postAuthRedirectUri);
17
- window.location.href = client.loginUrl(postAuthRedirectUri);
20
+ environment.location.href = client.loginUrl(postAuthRedirectUri);
21
+ }
22
+ function failMissingPageEnvironment() {
23
+ throw new Error(SESSION_PAGE_ENVIRONMENT_ERROR_MESSAGE);
18
24
  }
19
25
  //#endregion
20
26
  export { loginWithRedirect };
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/web/index.ts"],"sourcesContent":["// Session Context Client — Browser Adapter\n//\n// Canonical import path:\n// import { loginWithRedirect } from \"@securitydept/session-context-client/web\"\n//\n// Provides browser-specific convenience helpers that trigger full-page\n// navigation (redirect) for session-based authentication. The root\n// `SessionContextClient` stays host-neutral; this subpath owns the\n// browser side-effect layer.\n//\n// Stability: provisional\n\nimport type { SessionContextClient } from \"../client\";\n\n/**\n * Options for {@link loginWithRedirect}.\n */\nexport interface LoginWithRedirectOptions {\n\t/**\n\t * Where to redirect the user after successful authentication.\n\t *\n\t * When omitted, `window.location.href` is used as the return URI and\n\t * persisted via the client's pending-login-redirect store for post-auth\n\t * consumption.\n\t */\n\tpostAuthRedirectUri?: string;\n}\n\n/**\n * One-shot browser redirect to the session login endpoint.\n *\n * 1. Saves the post-auth redirect intent into the client's pending store\n * (when a session store is configured).\n * 2. Navigates the current window to the login URL.\n *\n * This is the recommended browser entry point for initiating session-based\n * login. It is intentionally a standalone function (not a method on\n * `SessionContextClient`) to keep browser-specific side-effects out of the\n * host-neutral client contract.\n */\nexport async function loginWithRedirect(\n\tclient: SessionContextClient,\n\toptions: LoginWithRedirectOptions = {},\n): Promise<void> {\n\tconst postAuthRedirectUri =\n\t\toptions.postAuthRedirectUri ?? window.location.href;\n\n\tawait client.savePendingLoginRedirect(postAuthRedirectUri);\n\n\twindow.location.href = client.loginUrl(postAuthRedirectUri);\n}\n"],"mappings":";;;;;;;;;;;;;AAwCA,eAAsB,kBACrB,QACA,UAAoC,EAAE,EACtB;CAChB,MAAM,sBACL,QAAQ,uBAAuB,OAAO,SAAS;AAEhD,OAAM,OAAO,yBAAyB,oBAAoB;AAE1D,QAAO,SAAS,OAAO,OAAO,SAAS,oBAAoB"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/web/index.ts"],"sourcesContent":["// Session Context Client — Browser Adapter\n//\n// Canonical import path:\n// import { loginWithRedirect } from \"@securitydept/session-context-client/web\"\n//\n// Provides browser-specific convenience helpers that trigger full-page\n// navigation (redirect) for session-based authentication. The root\n// `SessionContextClient` stays host-neutral; this subpath owns the\n// browser side-effect layer.\n//\n// Stability: provisional\n\nimport type { PageLocationCapability } from \"@securitydept/client\";\nimport { assertResolveEnvironment } from \"@securitydept/client/web\";\nimport type { SessionContextClient } from \"../client\";\n\nconst SESSION_PAGE_ENVIRONMENT_ERROR_MESSAGE =\n\t\"session browser redirect helpers require an explicit page environment.\\n\" +\n\t\"Create one in your composition root with createBrowserPageClientEnvironment(...).\";\n\n/**\n * Options for {@link loginWithRedirect}.\n */\nexport interface LoginWithRedirectOptions {\n\tenvironment?: PageLocationCapability;\n\n\t/**\n\t * Where to redirect the user after successful authentication.\n\t *\n\t * When omitted, `environment.location.href` is used as the return URI and\n\t * persisted via the client's pending-login-redirect store for post-auth\n\t * consumption.\n\t */\n\tpostAuthRedirectUri?: string;\n}\n\n/**\n * One-shot browser redirect to the session login endpoint.\n *\n * 1. Saves the post-auth redirect intent into the client's pending store\n * (when a session store is configured).\n * 2. Navigates the current window to the login URL.\n *\n * This is the recommended browser entry point for initiating session-based\n * login. It is intentionally a standalone function (not a method on\n * `SessionContextClient`) to keep browser-specific side-effects out of the\n * host-neutral client contract.\n */\nexport async function loginWithRedirect(\n\tclient: SessionContextClient,\n\toptions: LoginWithRedirectOptions = {},\n): Promise<void> {\n\tconst environment = assertResolveEnvironment(\n\t\toptions.environment,\n\t\tfailMissingPageEnvironment,\n\t);\n\tconst postAuthRedirectUri =\n\t\toptions.postAuthRedirectUri ?? environment.location.href;\n\n\tawait client.savePendingLoginRedirect(postAuthRedirectUri);\n\n\tenvironment.location.href = client.loginUrl(postAuthRedirectUri);\n}\n\nfunction failMissingPageEnvironment(): never {\n\tthrow new Error(SESSION_PAGE_ENVIRONMENT_ERROR_MESSAGE);\n}\n"],"mappings":";;AAgBA,MAAM,yCACL;;;;;;;;;;;;;AA+BD,eAAsB,kBACrB,QACA,UAAoC,EAAE,EACtB;CAChB,MAAM,cAAc,yBACnB,QAAQ,aACR,2BACA;CACD,MAAM,sBACL,QAAQ,uBAAuB,YAAY,SAAS;CAErD,MAAM,OAAO,yBAAyB,oBAAoB;CAE1D,YAAY,SAAS,OAAO,OAAO,SAAS,oBAAoB;;AAGjE,SAAS,6BAAoC;CAC5C,MAAM,IAAI,MAAM,uCAAuC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securitydept/session-context-client",
3
- "version": "0.3.0-beta.1",
3
+ "version": "0.3.0-beta.3",
4
4
  "description": "Session Context Client of the SecurityDept TypeScript SDK, a layered authentication and authorization toolkit built as reusable Rust crates, a TypeScript client SDK workspace, and reference server/web apps.",
5
5
  "author": "Zhou Yeheng <root@aitiotekt.com>",
6
6
  "license": "MIT",
@@ -45,10 +45,10 @@
45
45
  "dist"
46
46
  ],
47
47
  "dependencies": {
48
- "@securitydept/client": "0.3.0-beta.1"
48
+ "@securitydept/client": "0.3.0-beta.3"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "tsdown",
52
- "typecheck": "tsc -b"
52
+ "typecheck": "tsc -b --force"
53
53
  }
54
54
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"client-BcZv4X8b.mjs","names":[],"sources":["../src/schemas.ts","../src/types.ts","../src/client.ts"],"sourcesContent":["// Session /auth/session/user-info response schemas — shared principal validation\n//\n// These schemas validate the two accepted shapes of the session user-info\n// response:\n// 1. Internal (camelCase):\n// { principal: { subject, displayName, picture?, issuer?, claims? } }\n// 2. Rust server wire (snake_case):\n// { subject, display_name, picture?, issuer?, claims? }\n\nimport {\n\tcreateSchema,\n\tnormalizeAuthenticatedPrincipal,\n\tnormalizeAuthenticatedPrincipalWire,\n} from \"@securitydept/client\";\nimport type { SessionInfo, SessionPrincipal } from \"./types\";\n\n/**\n * Schema for the canonical SessionInfo shape (camelCase principal).\n *\n * This is the preferred shape once normalized.\n */\nexport const SessionInfoSchema = createSchema<SessionInfo>({\n\tvalidate(input: unknown) {\n\t\tif (\n\t\t\ttypeof input === \"object\" &&\n\t\t\tinput !== null &&\n\t\t\t\"principal\" in input &&\n\t\t\ttypeof (input as Record<string, unknown>).principal === \"object\" &&\n\t\t\t(input as Record<string, unknown>).principal !== null\n\t\t) {\n\t\t\tconst principal = normalizeAuthenticatedPrincipal(\n\t\t\t\t(input as { principal: Record<string, unknown> }).principal,\n\t\t\t);\n\t\t\tif (principal) {\n\t\t\t\treturn { value: { principal } };\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\tissues: [\n\t\t\t\t{\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t\"Expected SessionInfo with principal.subject and principal.displayName\",\n\t\t\t\t\tpath: [\"principal\", \"subject\"],\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t},\n});\n\n/**\n * Schema for the Rust server session user-info response shape.\n *\n * Validates the real `/auth/session/user-info` wire payload and normalizes it\n * into SessionInfo in one step.\n */\nexport const SessionUserInfoResponseSchema = createSchema<SessionInfo>({\n\tvalidate(input: unknown) {\n\t\tif (\n\t\t\ttypeof input === \"object\" &&\n\t\t\tinput !== null &&\n\t\t\t\"subject\" in input &&\n\t\t\ttypeof (input as Record<string, unknown>).subject === \"string\"\n\t\t) {\n\t\t\tconst raw = input as Record<string, unknown>;\n\t\t\tconst principal = normalizeAuthenticatedPrincipalWire(raw);\n\t\t\tif (principal) {\n\t\t\t\treturn { value: { principal: principal as SessionPrincipal } };\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\tissues: [\n\t\t\t\t{\n\t\t\t\t\tmessage: \"Expected server UserInfo response with subject (string)\",\n\t\t\t\t\tpath: [\"subject\"],\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t},\n});\n","import type { AuthenticatedPrincipal } from \"@securitydept/client\";\n\n// --- Session Context Client types ---\n\nexport const SessionContextSource = {\n\tSessionContext: \"session-context\",\n} as const;\n\nexport type SessionContextSource =\n\t(typeof SessionContextSource)[keyof typeof SessionContextSource];\n\n/** Session principal — shared authenticated principal semantics via @securitydept/client. */\nexport type SessionPrincipal = AuthenticatedPrincipal;\n\n/** Session info returned from the server. */\nexport interface SessionInfo {\n\tprincipal: SessionPrincipal;\n\tattributes?: Record<string, unknown>;\n\textra?: Record<string, unknown>;\n}\n\n/** Configuration for the Session Context Client. */\nexport interface SessionContextClientConfig {\n\t/** Base URL of the SecurityDept server. */\n\tbaseUrl: string;\n\t/** Login path (default: \"/login\"). */\n\tloginPath?: string;\n\t/** Logout path (default: \"/logout\"). */\n\tlogoutPath?: string;\n\t/** User info endpoint path (default: \"/auth/session/user-info\"). */\n\tuserInfoPath?: string;\n\t/** Name of the query parameter for post-auth redirect (default: \"post_auth_redirect_uri\"). */\n\tpostAuthRedirectParam?: string;\n\t/** Optional key used with `runtime.sessionStore` for pending login redirect state. */\n\tloginRedirectStateKey?: string;\n}\n","import type {\n\tCancellationTokenTrait,\n\tClientRuntime,\n\tEphemeralFlowStore,\n\tHttpTransport,\n} from \"@securitydept/client\";\nimport {\n\tClientError,\n\tClientErrorKind,\n\tcreateEphemeralFlowStore,\n\tvalidateWithSchemaSync,\n} from \"@securitydept/client\";\nimport { SessionInfoSchema, SessionUserInfoResponseSchema } from \"./schemas\";\nimport {\n\ttype SessionContextClientConfig,\n\tSessionContextSource,\n\ttype SessionInfo,\n} from \"./types\";\n\nconst DEFAULT_LOGIN_PATH = \"/auth/session/login\";\nconst DEFAULT_LOGOUT_PATH = \"/auth/session/logout\";\nconst DEFAULT_USER_INFO_PATH = \"/auth/session/user-info\";\nconst DEFAULT_POST_AUTH_REDIRECT_PARAM = \"post_auth_redirect_uri\";\nconst DEFAULT_LOGIN_REDIRECT_STATE_KEY =\n\t\"securitydept.session_context.pending_login_redirect\";\n\n/**\n * Session Context Client.\n *\n * Provides login/logout URL construction, session probing via `/user-info`, and\n * minimal session-scoped flow state for login redirect intent.\n */\nexport class SessionContextClient {\n\tprivate readonly _baseUrl: string;\n\tprivate readonly _loginPath: string;\n\tprivate readonly _logoutPath: string;\n\tprivate readonly _userInfoPath: string;\n\tprivate readonly _postAuthRedirectParam: string;\n\tprivate readonly _pendingLoginRedirectStore?: EphemeralFlowStore<string>;\n\tprivate readonly _loginRedirectStateKey: string;\n\n\tconstructor(\n\t\tconfig: SessionContextClientConfig,\n\t\truntime: Pick<ClientRuntime, \"sessionStore\"> = {},\n\t) {\n\t\tthis._baseUrl = config.baseUrl.replace(/\\/+$/, \"\");\n\t\tthis._loginPath = config.loginPath ?? DEFAULT_LOGIN_PATH;\n\t\tthis._logoutPath = config.logoutPath ?? DEFAULT_LOGOUT_PATH;\n\t\tthis._userInfoPath = config.userInfoPath ?? DEFAULT_USER_INFO_PATH;\n\t\tthis._postAuthRedirectParam =\n\t\t\tconfig.postAuthRedirectParam ?? DEFAULT_POST_AUTH_REDIRECT_PARAM;\n\t\tthis._loginRedirectStateKey =\n\t\t\tconfig.loginRedirectStateKey ??\n\t\t\t`${DEFAULT_LOGIN_REDIRECT_STATE_KEY}:${this._baseUrl || \"relative\"}`;\n\t\tthis._pendingLoginRedirectStore = runtime.sessionStore\n\t\t\t? createEphemeralFlowStore<string>({\n\t\t\t\t\tstore: runtime.sessionStore,\n\t\t\t\t\tkey: this._loginRedirectStateKey,\n\t\t\t\t\tcodec: {\n\t\t\t\t\t\tencode(value) {\n\t\t\t\t\t\t\treturn value;\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdecode(raw) {\n\t\t\t\t\t\t\treturn raw;\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t: undefined;\n\t}\n\n\t/** Build the login URL, optionally with a post-auth redirect. */\n\tloginUrl(postAuthRedirectUri?: string): string {\n\t\tconst base = this._baseUrl + this._loginPath;\n\t\tif (postAuthRedirectUri) {\n\t\t\tconst params = new URLSearchParams({\n\t\t\t\t[this._postAuthRedirectParam]: postAuthRedirectUri,\n\t\t\t});\n\t\t\treturn `${base}?${params.toString()}`;\n\t\t}\n\t\treturn base;\n\t}\n\n\t/** Build the logout URL. */\n\tlogoutUrl(): string {\n\t\treturn this._baseUrl + this._logoutPath;\n\t}\n\n\t/** Save the pending post-auth redirect using the canonical browser-shell vocabulary. */\n\tasync rememberPostAuthRedirect(postAuthRedirectUri: string): Promise<void> {\n\t\tawait this.savePendingLoginRedirect(postAuthRedirectUri);\n\t}\n\n\t/** Save the pending post-auth redirect in `sessionStore` when available. */\n\tasync savePendingLoginRedirect(postAuthRedirectUri: string): Promise<void> {\n\t\tif (!this._pendingLoginRedirectStore) {\n\t\t\treturn;\n\t\t}\n\n\t\tawait this._pendingLoginRedirectStore.save(postAuthRedirectUri);\n\t}\n\n\t/** Load the pending post-auth redirect from `sessionStore`. */\n\tasync loadPendingLoginRedirect(): Promise<string | null> {\n\t\tif (!this._pendingLoginRedirectStore) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn await this._pendingLoginRedirectStore.load();\n\t}\n\n\t/** Load and consume the pending post-auth redirect from `sessionStore`. */\n\tasync consumePendingLoginRedirect(): Promise<string | null> {\n\t\tif (!this._pendingLoginRedirectStore) {\n\t\t\treturn null;\n\t\t}\n\t\treturn await this._pendingLoginRedirectStore.consume();\n\t}\n\n\t/** Clear the pending post-auth redirect from `sessionStore`. */\n\tasync clearPendingLoginRedirect(): Promise<void> {\n\t\tif (!this._pendingLoginRedirectStore) {\n\t\t\treturn;\n\t\t}\n\n\t\tawait this._pendingLoginRedirectStore.clear();\n\t}\n\n\t/** Clear the pending post-auth redirect using the canonical browser-shell vocabulary. */\n\tasync clearPostAuthRedirect(): Promise<void> {\n\t\tawait this.clearPendingLoginRedirect();\n\t}\n\n\t/** Resolve the next login URL by consuming any pending post-auth redirect intent. */\n\tasync resolveLoginUrl(): Promise<string> {\n\t\tconst pendingRedirect = await this.consumePendingLoginRedirect();\n\t\treturn this.loginUrl(pendingRedirect ?? undefined);\n\t}\n\n\t/**\n\t * Fetch the current session info from `/user-info`.\n\t *\n\t * Returns `null` only for 401/403 (unauthenticated).\n\t * Throws `ClientError` for any other non-2xx response to distinguish\n\t * server failures from unauthenticated state.\n\t */\n\tasync fetchUserInfo(\n\t\ttransport: HttpTransport,\n\t\tcancellationToken?: CancellationTokenTrait,\n\t): Promise<SessionInfo | null> {\n\t\tconst response = await transport.execute({\n\t\t\turl: this._baseUrl + this._userInfoPath,\n\t\t\tmethod: \"GET\",\n\t\t\theaders: {},\n\t\t\tcancellationToken,\n\t\t});\n\n\t\tif (response.status === 401 || response.status === 403) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (response.status >= 200 && response.status < 300) {\n\t\t\treturn normalizeSessionInfo(response.body);\n\t\t}\n\n\t\t// Non-2xx, non-401/403 — this is a server error, not \"unauthenticated\".\n\t\tthrow ClientError.fromHttpResponse(response.status, response.body);\n\t}\n\n\t/** Check whether a session exists on the server. */\n\tasync isAuthenticated(\n\t\ttransport: HttpTransport,\n\t\tcancellationToken?: CancellationTokenTrait,\n\t): Promise<boolean> {\n\t\tconst me = await this.fetchUserInfo(transport, cancellationToken);\n\t\treturn me !== null;\n\t}\n\n\t/** Execute logout against the configured session logout endpoint. */\n\tasync logout(\n\t\ttransport: HttpTransport,\n\t\tcancellationToken?: CancellationTokenTrait,\n\t): Promise<void> {\n\t\tconst response = await transport.execute({\n\t\t\turl: this.logoutUrl(),\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"content-type\": \"application/json\" },\n\t\t\tbody: JSON.stringify({}),\n\t\t\tcancellationToken,\n\t\t});\n\n\t\tif (response.status >= 200 && response.status < 300) {\n\t\t\treturn;\n\t\t}\n\n\t\tthrow ClientError.fromHttpResponse(response.status, response.body);\n\t}\n\n\t/** Execute logout and clear any stale post-auth redirect intent in one canonical step. */\n\tasync logoutAndClearPendingLoginRedirect(\n\t\ttransport: HttpTransport,\n\t\tcancellationToken?: CancellationTokenTrait,\n\t): Promise<void> {\n\t\tawait this.logout(transport, cancellationToken);\n\t\tawait this.clearPendingLoginRedirect();\n\t}\n}\n\n/**\n * Normalize a /me response body using @standard-schema aligned schemas.\n *\n * Tries the canonical SessionInfo shape first, then falls back to the\n * server-side snake_case UserInfo shape. Throws a protocol-level ClientError\n * if neither schema matches.\n */\nfunction normalizeSessionInfo(body: unknown): SessionInfo {\n\t// Try canonical camelCase shape first.\n\tconst infoResult = validateWithSchemaSync(SessionInfoSchema, body);\n\tif (infoResult.success) {\n\t\treturn infoResult.value;\n\t}\n\n\t// Try server-side snake_case shape.\n\tconst userInfoResult = validateWithSchemaSync(\n\t\tSessionUserInfoResponseSchema,\n\t\tbody,\n\t);\n\tif (userInfoResult.success) {\n\t\treturn userInfoResult.value;\n\t}\n\n\tthrow new ClientError({\n\t\tkind: ClientErrorKind.Protocol,\n\t\tcode: \"session.invalid_user_info_payload\",\n\t\tmessage: \"Session /user-info payload is invalid\",\n\t\tsource: SessionContextSource.SessionContext,\n\t});\n}\n"],"mappings":"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"}