lemma-sdk 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +88 -0
  2. package/dist/assistant-events.d.ts +7 -0
  3. package/dist/assistant-events.js +78 -0
  4. package/dist/auth.d.ts +56 -5
  5. package/dist/auth.js +247 -30
  6. package/dist/browser/lemma-client.js +283 -53
  7. package/dist/browser.d.ts +1 -1
  8. package/dist/browser.js +1 -1
  9. package/dist/client.d.ts +4 -1
  10. package/dist/client.js +3 -3
  11. package/dist/index.d.ts +8 -2
  12. package/dist/index.js +4 -1
  13. package/dist/namespaces/desks.d.ts +2 -2
  14. package/dist/namespaces/desks.js +7 -7
  15. package/dist/namespaces/icons.js +4 -1
  16. package/dist/openapi_client/index.d.ts +10 -7
  17. package/dist/openapi_client/models/{AgentNode.d.ts → AgentNode_Input.d.ts} +1 -1
  18. package/dist/openapi_client/models/AgentNode_Output.d.ts +11 -0
  19. package/dist/openapi_client/models/AppDescriptorResponse.d.ts +2 -2
  20. package/dist/openapi_client/models/Body_upload_file_files__resource_type___resource_id__upload_post.d.ts +1 -1
  21. package/dist/openapi_client/models/{Body_file_upload.d.ts → DatastoreFileUploadRequest.d.ts} +2 -2
  22. package/dist/openapi_client/models/{DecisionNode.d.ts → DecisionNode_Input.d.ts} +1 -1
  23. package/dist/openapi_client/models/DecisionNode_Output.d.ts +11 -0
  24. package/dist/openapi_client/models/DeskBundleUploadRequest.d.ts +4 -0
  25. package/dist/openapi_client/models/FlowEntity.d.ts +4 -4
  26. package/dist/openapi_client/models/{FunctionNode.d.ts → FunctionNode_Input.d.ts} +1 -1
  27. package/dist/openapi_client/models/FunctionNode_Output.d.ts +11 -0
  28. package/dist/openapi_client/models/FunctionNode_Output.js +1 -0
  29. package/dist/openapi_client/models/IconUploadRequest.d.ts +3 -0
  30. package/dist/openapi_client/models/IconUploadRequest.js +1 -0
  31. package/dist/openapi_client/models/ValidationError.d.ts +2 -0
  32. package/dist/openapi_client/models/WorkflowGraphUpdateRequest.d.ts +4 -4
  33. package/dist/openapi_client/models/{Body_file_update.d.ts → update.d.ts} +2 -2
  34. package/dist/openapi_client/models/update.js +1 -0
  35. package/dist/openapi_client/services/ApplicationsService.d.ts +2 -2
  36. package/dist/openapi_client/services/ApplicationsService.js +3 -3
  37. package/dist/openapi_client/services/DesksService.d.ts +9 -9
  38. package/dist/openapi_client/services/DesksService.js +8 -8
  39. package/dist/openapi_client/services/FilesService.d.ts +4 -4
  40. package/dist/openapi_client/services/IconsService.d.ts +2 -2
  41. package/dist/openapi_client/services/PublicSdkService.d.ts +1 -1
  42. package/dist/openapi_client/services/PublicSdkService.js +1 -1
  43. package/dist/react/index.d.ts +12 -0
  44. package/dist/react/index.js +6 -0
  45. package/dist/react/useAssistantRun.d.ts +1 -1
  46. package/dist/react/useAssistantRun.js +23 -69
  47. package/dist/react/useAssistantRuntime.d.ts +13 -0
  48. package/dist/react/useAssistantRuntime.js +108 -0
  49. package/dist/react/useAssistantSession.d.ts +61 -0
  50. package/dist/react/useAssistantSession.js +278 -0
  51. package/dist/react/useAuth.d.ts +6 -2
  52. package/dist/react/useAuth.js +1 -1
  53. package/dist/react/useFlowRunHistory.d.ts +19 -0
  54. package/dist/react/useFlowRunHistory.js +77 -0
  55. package/dist/react/useFlowSession.d.ts +39 -0
  56. package/dist/react/useFlowSession.js +195 -0
  57. package/dist/react/useFunctionSession.d.ts +32 -0
  58. package/dist/react/useFunctionSession.js +147 -0
  59. package/dist/react/useTaskSession.d.ts +35 -0
  60. package/dist/react/useTaskSession.js +207 -0
  61. package/dist/run-utils.d.ts +18 -0
  62. package/dist/run-utils.js +62 -0
  63. package/dist/task-events.d.ts +7 -0
  64. package/dist/task-events.js +78 -0
  65. package/dist/types.d.ts +3 -1
  66. package/package.json +1 -1
  67. package/dist/openapi_client/models/Body_icon_upload.d.ts +0 -3
  68. package/dist/openapi_client/models/Body_pod_desk_bundle_upload.d.ts +0 -4
  69. /package/dist/openapi_client/models/{AgentNode.js → AgentNode_Input.js} +0 -0
  70. /package/dist/openapi_client/models/{Body_file_update.js → AgentNode_Output.js} +0 -0
  71. /package/dist/openapi_client/models/{Body_file_upload.js → DatastoreFileUploadRequest.js} +0 -0
  72. /package/dist/openapi_client/models/{Body_icon_upload.js → DecisionNode_Input.js} +0 -0
  73. /package/dist/openapi_client/models/{Body_pod_desk_bundle_upload.js → DecisionNode_Output.js} +0 -0
  74. /package/dist/openapi_client/models/{DecisionNode.js → DeskBundleUploadRequest.js} +0 -0
  75. /package/dist/openapi_client/models/{FunctionNode.js → FunctionNode_Input.js} +0 -0
package/README.md CHANGED
@@ -8,6 +8,12 @@ Official TypeScript SDK for Lemma APIs with pod-scoped namespaces, auth helpers,
8
8
  npm i lemma-sdk
9
9
  ```
10
10
 
11
+ For local workspace development against the checked-out SDK instead of npm:
12
+
13
+ ```bash
14
+ npm i file:../lemma-typescript
15
+ ```
16
+
11
17
  If you want to import as `lemma`, use npm aliasing:
12
18
 
13
19
  ```bash
@@ -39,6 +45,69 @@ const supportAssistant = await client.assistants.get("support_assistant");
39
45
  - `client.request(method, path, options)` escape hatch for endpoints not yet modeled.
40
46
  - `client.resources` for generic file resource APIs (`conversation`, `assistant`, `task`, etc.).
41
47
  - Ergonomic type aliases exported at top level: `Agent`, `Assistant`, `Conversation`, `Task`, `TaskMessage`, `CreateAgentInput`, `CreateAssistantInput`, etc.
48
+ - `client.withPod(podId)` returns a pod-scoped client that shares auth state with the parent client.
49
+
50
+ ## Auth Helpers
51
+
52
+ ```ts
53
+ import { LemmaClient, buildAuthUrl, resolveSafeRedirectUri } from "lemma-sdk";
54
+
55
+ const client = new LemmaClient({
56
+ apiUrl: "https://api-next.asur.work",
57
+ authUrl: "https://auth.asur.work/auth",
58
+ });
59
+
60
+ // Build auth URLs (server/client)
61
+ const loginUrl = buildAuthUrl(client.authUrl, { redirectUri: "https://app.asur.work/" });
62
+ const signupUrl = buildAuthUrl(client.authUrl, { mode: "signup", redirectUri: "https://app.asur.work/" });
63
+
64
+ // Redirect safety helper for auth route handlers
65
+ const safeRedirect = resolveSafeRedirectUri("/pod/123", {
66
+ siteOrigin: "https://app.asur.work",
67
+ fallback: "/",
68
+ });
69
+
70
+ // Browser helpers
71
+ await client.auth.checkAuth();
72
+ await client.auth.signOut();
73
+ const token = await client.auth.getAccessToken();
74
+ const refreshed = await client.auth.refreshAccessToken();
75
+ client.auth.redirectToAuth({ mode: "signup", redirectUri: safeRedirect });
76
+ ```
77
+
78
+ ### Browser Testing With Injected Token
79
+
80
+ For desk and app testing, the SDK supports a fixed bearer token injected through localStorage.
81
+ This is the only supported browser token-injection path.
82
+
83
+ ```ts
84
+ import { LemmaClient, setTestingToken, clearTestingToken } from "lemma-sdk";
85
+
86
+ setTestingToken("<access-token>");
87
+
88
+ const client = new LemmaClient({
89
+ apiUrl: "/api",
90
+ authUrl: "http://localhost:4173",
91
+ podId: "<pod-id>",
92
+ });
93
+
94
+ await client.initialize();
95
+
96
+ clearTestingToken();
97
+ ```
98
+
99
+ Equivalent manual browser setup:
100
+
101
+ ```js
102
+ localStorage.setItem("lemma_token", "<access-token>");
103
+ window.location.reload();
104
+ ```
105
+
106
+ Notes:
107
+
108
+ - do not pass testing tokens in query parameters
109
+ - prefer a same-origin dev proxy such as Vite `/api` during local browser testing to avoid CORS on `/users/me`
110
+ - production auth should use the normal cookie/session flow
42
111
 
43
112
  ## Assistants + Agent Runs
44
113
 
@@ -104,6 +173,21 @@ Import from `lemma-sdk/react`:
104
173
  - `AuthGuard`
105
174
  - `useAgentRunStream(...)`
106
175
  - `useAssistantRun(...)`
176
+ - `useAssistantSession(...)`
177
+ - `useTaskSession(...)`
178
+ - `useFunctionSession(...)`
179
+ - `useFlowSession(...)`
180
+
181
+ Core run helpers from `lemma-sdk`:
182
+
183
+ - `normalizeRunStatus(...)`
184
+ - `isTerminalTaskStatus(...)`
185
+ - `isTerminalFunctionStatus(...)`
186
+ - `isTerminalFlowStatus(...)`
187
+ - `parseTaskStreamEvent(...)`
188
+ - `upsertTaskMessage(...)`
189
+ - `parseAssistantStreamEvent(...)`
190
+ - `upsertConversationMessage(...)`
107
191
 
108
192
  Example:
109
193
 
@@ -120,6 +204,10 @@ const { sendMessage, stop, isStreaming } = useAssistantRun({
120
204
  });
121
205
  ```
122
206
 
207
+ For the SDK consumption UI roadmap (AssistantChat / FunctionInvokeForm / FlowRunExperience / RunPanel), see:
208
+
209
+ - `docs/sdk-consumption-ui-v2.md`
210
+
123
211
  ## File Resources
124
212
 
125
213
  ```ts
@@ -0,0 +1,7 @@
1
+ import type { ConversationMessage } from "./types.js";
2
+ export interface ParsedAssistantStreamEvent {
3
+ message?: ConversationMessage;
4
+ status?: string;
5
+ }
6
+ export declare function parseAssistantStreamEvent(value: unknown): ParsedAssistantStreamEvent;
7
+ export declare function upsertConversationMessage(messages: ConversationMessage[], incoming: ConversationMessage): ConversationMessage[];
@@ -0,0 +1,78 @@
1
+ function isRecord(value) {
2
+ return !!value && typeof value === "object" && !Array.isArray(value);
3
+ }
4
+ function normalizeStatus(status) {
5
+ if (typeof status !== "string")
6
+ return undefined;
7
+ const normalized = status.trim().toUpperCase();
8
+ return normalized.length > 0 ? normalized : undefined;
9
+ }
10
+ function toConversationMessage(value) {
11
+ if (!isRecord(value))
12
+ return undefined;
13
+ if (typeof value.id !== "string")
14
+ return undefined;
15
+ if (typeof value.role !== "string")
16
+ return undefined;
17
+ if (!("content" in value))
18
+ return undefined;
19
+ const message = {
20
+ id: value.id,
21
+ role: value.role,
22
+ content: value.content,
23
+ created_at: typeof value.created_at === "string" ? value.created_at : new Date().toISOString(),
24
+ metadata: isRecord(value.metadata) ? value.metadata : null,
25
+ };
26
+ return message;
27
+ }
28
+ function extractPayload(record) {
29
+ if ("data" in record)
30
+ return record.data;
31
+ if ("payload" in record)
32
+ return record.payload;
33
+ return undefined;
34
+ }
35
+ function extractStatus(payload) {
36
+ if (isRecord(payload)) {
37
+ return normalizeStatus(payload.status)
38
+ ?? normalizeStatus(payload.conversation_status)
39
+ ?? normalizeStatus(payload.run_status)
40
+ ?? (isRecord(payload.conversation) ? normalizeStatus(payload.conversation.status) : undefined);
41
+ }
42
+ return normalizeStatus(payload);
43
+ }
44
+ export function parseAssistantStreamEvent(value) {
45
+ const directMessage = toConversationMessage(value);
46
+ if (directMessage) {
47
+ return { message: directMessage };
48
+ }
49
+ if (!isRecord(value)) {
50
+ return {};
51
+ }
52
+ const eventType = typeof value.type === "string" ? value.type.toLowerCase() : "";
53
+ const payload = extractPayload(value);
54
+ if (eventType === "message" || eventType === "message_added") {
55
+ const message = toConversationMessage(payload);
56
+ return message ? { message } : {};
57
+ }
58
+ if (eventType === "status"
59
+ || eventType === "conversation_status"
60
+ || eventType === "conversation_updated"
61
+ || eventType === "run_status") {
62
+ const status = extractStatus(payload);
63
+ return status ? { status } : {};
64
+ }
65
+ return {};
66
+ }
67
+ export function upsertConversationMessage(messages, incoming) {
68
+ const next = [...messages];
69
+ const index = next.findIndex((message) => message.id === incoming.id);
70
+ if (index >= 0) {
71
+ next[index] = incoming;
72
+ }
73
+ else {
74
+ next.push(incoming);
75
+ }
76
+ next.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
77
+ return next;
78
+ }
package/dist/auth.d.ts CHANGED
@@ -3,11 +3,10 @@
3
3
  * for agent/dev testing.
4
4
  *
5
5
  * Auth resolution order on init:
6
- * 1. ?lemma_token=<token> query param (stored in memory for session)
7
- * 2. localStorage.getItem("lemma_token")
8
- * 3. Session cookie (credentials: "include") — production path
6
+ * 1. localStorage.getItem("lemma_token")
7
+ * 2. Session cookie (credentials: "include") — production path
9
8
  *
10
- * If a token is found in (1) or (2), all requests use Authorization: Bearer <token>.
9
+ * If a token is found in (1), all requests use Authorization: Bearer <token>.
11
10
  * Otherwise requests rely on cookies, and the server must set the session cookie
12
11
  * after the user authenticates at the auth service. In cookie mode we initialise
13
12
  * the SuperTokens browser SDK so fetch/XHR automatically handles anti-CSRF and
@@ -28,6 +27,30 @@ export interface AuthState {
28
27
  user: UserInfo | null;
29
28
  }
30
29
  export type AuthListener = (state: AuthState) => void;
30
+ export type AuthRedirectMode = "login" | "signup";
31
+ export interface BuildAuthUrlOptions {
32
+ /** Optional auth path segment relative to authUrl pathname, e.g. "callback" -> /auth/callback. */
33
+ path?: string;
34
+ /** Adds signup mode query, preserving existing params. */
35
+ mode?: AuthRedirectMode;
36
+ /** Redirect URI passed to auth service. */
37
+ redirectUri?: string;
38
+ /** Additional query parameters appended to auth URL. */
39
+ params?: Record<string, string | number | boolean | Array<string | number | boolean> | null | undefined>;
40
+ }
41
+ export interface ResolveSafeRedirectUriOptions {
42
+ /** Origin for resolving relative paths. */
43
+ siteOrigin: string;
44
+ /** Fallback path or URL when input is empty/invalid/blocked. Defaults to "/". */
45
+ fallback?: string;
46
+ /** Local paths blocked as redirect targets to avoid auth loops. */
47
+ blockedPaths?: string[];
48
+ }
49
+ export declare function setTestingToken(token: string): void;
50
+ export declare function getTestingToken(): string | null;
51
+ export declare function clearTestingToken(): void;
52
+ export declare function buildAuthUrl(authUrl: string, options?: BuildAuthUrlOptions): string;
53
+ export declare function resolveSafeRedirectUri(rawValue: string | null | undefined, options: ResolveSafeRedirectUriOptions): string;
31
54
  export declare class AuthManager {
32
55
  private readonly apiUrl;
33
56
  private readonly authUrl;
@@ -47,6 +70,23 @@ export declare class AuthManager {
47
70
  subscribe(listener: AuthListener): () => void;
48
71
  private notify;
49
72
  private setState;
73
+ private assertBrowserContext;
74
+ private getCookie;
75
+ private clearInjectedToken;
76
+ private rawSignOutViaBackend;
77
+ /**
78
+ * Check whether a cookie-backed session is active without mutating auth state.
79
+ */
80
+ isAuthenticatedViaCookie(): Promise<boolean>;
81
+ /**
82
+ * Return a browser access token from the session layer.
83
+ * Throws if no token is available.
84
+ */
85
+ getAccessToken(): Promise<string>;
86
+ /**
87
+ * Force a refresh-token flow and return the new access token.
88
+ */
89
+ refreshAccessToken(): Promise<string>;
50
90
  /**
51
91
  * Build request headers for an API call.
52
92
  * Uses Bearer token if one was injected, otherwise omits Authorization
@@ -63,10 +103,21 @@ export declare class AuthManager {
63
103
  * Does NOT redirect — call redirectToAuth() explicitly if desired.
64
104
  */
65
105
  markUnauthenticated(): void;
106
+ /**
107
+ * Sign out the current user session.
108
+ * Returns true when the session is no longer active.
109
+ */
110
+ signOut(): Promise<boolean>;
111
+ /**
112
+ * Build auth URL for login/signup/custom auth sub-path.
113
+ */
114
+ getAuthUrl(options?: BuildAuthUrlOptions): string;
66
115
  /**
67
116
  * Redirect to the auth service, passing the current URL as redirect_uri.
68
117
  * After the user authenticates, the auth service should redirect back to
69
118
  * the original URL and set the session cookie.
70
119
  */
71
- redirectToAuth(): void;
120
+ redirectToAuth(options?: Omit<BuildAuthUrlOptions, "redirectUri"> & {
121
+ redirectUri?: string;
122
+ }): void;
72
123
  }
package/dist/auth.js CHANGED
@@ -3,11 +3,10 @@
3
3
  * for agent/dev testing.
4
4
  *
5
5
  * Auth resolution order on init:
6
- * 1. ?lemma_token=<token> query param (stored in memory for session)
7
- * 2. localStorage.getItem("lemma_token")
8
- * 3. Session cookie (credentials: "include") — production path
6
+ * 1. localStorage.getItem("lemma_token")
7
+ * 2. Session cookie (credentials: "include") — production path
9
8
  *
10
- * If a token is found in (1) or (2), all requests use Authorization: Bearer <token>.
9
+ * If a token is found in (1), all requests use Authorization: Bearer <token>.
11
10
  * Otherwise requests rely on cookies, and the server must set the session cookie
12
11
  * after the user authenticates at the auth service. In cookie mode we initialise
13
12
  * the SuperTokens browser SDK so fetch/XHR automatically handles anti-CSRF and
@@ -16,41 +15,131 @@
16
15
  * Auth state is determined by calling GET /users/me (user.current.get).
17
16
  * 401 → unauthenticated. 200 → authenticated.
18
17
  */
18
+ import Session from "supertokens-web-js/recipe/session";
19
19
  import { ensureCookieSessionSupport } from "./supertokens.js";
20
+ const DEFAULT_BLOCKED_REDIRECT_PATHS = ["/login", "/signup", "/auth"];
20
21
  const LOCALSTORAGE_TOKEN_KEY = "lemma_token";
21
- const QUERY_PARAM_TOKEN_KEY = "lemma_token";
22
- function detectInjectedToken() {
22
+ function readStorageToken() {
23
23
  if (typeof window === "undefined")
24
24
  return null;
25
- // 1. Query param — highest priority, persist to sessionStorage for this session
26
25
  try {
27
- const params = new URLSearchParams(window.location.search);
28
- const qpToken = params.get(QUERY_PARAM_TOKEN_KEY);
29
- if (qpToken) {
30
- try {
31
- sessionStorage.setItem(LOCALSTORAGE_TOKEN_KEY, qpToken);
32
- }
33
- catch { /* ignore */ }
34
- return qpToken;
35
- }
26
+ return localStorage.getItem(LOCALSTORAGE_TOKEN_KEY);
27
+ }
28
+ catch {
29
+ return null;
36
30
  }
37
- catch { /* ignore */ }
38
- // 2. sessionStorage — survives HMR and same-tab navigation
31
+ }
32
+ function writeStorageToken(token) {
33
+ if (typeof window === "undefined")
34
+ return;
39
35
  try {
40
- const stored = sessionStorage.getItem(LOCALSTORAGE_TOKEN_KEY);
41
- if (stored)
42
- return stored;
36
+ localStorage.setItem(LOCALSTORAGE_TOKEN_KEY, token);
37
+ }
38
+ catch {
39
+ // ignore storage errors
43
40
  }
44
- catch { /* ignore */ }
45
- // 3. localStorage — set manually by dev/agent for persistent testing
41
+ }
42
+ function removeStorageToken() {
43
+ if (typeof window === "undefined")
44
+ return;
46
45
  try {
47
- const stored = localStorage.getItem(LOCALSTORAGE_TOKEN_KEY);
48
- if (stored)
49
- return stored;
46
+ localStorage.removeItem(LOCALSTORAGE_TOKEN_KEY);
50
47
  }
51
- catch { /* ignore */ }
48
+ catch {
49
+ // ignore storage errors
50
+ }
51
+ }
52
+ export function setTestingToken(token) {
53
+ writeStorageToken(token);
54
+ }
55
+ export function getTestingToken() {
56
+ return readStorageToken();
57
+ }
58
+ export function clearTestingToken() {
59
+ removeStorageToken();
60
+ }
61
+ function detectInjectedToken() {
62
+ if (typeof window === "undefined")
63
+ return null;
64
+ // 1. localStorage — the only supported browser testing path
65
+ const localToken = readStorageToken();
66
+ if (localToken)
67
+ return localToken;
52
68
  return null;
53
69
  }
70
+ function normalizePath(path) {
71
+ const trimmed = path.trim();
72
+ if (!trimmed)
73
+ return "/";
74
+ if (trimmed === "/")
75
+ return "/";
76
+ const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
77
+ return withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
78
+ }
79
+ function resolveAuthPath(basePath, path) {
80
+ const normalizedBase = normalizePath(basePath);
81
+ if (!path || !path.trim()) {
82
+ return normalizedBase;
83
+ }
84
+ const segment = path.trim().replace(/^\/+/, "");
85
+ if (!segment) {
86
+ return normalizedBase;
87
+ }
88
+ return `${normalizedBase}/${segment}`.replace(/\/{2,}/g, "/");
89
+ }
90
+ function isBlockedLocalPath(pathname, blockedPaths) {
91
+ const normalizedPathname = normalizePath(pathname);
92
+ return blockedPaths.some((rawBlockedPath) => {
93
+ const blockedPath = normalizePath(rawBlockedPath);
94
+ return normalizedPathname === blockedPath || normalizedPathname.startsWith(`${blockedPath}/`);
95
+ });
96
+ }
97
+ function normalizeOrigin(rawOrigin) {
98
+ const parsed = new URL(rawOrigin);
99
+ return parsed.origin;
100
+ }
101
+ export function buildAuthUrl(authUrl, options = {}) {
102
+ const url = new URL(authUrl);
103
+ url.pathname = resolveAuthPath(url.pathname, options.path);
104
+ for (const [key, value] of Object.entries(options.params ?? {})) {
105
+ if (value === null || value === undefined)
106
+ continue;
107
+ if (Array.isArray(value)) {
108
+ url.searchParams.delete(key);
109
+ for (const item of value) {
110
+ url.searchParams.append(key, String(item));
111
+ }
112
+ continue;
113
+ }
114
+ url.searchParams.set(key, String(value));
115
+ }
116
+ if (options.mode === "signup") {
117
+ url.searchParams.set("show", "signup");
118
+ }
119
+ if (options.redirectUri && options.redirectUri.trim()) {
120
+ url.searchParams.set("redirect_uri", options.redirectUri);
121
+ }
122
+ return url.toString();
123
+ }
124
+ export function resolveSafeRedirectUri(rawValue, options) {
125
+ const siteOrigin = normalizeOrigin(options.siteOrigin);
126
+ const blockedPaths = options.blockedPaths ?? DEFAULT_BLOCKED_REDIRECT_PATHS;
127
+ const fallbackTarget = options.fallback ?? "/";
128
+ const fallback = new URL(fallbackTarget, siteOrigin).toString();
129
+ if (!rawValue || !rawValue.trim()) {
130
+ return fallback;
131
+ }
132
+ try {
133
+ const parsed = new URL(rawValue, siteOrigin);
134
+ if (parsed.origin === siteOrigin && isBlockedLocalPath(parsed.pathname, blockedPaths)) {
135
+ return fallback;
136
+ }
137
+ return parsed.toString();
138
+ }
139
+ catch {
140
+ return fallback;
141
+ }
142
+ }
54
143
  export class AuthManager {
55
144
  apiUrl;
56
145
  authUrl;
@@ -93,6 +182,96 @@ export class AuthManager {
93
182
  this.state = state;
94
183
  this.notify();
95
184
  }
185
+ assertBrowserContext() {
186
+ if (typeof window === "undefined") {
187
+ throw new Error("This auth method is only available in browser environments.");
188
+ }
189
+ }
190
+ getCookie(name) {
191
+ if (typeof document === "undefined")
192
+ return undefined;
193
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
194
+ const match = document.cookie.match(new RegExp(`(?:^|; )${escaped}=([^;]*)`));
195
+ return match ? decodeURIComponent(match[1]) : undefined;
196
+ }
197
+ clearInjectedToken() {
198
+ this.injectedToken = null;
199
+ clearTestingToken();
200
+ }
201
+ async rawSignOutViaBackend() {
202
+ const antiCsrf = this.getCookie("sAntiCsrf");
203
+ const headers = {
204
+ Accept: "application/json",
205
+ "Content-Type": "application/json",
206
+ rid: "anti-csrf",
207
+ "fdi-version": "4.2",
208
+ "st-auth-mode": "cookie",
209
+ };
210
+ if (antiCsrf) {
211
+ headers["anti-csrf"] = antiCsrf;
212
+ }
213
+ const separator = this.apiUrl.includes("?") ? "&" : "?";
214
+ const signOutUrl = `${this.apiUrl.replace(/\/$/, "")}/st/auth/signout${separator}superTokensDoNotDoInterception=true`;
215
+ await fetch(signOutUrl, {
216
+ method: "POST",
217
+ credentials: "include",
218
+ headers,
219
+ });
220
+ }
221
+ /**
222
+ * Check whether a cookie-backed session is active without mutating auth state.
223
+ */
224
+ async isAuthenticatedViaCookie() {
225
+ if (this.injectedToken) {
226
+ return this.isAuthenticated();
227
+ }
228
+ try {
229
+ const response = await fetch(`${this.apiUrl}/users/me`, {
230
+ method: "GET",
231
+ credentials: "include",
232
+ headers: { Accept: "application/json" },
233
+ });
234
+ return response.status !== 401;
235
+ }
236
+ catch {
237
+ return false;
238
+ }
239
+ }
240
+ /**
241
+ * Return a browser access token from the session layer.
242
+ * Throws if no token is available.
243
+ */
244
+ async getAccessToken() {
245
+ if (this.injectedToken) {
246
+ return this.injectedToken;
247
+ }
248
+ this.assertBrowserContext();
249
+ ensureCookieSessionSupport(this.apiUrl, () => this.markUnauthenticated());
250
+ const token = await Session.getAccessToken();
251
+ if (!token) {
252
+ throw new Error("Token unavailable");
253
+ }
254
+ return token;
255
+ }
256
+ /**
257
+ * Force a refresh-token flow and return the new access token.
258
+ */
259
+ async refreshAccessToken() {
260
+ if (this.injectedToken) {
261
+ return this.injectedToken;
262
+ }
263
+ this.assertBrowserContext();
264
+ ensureCookieSessionSupport(this.apiUrl, () => this.markUnauthenticated());
265
+ const refreshed = await Session.attemptRefreshingSession();
266
+ if (!refreshed) {
267
+ throw new Error("Session refresh failed");
268
+ }
269
+ const token = await Session.getAccessToken();
270
+ if (!token) {
271
+ throw new Error("Token unavailable");
272
+ }
273
+ return token;
274
+ }
96
275
  /**
97
276
  * Build request headers for an API call.
98
277
  * Uses Bearer token if one was injected, otherwise omits Authorization
@@ -151,16 +330,54 @@ export class AuthManager {
151
330
  markUnauthenticated() {
152
331
  this.setState({ status: "unauthenticated", user: null });
153
332
  }
333
+ /**
334
+ * Sign out the current user session.
335
+ * Returns true when the session is no longer active.
336
+ */
337
+ async signOut() {
338
+ if (this.injectedToken) {
339
+ this.clearInjectedToken();
340
+ this.markUnauthenticated();
341
+ return true;
342
+ }
343
+ this.assertBrowserContext();
344
+ ensureCookieSessionSupport(this.apiUrl, () => this.markUnauthenticated());
345
+ try {
346
+ await Session.signOut();
347
+ }
348
+ catch {
349
+ // continue with raw fallback
350
+ }
351
+ if (await this.isAuthenticatedViaCookie()) {
352
+ try {
353
+ await this.rawSignOutViaBackend();
354
+ }
355
+ catch {
356
+ // best effort fallback only
357
+ }
358
+ }
359
+ const isAuthenticated = await this.isAuthenticatedViaCookie();
360
+ if (!isAuthenticated) {
361
+ this.markUnauthenticated();
362
+ }
363
+ return !isAuthenticated;
364
+ }
365
+ /**
366
+ * Build auth URL for login/signup/custom auth sub-path.
367
+ */
368
+ getAuthUrl(options = {}) {
369
+ return buildAuthUrl(this.authUrl, options);
370
+ }
154
371
  /**
155
372
  * Redirect to the auth service, passing the current URL as redirect_uri.
156
373
  * After the user authenticates, the auth service should redirect back to
157
374
  * the original URL and set the session cookie.
158
375
  */
159
- redirectToAuth() {
376
+ redirectToAuth(options = {}) {
160
377
  if (typeof window === "undefined") {
161
378
  return;
162
379
  }
163
- const redirectUri = encodeURIComponent(window.location.href);
164
- window.location.href = `${this.authUrl}?redirect_uri=${redirectUri}`;
380
+ const redirectUri = options.redirectUri ?? window.location.href;
381
+ window.location.href = this.getAuthUrl({ ...options, redirectUri });
165
382
  }
166
383
  }