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.
- package/README.md +88 -0
- package/dist/assistant-events.d.ts +7 -0
- package/dist/assistant-events.js +78 -0
- package/dist/auth.d.ts +56 -5
- package/dist/auth.js +247 -30
- package/dist/browser/lemma-client.js +283 -53
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +1 -1
- package/dist/client.d.ts +4 -1
- package/dist/client.js +3 -3
- package/dist/index.d.ts +8 -2
- package/dist/index.js +4 -1
- package/dist/namespaces/desks.d.ts +2 -2
- package/dist/namespaces/desks.js +7 -7
- package/dist/namespaces/icons.js +4 -1
- package/dist/openapi_client/index.d.ts +10 -7
- package/dist/openapi_client/models/{AgentNode.d.ts → AgentNode_Input.d.ts} +1 -1
- package/dist/openapi_client/models/AgentNode_Output.d.ts +11 -0
- package/dist/openapi_client/models/AppDescriptorResponse.d.ts +2 -2
- package/dist/openapi_client/models/Body_upload_file_files__resource_type___resource_id__upload_post.d.ts +1 -1
- package/dist/openapi_client/models/{Body_file_upload.d.ts → DatastoreFileUploadRequest.d.ts} +2 -2
- package/dist/openapi_client/models/{DecisionNode.d.ts → DecisionNode_Input.d.ts} +1 -1
- package/dist/openapi_client/models/DecisionNode_Output.d.ts +11 -0
- package/dist/openapi_client/models/DeskBundleUploadRequest.d.ts +4 -0
- package/dist/openapi_client/models/FlowEntity.d.ts +4 -4
- package/dist/openapi_client/models/{FunctionNode.d.ts → FunctionNode_Input.d.ts} +1 -1
- package/dist/openapi_client/models/FunctionNode_Output.d.ts +11 -0
- package/dist/openapi_client/models/FunctionNode_Output.js +1 -0
- package/dist/openapi_client/models/IconUploadRequest.d.ts +3 -0
- package/dist/openapi_client/models/IconUploadRequest.js +1 -0
- package/dist/openapi_client/models/ValidationError.d.ts +2 -0
- package/dist/openapi_client/models/WorkflowGraphUpdateRequest.d.ts +4 -4
- package/dist/openapi_client/models/{Body_file_update.d.ts → update.d.ts} +2 -2
- package/dist/openapi_client/models/update.js +1 -0
- package/dist/openapi_client/services/ApplicationsService.d.ts +2 -2
- package/dist/openapi_client/services/ApplicationsService.js +3 -3
- package/dist/openapi_client/services/DesksService.d.ts +9 -9
- package/dist/openapi_client/services/DesksService.js +8 -8
- package/dist/openapi_client/services/FilesService.d.ts +4 -4
- package/dist/openapi_client/services/IconsService.d.ts +2 -2
- package/dist/openapi_client/services/PublicSdkService.d.ts +1 -1
- package/dist/openapi_client/services/PublicSdkService.js +1 -1
- package/dist/react/index.d.ts +12 -0
- package/dist/react/index.js +6 -0
- package/dist/react/useAssistantRun.d.ts +1 -1
- package/dist/react/useAssistantRun.js +23 -69
- package/dist/react/useAssistantRuntime.d.ts +13 -0
- package/dist/react/useAssistantRuntime.js +108 -0
- package/dist/react/useAssistantSession.d.ts +61 -0
- package/dist/react/useAssistantSession.js +278 -0
- package/dist/react/useAuth.d.ts +6 -2
- package/dist/react/useAuth.js +1 -1
- package/dist/react/useFlowRunHistory.d.ts +19 -0
- package/dist/react/useFlowRunHistory.js +77 -0
- package/dist/react/useFlowSession.d.ts +39 -0
- package/dist/react/useFlowSession.js +195 -0
- package/dist/react/useFunctionSession.d.ts +32 -0
- package/dist/react/useFunctionSession.js +147 -0
- package/dist/react/useTaskSession.d.ts +35 -0
- package/dist/react/useTaskSession.js +207 -0
- package/dist/run-utils.d.ts +18 -0
- package/dist/run-utils.js +62 -0
- package/dist/task-events.d.ts +7 -0
- package/dist/task-events.js +78 -0
- package/dist/types.d.ts +3 -1
- package/package.json +1 -1
- package/dist/openapi_client/models/Body_icon_upload.d.ts +0 -3
- package/dist/openapi_client/models/Body_pod_desk_bundle_upload.d.ts +0 -4
- /package/dist/openapi_client/models/{AgentNode.js → AgentNode_Input.js} +0 -0
- /package/dist/openapi_client/models/{Body_file_update.js → AgentNode_Output.js} +0 -0
- /package/dist/openapi_client/models/{Body_file_upload.js → DatastoreFileUploadRequest.js} +0 -0
- /package/dist/openapi_client/models/{Body_icon_upload.js → DecisionNode_Input.js} +0 -0
- /package/dist/openapi_client/models/{Body_pod_desk_bundle_upload.js → DecisionNode_Output.js} +0 -0
- /package/dist/openapi_client/models/{DecisionNode.js → DeskBundleUploadRequest.js} +0 -0
- /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.
|
|
7
|
-
* 2.
|
|
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)
|
|
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(
|
|
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.
|
|
7
|
-
* 2.
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
38
|
-
|
|
31
|
+
}
|
|
32
|
+
function writeStorageToken(token) {
|
|
33
|
+
if (typeof window === "undefined")
|
|
34
|
+
return;
|
|
39
35
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
localStorage.setItem(LOCALSTORAGE_TOKEN_KEY, token);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// ignore storage errors
|
|
43
40
|
}
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
}
|
|
42
|
+
function removeStorageToken() {
|
|
43
|
+
if (typeof window === "undefined")
|
|
44
|
+
return;
|
|
46
45
|
try {
|
|
47
|
-
|
|
48
|
-
if (stored)
|
|
49
|
-
return stored;
|
|
46
|
+
localStorage.removeItem(LOCALSTORAGE_TOKEN_KEY);
|
|
50
47
|
}
|
|
51
|
-
catch {
|
|
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 =
|
|
164
|
-
window.location.href =
|
|
380
|
+
const redirectUri = options.redirectUri ?? window.location.href;
|
|
381
|
+
window.location.href = this.getAuthUrl({ ...options, redirectUri });
|
|
165
382
|
}
|
|
166
383
|
}
|