lemma-sdk 0.2.3 → 0.2.4
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 +29 -0
- package/dist/auth.d.ts +50 -1
- package/dist/auth.js +219 -3
- package/dist/browser/lemma-client.js +227 -7
- 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 +2 -2
- package/dist/index.js +1 -1
- package/dist/react/useAuth.d.ts +6 -2
- package/dist/react/useAuth.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,6 +39,35 @@ const supportAssistant = await client.assistants.get("support_assistant");
|
|
|
39
39
|
- `client.request(method, path, options)` escape hatch for endpoints not yet modeled.
|
|
40
40
|
- `client.resources` for generic file resource APIs (`conversation`, `assistant`, `task`, etc.).
|
|
41
41
|
- Ergonomic type aliases exported at top level: `Agent`, `Assistant`, `Conversation`, `Task`, `TaskMessage`, `CreateAgentInput`, `CreateAssistantInput`, etc.
|
|
42
|
+
- `client.withPod(podId)` returns a pod-scoped client that shares auth state with the parent client.
|
|
43
|
+
|
|
44
|
+
## Auth Helpers
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { LemmaClient, buildAuthUrl, resolveSafeRedirectUri } from "lemma-sdk";
|
|
48
|
+
|
|
49
|
+
const client = new LemmaClient({
|
|
50
|
+
apiUrl: "https://api-next.asur.work",
|
|
51
|
+
authUrl: "https://auth.asur.work/auth",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Build auth URLs (server/client)
|
|
55
|
+
const loginUrl = buildAuthUrl(client.authUrl, { redirectUri: "https://app.asur.work/" });
|
|
56
|
+
const signupUrl = buildAuthUrl(client.authUrl, { mode: "signup", redirectUri: "https://app.asur.work/" });
|
|
57
|
+
|
|
58
|
+
// Redirect safety helper for auth route handlers
|
|
59
|
+
const safeRedirect = resolveSafeRedirectUri("/pod/123", {
|
|
60
|
+
siteOrigin: "https://app.asur.work",
|
|
61
|
+
fallback: "/",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Browser helpers
|
|
65
|
+
await client.auth.checkAuth();
|
|
66
|
+
await client.auth.signOut();
|
|
67
|
+
const token = await client.auth.getAccessToken();
|
|
68
|
+
const refreshed = await client.auth.refreshAccessToken();
|
|
69
|
+
client.auth.redirectToAuth({ mode: "signup", redirectUri: safeRedirect });
|
|
70
|
+
```
|
|
42
71
|
|
|
43
72
|
## Assistants + Agent Runs
|
|
44
73
|
|
package/dist/auth.d.ts
CHANGED
|
@@ -28,6 +28,27 @@ export interface AuthState {
|
|
|
28
28
|
user: UserInfo | null;
|
|
29
29
|
}
|
|
30
30
|
export type AuthListener = (state: AuthState) => void;
|
|
31
|
+
export type AuthRedirectMode = "login" | "signup";
|
|
32
|
+
export interface BuildAuthUrlOptions {
|
|
33
|
+
/** Optional auth path segment relative to authUrl pathname, e.g. "callback" -> /auth/callback. */
|
|
34
|
+
path?: string;
|
|
35
|
+
/** Adds signup mode query, preserving existing params. */
|
|
36
|
+
mode?: AuthRedirectMode;
|
|
37
|
+
/** Redirect URI passed to auth service. */
|
|
38
|
+
redirectUri?: string;
|
|
39
|
+
/** Additional query parameters appended to auth URL. */
|
|
40
|
+
params?: Record<string, string | number | boolean | Array<string | number | boolean> | null | undefined>;
|
|
41
|
+
}
|
|
42
|
+
export interface ResolveSafeRedirectUriOptions {
|
|
43
|
+
/** Origin for resolving relative paths. */
|
|
44
|
+
siteOrigin: string;
|
|
45
|
+
/** Fallback path or URL when input is empty/invalid/blocked. Defaults to "/". */
|
|
46
|
+
fallback?: string;
|
|
47
|
+
/** Local paths blocked as redirect targets to avoid auth loops. */
|
|
48
|
+
blockedPaths?: string[];
|
|
49
|
+
}
|
|
50
|
+
export declare function buildAuthUrl(authUrl: string, options?: BuildAuthUrlOptions): string;
|
|
51
|
+
export declare function resolveSafeRedirectUri(rawValue: string | null | undefined, options: ResolveSafeRedirectUriOptions): string;
|
|
31
52
|
export declare class AuthManager {
|
|
32
53
|
private readonly apiUrl;
|
|
33
54
|
private readonly authUrl;
|
|
@@ -47,6 +68,23 @@ export declare class AuthManager {
|
|
|
47
68
|
subscribe(listener: AuthListener): () => void;
|
|
48
69
|
private notify;
|
|
49
70
|
private setState;
|
|
71
|
+
private assertBrowserContext;
|
|
72
|
+
private getCookie;
|
|
73
|
+
private clearInjectedToken;
|
|
74
|
+
private rawSignOutViaBackend;
|
|
75
|
+
/**
|
|
76
|
+
* Check whether a cookie-backed session is active without mutating auth state.
|
|
77
|
+
*/
|
|
78
|
+
isAuthenticatedViaCookie(): Promise<boolean>;
|
|
79
|
+
/**
|
|
80
|
+
* Return a browser access token from the session layer.
|
|
81
|
+
* Throws if no token is available.
|
|
82
|
+
*/
|
|
83
|
+
getAccessToken(): Promise<string>;
|
|
84
|
+
/**
|
|
85
|
+
* Force a refresh-token flow and return the new access token.
|
|
86
|
+
*/
|
|
87
|
+
refreshAccessToken(): Promise<string>;
|
|
50
88
|
/**
|
|
51
89
|
* Build request headers for an API call.
|
|
52
90
|
* Uses Bearer token if one was injected, otherwise omits Authorization
|
|
@@ -63,10 +101,21 @@ export declare class AuthManager {
|
|
|
63
101
|
* Does NOT redirect — call redirectToAuth() explicitly if desired.
|
|
64
102
|
*/
|
|
65
103
|
markUnauthenticated(): void;
|
|
104
|
+
/**
|
|
105
|
+
* Sign out the current user session.
|
|
106
|
+
* Returns true when the session is no longer active.
|
|
107
|
+
*/
|
|
108
|
+
signOut(): Promise<boolean>;
|
|
109
|
+
/**
|
|
110
|
+
* Build auth URL for login/signup/custom auth sub-path.
|
|
111
|
+
*/
|
|
112
|
+
getAuthUrl(options?: BuildAuthUrlOptions): string;
|
|
66
113
|
/**
|
|
67
114
|
* Redirect to the auth service, passing the current URL as redirect_uri.
|
|
68
115
|
* After the user authenticates, the auth service should redirect back to
|
|
69
116
|
* the original URL and set the session cookie.
|
|
70
117
|
*/
|
|
71
|
-
redirectToAuth(
|
|
118
|
+
redirectToAuth(options?: Omit<BuildAuthUrlOptions, "redirectUri"> & {
|
|
119
|
+
redirectUri?: string;
|
|
120
|
+
}): void;
|
|
72
121
|
}
|
package/dist/auth.js
CHANGED
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
* Auth state is determined by calling GET /users/me (user.current.get).
|
|
17
17
|
* 401 → unauthenticated. 200 → authenticated.
|
|
18
18
|
*/
|
|
19
|
+
import Session from "supertokens-web-js/recipe/session";
|
|
19
20
|
import { ensureCookieSessionSupport } from "./supertokens.js";
|
|
21
|
+
const DEFAULT_BLOCKED_REDIRECT_PATHS = ["/login", "/signup", "/auth"];
|
|
20
22
|
const LOCALSTORAGE_TOKEN_KEY = "lemma_token";
|
|
21
23
|
const QUERY_PARAM_TOKEN_KEY = "lemma_token";
|
|
22
24
|
function detectInjectedToken() {
|
|
@@ -51,6 +53,79 @@ function detectInjectedToken() {
|
|
|
51
53
|
catch { /* ignore */ }
|
|
52
54
|
return null;
|
|
53
55
|
}
|
|
56
|
+
function normalizePath(path) {
|
|
57
|
+
const trimmed = path.trim();
|
|
58
|
+
if (!trimmed)
|
|
59
|
+
return "/";
|
|
60
|
+
if (trimmed === "/")
|
|
61
|
+
return "/";
|
|
62
|
+
const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
63
|
+
return withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
|
|
64
|
+
}
|
|
65
|
+
function resolveAuthPath(basePath, path) {
|
|
66
|
+
const normalizedBase = normalizePath(basePath);
|
|
67
|
+
if (!path || !path.trim()) {
|
|
68
|
+
return normalizedBase;
|
|
69
|
+
}
|
|
70
|
+
const segment = path.trim().replace(/^\/+/, "");
|
|
71
|
+
if (!segment) {
|
|
72
|
+
return normalizedBase;
|
|
73
|
+
}
|
|
74
|
+
return `${normalizedBase}/${segment}`.replace(/\/{2,}/g, "/");
|
|
75
|
+
}
|
|
76
|
+
function isBlockedLocalPath(pathname, blockedPaths) {
|
|
77
|
+
const normalizedPathname = normalizePath(pathname);
|
|
78
|
+
return blockedPaths.some((rawBlockedPath) => {
|
|
79
|
+
const blockedPath = normalizePath(rawBlockedPath);
|
|
80
|
+
return normalizedPathname === blockedPath || normalizedPathname.startsWith(`${blockedPath}/`);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function normalizeOrigin(rawOrigin) {
|
|
84
|
+
const parsed = new URL(rawOrigin);
|
|
85
|
+
return parsed.origin;
|
|
86
|
+
}
|
|
87
|
+
export function buildAuthUrl(authUrl, options = {}) {
|
|
88
|
+
const url = new URL(authUrl);
|
|
89
|
+
url.pathname = resolveAuthPath(url.pathname, options.path);
|
|
90
|
+
for (const [key, value] of Object.entries(options.params ?? {})) {
|
|
91
|
+
if (value === null || value === undefined)
|
|
92
|
+
continue;
|
|
93
|
+
if (Array.isArray(value)) {
|
|
94
|
+
url.searchParams.delete(key);
|
|
95
|
+
for (const item of value) {
|
|
96
|
+
url.searchParams.append(key, String(item));
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
url.searchParams.set(key, String(value));
|
|
101
|
+
}
|
|
102
|
+
if (options.mode === "signup") {
|
|
103
|
+
url.searchParams.set("show", "signup");
|
|
104
|
+
}
|
|
105
|
+
if (options.redirectUri && options.redirectUri.trim()) {
|
|
106
|
+
url.searchParams.set("redirect_uri", options.redirectUri);
|
|
107
|
+
}
|
|
108
|
+
return url.toString();
|
|
109
|
+
}
|
|
110
|
+
export function resolveSafeRedirectUri(rawValue, options) {
|
|
111
|
+
const siteOrigin = normalizeOrigin(options.siteOrigin);
|
|
112
|
+
const blockedPaths = options.blockedPaths ?? DEFAULT_BLOCKED_REDIRECT_PATHS;
|
|
113
|
+
const fallbackTarget = options.fallback ?? "/";
|
|
114
|
+
const fallback = new URL(fallbackTarget, siteOrigin).toString();
|
|
115
|
+
if (!rawValue || !rawValue.trim()) {
|
|
116
|
+
return fallback;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const parsed = new URL(rawValue, siteOrigin);
|
|
120
|
+
if (parsed.origin === siteOrigin && isBlockedLocalPath(parsed.pathname, blockedPaths)) {
|
|
121
|
+
return fallback;
|
|
122
|
+
}
|
|
123
|
+
return parsed.toString();
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return fallback;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
54
129
|
export class AuthManager {
|
|
55
130
|
apiUrl;
|
|
56
131
|
authUrl;
|
|
@@ -93,6 +168,109 @@ export class AuthManager {
|
|
|
93
168
|
this.state = state;
|
|
94
169
|
this.notify();
|
|
95
170
|
}
|
|
171
|
+
assertBrowserContext() {
|
|
172
|
+
if (typeof window === "undefined") {
|
|
173
|
+
throw new Error("This auth method is only available in browser environments.");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
getCookie(name) {
|
|
177
|
+
if (typeof document === "undefined")
|
|
178
|
+
return undefined;
|
|
179
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
180
|
+
const match = document.cookie.match(new RegExp(`(?:^|; )${escaped}=([^;]*)`));
|
|
181
|
+
return match ? decodeURIComponent(match[1]) : undefined;
|
|
182
|
+
}
|
|
183
|
+
clearInjectedToken() {
|
|
184
|
+
this.injectedToken = null;
|
|
185
|
+
if (typeof window === "undefined")
|
|
186
|
+
return;
|
|
187
|
+
try {
|
|
188
|
+
sessionStorage.removeItem(LOCALSTORAGE_TOKEN_KEY);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// ignore storage errors
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
localStorage.removeItem(LOCALSTORAGE_TOKEN_KEY);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// ignore storage errors
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async rawSignOutViaBackend() {
|
|
201
|
+
const antiCsrf = this.getCookie("sAntiCsrf");
|
|
202
|
+
const headers = {
|
|
203
|
+
Accept: "application/json",
|
|
204
|
+
"Content-Type": "application/json",
|
|
205
|
+
rid: "anti-csrf",
|
|
206
|
+
"fdi-version": "4.2",
|
|
207
|
+
"st-auth-mode": "cookie",
|
|
208
|
+
};
|
|
209
|
+
if (antiCsrf) {
|
|
210
|
+
headers["anti-csrf"] = antiCsrf;
|
|
211
|
+
}
|
|
212
|
+
const separator = this.apiUrl.includes("?") ? "&" : "?";
|
|
213
|
+
const signOutUrl = `${this.apiUrl.replace(/\/$/, "")}/st/auth/signout${separator}superTokensDoNotDoInterception=true`;
|
|
214
|
+
await fetch(signOutUrl, {
|
|
215
|
+
method: "POST",
|
|
216
|
+
credentials: "include",
|
|
217
|
+
headers,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Check whether a cookie-backed session is active without mutating auth state.
|
|
222
|
+
*/
|
|
223
|
+
async isAuthenticatedViaCookie() {
|
|
224
|
+
if (this.injectedToken) {
|
|
225
|
+
return this.isAuthenticated();
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const response = await fetch(`${this.apiUrl}/users/me`, {
|
|
229
|
+
method: "GET",
|
|
230
|
+
credentials: "include",
|
|
231
|
+
headers: { Accept: "application/json" },
|
|
232
|
+
});
|
|
233
|
+
return response.status !== 401;
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Return a browser access token from the session layer.
|
|
241
|
+
* Throws if no token is available.
|
|
242
|
+
*/
|
|
243
|
+
async getAccessToken() {
|
|
244
|
+
if (this.injectedToken) {
|
|
245
|
+
return this.injectedToken;
|
|
246
|
+
}
|
|
247
|
+
this.assertBrowserContext();
|
|
248
|
+
ensureCookieSessionSupport(this.apiUrl, () => this.markUnauthenticated());
|
|
249
|
+
const token = await Session.getAccessToken();
|
|
250
|
+
if (!token) {
|
|
251
|
+
throw new Error("Token unavailable");
|
|
252
|
+
}
|
|
253
|
+
return token;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Force a refresh-token flow and return the new access token.
|
|
257
|
+
*/
|
|
258
|
+
async refreshAccessToken() {
|
|
259
|
+
if (this.injectedToken) {
|
|
260
|
+
return this.injectedToken;
|
|
261
|
+
}
|
|
262
|
+
this.assertBrowserContext();
|
|
263
|
+
ensureCookieSessionSupport(this.apiUrl, () => this.markUnauthenticated());
|
|
264
|
+
const refreshed = await Session.attemptRefreshingSession();
|
|
265
|
+
if (!refreshed) {
|
|
266
|
+
throw new Error("Session refresh failed");
|
|
267
|
+
}
|
|
268
|
+
const token = await Session.getAccessToken();
|
|
269
|
+
if (!token) {
|
|
270
|
+
throw new Error("Token unavailable");
|
|
271
|
+
}
|
|
272
|
+
return token;
|
|
273
|
+
}
|
|
96
274
|
/**
|
|
97
275
|
* Build request headers for an API call.
|
|
98
276
|
* Uses Bearer token if one was injected, otherwise omits Authorization
|
|
@@ -151,16 +329,54 @@ export class AuthManager {
|
|
|
151
329
|
markUnauthenticated() {
|
|
152
330
|
this.setState({ status: "unauthenticated", user: null });
|
|
153
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* Sign out the current user session.
|
|
334
|
+
* Returns true when the session is no longer active.
|
|
335
|
+
*/
|
|
336
|
+
async signOut() {
|
|
337
|
+
if (this.injectedToken) {
|
|
338
|
+
this.clearInjectedToken();
|
|
339
|
+
this.markUnauthenticated();
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
this.assertBrowserContext();
|
|
343
|
+
ensureCookieSessionSupport(this.apiUrl, () => this.markUnauthenticated());
|
|
344
|
+
try {
|
|
345
|
+
await Session.signOut();
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
// continue with raw fallback
|
|
349
|
+
}
|
|
350
|
+
if (await this.isAuthenticatedViaCookie()) {
|
|
351
|
+
try {
|
|
352
|
+
await this.rawSignOutViaBackend();
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
// best effort fallback only
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const isAuthenticated = await this.isAuthenticatedViaCookie();
|
|
359
|
+
if (!isAuthenticated) {
|
|
360
|
+
this.markUnauthenticated();
|
|
361
|
+
}
|
|
362
|
+
return !isAuthenticated;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Build auth URL for login/signup/custom auth sub-path.
|
|
366
|
+
*/
|
|
367
|
+
getAuthUrl(options = {}) {
|
|
368
|
+
return buildAuthUrl(this.authUrl, options);
|
|
369
|
+
}
|
|
154
370
|
/**
|
|
155
371
|
* Redirect to the auth service, passing the current URL as redirect_uri.
|
|
156
372
|
* After the user authenticates, the auth service should redirect back to
|
|
157
373
|
* the original URL and set the session cookie.
|
|
158
374
|
*/
|
|
159
|
-
redirectToAuth() {
|
|
375
|
+
redirectToAuth(options = {}) {
|
|
160
376
|
if (typeof window === "undefined") {
|
|
161
377
|
return;
|
|
162
378
|
}
|
|
163
|
-
const redirectUri =
|
|
164
|
-
window.location.href =
|
|
379
|
+
const redirectUri = options.redirectUri ?? window.location.href;
|
|
380
|
+
window.location.href = this.getAuthUrl({ ...options, redirectUri });
|
|
165
381
|
}
|
|
166
382
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"./browser.js": function (module, exports, require) {
|
|
4
4
|
"use strict";
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.ApiError = exports.AuthManager = exports.LemmaClient = void 0;
|
|
6
|
+
exports.ApiError = exports.resolveSafeRedirectUri = exports.buildAuthUrl = exports.AuthManager = exports.LemmaClient = void 0;
|
|
7
7
|
/**
|
|
8
8
|
* Browser bundle entry point.
|
|
9
9
|
* Exposes LemmaClient as globalThis.LemmaClient.LemmaClient
|
|
@@ -18,6 +18,8 @@ var client_js_1 = require("./client.js");
|
|
|
18
18
|
Object.defineProperty(exports, "LemmaClient", { enumerable: true, get: function () { return client_js_1.LemmaClient; } });
|
|
19
19
|
var auth_js_1 = require("./auth.js");
|
|
20
20
|
Object.defineProperty(exports, "AuthManager", { enumerable: true, get: function () { return auth_js_1.AuthManager; } });
|
|
21
|
+
Object.defineProperty(exports, "buildAuthUrl", { enumerable: true, get: function () { return auth_js_1.buildAuthUrl; } });
|
|
22
|
+
Object.defineProperty(exports, "resolveSafeRedirectUri", { enumerable: true, get: function () { return auth_js_1.resolveSafeRedirectUri; } });
|
|
21
23
|
var http_js_1 = require("./http.js");
|
|
22
24
|
Object.defineProperty(exports, "ApiError", { enumerable: true, get: function () { return http_js_1.ApiError; } });
|
|
23
25
|
|
|
@@ -50,11 +52,11 @@ const tasks_js_1 = require("./namespaces/tasks.js");
|
|
|
50
52
|
const users_js_1 = require("./namespaces/users.js");
|
|
51
53
|
const workflows_js_1 = require("./namespaces/workflows.js");
|
|
52
54
|
class LemmaClient {
|
|
53
|
-
constructor(overrides = {}) {
|
|
55
|
+
constructor(overrides = {}, internalOptions = {}) {
|
|
54
56
|
this._config = (0, config_js_1.resolveConfig)(overrides);
|
|
55
57
|
this._currentPodId = this._config.podId;
|
|
56
58
|
this._podId = this._config.podId;
|
|
57
|
-
this.auth = new auth_js_1.AuthManager(this._config.apiUrl, this._config.authUrl);
|
|
59
|
+
this.auth = internalOptions.authManager ?? new auth_js_1.AuthManager(this._config.apiUrl, this._config.authUrl);
|
|
58
60
|
this._http = new http_js_1.HttpClient(this._config.apiUrl, this.auth);
|
|
59
61
|
this._generated = new generated_js_1.GeneratedClientAdapter(this._config.apiUrl, this.auth);
|
|
60
62
|
const podIdFn = () => {
|
|
@@ -89,7 +91,7 @@ class LemmaClient {
|
|
|
89
91
|
}
|
|
90
92
|
/** Return a new client scoped to a specific pod, sharing auth state. */
|
|
91
93
|
withPod(podId) {
|
|
92
|
-
return new LemmaClient({ ...this._config, podId });
|
|
94
|
+
return new LemmaClient({ ...this._config, podId }, { authManager: this.auth });
|
|
93
95
|
}
|
|
94
96
|
get podId() {
|
|
95
97
|
return this._currentPodId;
|
|
@@ -192,7 +194,11 @@ function resolveConfig(overrides = {}) {
|
|
|
192
194
|
*/
|
|
193
195
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
194
196
|
exports.AuthManager = void 0;
|
|
197
|
+
exports.buildAuthUrl = buildAuthUrl;
|
|
198
|
+
exports.resolveSafeRedirectUri = resolveSafeRedirectUri;
|
|
199
|
+
const session_1 = require("supertokens-web-js/recipe/session");
|
|
195
200
|
const supertokens_js_1 = require("./supertokens.js");
|
|
201
|
+
const DEFAULT_BLOCKED_REDIRECT_PATHS = ["/login", "/signup", "/auth"];
|
|
196
202
|
const LOCALSTORAGE_TOKEN_KEY = "lemma_token";
|
|
197
203
|
const QUERY_PARAM_TOKEN_KEY = "lemma_token";
|
|
198
204
|
function detectInjectedToken() {
|
|
@@ -227,6 +233,79 @@ function detectInjectedToken() {
|
|
|
227
233
|
catch { /* ignore */ }
|
|
228
234
|
return null;
|
|
229
235
|
}
|
|
236
|
+
function normalizePath(path) {
|
|
237
|
+
const trimmed = path.trim();
|
|
238
|
+
if (!trimmed)
|
|
239
|
+
return "/";
|
|
240
|
+
if (trimmed === "/")
|
|
241
|
+
return "/";
|
|
242
|
+
const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
243
|
+
return withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
|
|
244
|
+
}
|
|
245
|
+
function resolveAuthPath(basePath, path) {
|
|
246
|
+
const normalizedBase = normalizePath(basePath);
|
|
247
|
+
if (!path || !path.trim()) {
|
|
248
|
+
return normalizedBase;
|
|
249
|
+
}
|
|
250
|
+
const segment = path.trim().replace(/^\/+/, "");
|
|
251
|
+
if (!segment) {
|
|
252
|
+
return normalizedBase;
|
|
253
|
+
}
|
|
254
|
+
return `${normalizedBase}/${segment}`.replace(/\/{2,}/g, "/");
|
|
255
|
+
}
|
|
256
|
+
function isBlockedLocalPath(pathname, blockedPaths) {
|
|
257
|
+
const normalizedPathname = normalizePath(pathname);
|
|
258
|
+
return blockedPaths.some((rawBlockedPath) => {
|
|
259
|
+
const blockedPath = normalizePath(rawBlockedPath);
|
|
260
|
+
return normalizedPathname === blockedPath || normalizedPathname.startsWith(`${blockedPath}/`);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function normalizeOrigin(rawOrigin) {
|
|
264
|
+
const parsed = new URL(rawOrigin);
|
|
265
|
+
return parsed.origin;
|
|
266
|
+
}
|
|
267
|
+
function buildAuthUrl(authUrl, options = {}) {
|
|
268
|
+
const url = new URL(authUrl);
|
|
269
|
+
url.pathname = resolveAuthPath(url.pathname, options.path);
|
|
270
|
+
for (const [key, value] of Object.entries(options.params ?? {})) {
|
|
271
|
+
if (value === null || value === undefined)
|
|
272
|
+
continue;
|
|
273
|
+
if (Array.isArray(value)) {
|
|
274
|
+
url.searchParams.delete(key);
|
|
275
|
+
for (const item of value) {
|
|
276
|
+
url.searchParams.append(key, String(item));
|
|
277
|
+
}
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
url.searchParams.set(key, String(value));
|
|
281
|
+
}
|
|
282
|
+
if (options.mode === "signup") {
|
|
283
|
+
url.searchParams.set("show", "signup");
|
|
284
|
+
}
|
|
285
|
+
if (options.redirectUri && options.redirectUri.trim()) {
|
|
286
|
+
url.searchParams.set("redirect_uri", options.redirectUri);
|
|
287
|
+
}
|
|
288
|
+
return url.toString();
|
|
289
|
+
}
|
|
290
|
+
function resolveSafeRedirectUri(rawValue, options) {
|
|
291
|
+
const siteOrigin = normalizeOrigin(options.siteOrigin);
|
|
292
|
+
const blockedPaths = options.blockedPaths ?? DEFAULT_BLOCKED_REDIRECT_PATHS;
|
|
293
|
+
const fallbackTarget = options.fallback ?? "/";
|
|
294
|
+
const fallback = new URL(fallbackTarget, siteOrigin).toString();
|
|
295
|
+
if (!rawValue || !rawValue.trim()) {
|
|
296
|
+
return fallback;
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const parsed = new URL(rawValue, siteOrigin);
|
|
300
|
+
if (parsed.origin === siteOrigin && isBlockedLocalPath(parsed.pathname, blockedPaths)) {
|
|
301
|
+
return fallback;
|
|
302
|
+
}
|
|
303
|
+
return parsed.toString();
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return fallback;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
230
309
|
class AuthManager {
|
|
231
310
|
constructor(apiUrl, authUrl) {
|
|
232
311
|
this.state = { status: "loading", user: null };
|
|
@@ -266,6 +345,109 @@ class AuthManager {
|
|
|
266
345
|
this.state = state;
|
|
267
346
|
this.notify();
|
|
268
347
|
}
|
|
348
|
+
assertBrowserContext() {
|
|
349
|
+
if (typeof window === "undefined") {
|
|
350
|
+
throw new Error("This auth method is only available in browser environments.");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
getCookie(name) {
|
|
354
|
+
if (typeof document === "undefined")
|
|
355
|
+
return undefined;
|
|
356
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
357
|
+
const match = document.cookie.match(new RegExp(`(?:^|; )${escaped}=([^;]*)`));
|
|
358
|
+
return match ? decodeURIComponent(match[1]) : undefined;
|
|
359
|
+
}
|
|
360
|
+
clearInjectedToken() {
|
|
361
|
+
this.injectedToken = null;
|
|
362
|
+
if (typeof window === "undefined")
|
|
363
|
+
return;
|
|
364
|
+
try {
|
|
365
|
+
sessionStorage.removeItem(LOCALSTORAGE_TOKEN_KEY);
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
// ignore storage errors
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
localStorage.removeItem(LOCALSTORAGE_TOKEN_KEY);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
// ignore storage errors
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async rawSignOutViaBackend() {
|
|
378
|
+
const antiCsrf = this.getCookie("sAntiCsrf");
|
|
379
|
+
const headers = {
|
|
380
|
+
Accept: "application/json",
|
|
381
|
+
"Content-Type": "application/json",
|
|
382
|
+
rid: "anti-csrf",
|
|
383
|
+
"fdi-version": "4.2",
|
|
384
|
+
"st-auth-mode": "cookie",
|
|
385
|
+
};
|
|
386
|
+
if (antiCsrf) {
|
|
387
|
+
headers["anti-csrf"] = antiCsrf;
|
|
388
|
+
}
|
|
389
|
+
const separator = this.apiUrl.includes("?") ? "&" : "?";
|
|
390
|
+
const signOutUrl = `${this.apiUrl.replace(/\/$/, "")}/st/auth/signout${separator}superTokensDoNotDoInterception=true`;
|
|
391
|
+
await fetch(signOutUrl, {
|
|
392
|
+
method: "POST",
|
|
393
|
+
credentials: "include",
|
|
394
|
+
headers,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Check whether a cookie-backed session is active without mutating auth state.
|
|
399
|
+
*/
|
|
400
|
+
async isAuthenticatedViaCookie() {
|
|
401
|
+
if (this.injectedToken) {
|
|
402
|
+
return this.isAuthenticated();
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
const response = await fetch(`${this.apiUrl}/users/me`, {
|
|
406
|
+
method: "GET",
|
|
407
|
+
credentials: "include",
|
|
408
|
+
headers: { Accept: "application/json" },
|
|
409
|
+
});
|
|
410
|
+
return response.status !== 401;
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Return a browser access token from the session layer.
|
|
418
|
+
* Throws if no token is available.
|
|
419
|
+
*/
|
|
420
|
+
async getAccessToken() {
|
|
421
|
+
if (this.injectedToken) {
|
|
422
|
+
return this.injectedToken;
|
|
423
|
+
}
|
|
424
|
+
this.assertBrowserContext();
|
|
425
|
+
(0, supertokens_js_1.ensureCookieSessionSupport)(this.apiUrl, () => this.markUnauthenticated());
|
|
426
|
+
const token = await session_1.default.getAccessToken();
|
|
427
|
+
if (!token) {
|
|
428
|
+
throw new Error("Token unavailable");
|
|
429
|
+
}
|
|
430
|
+
return token;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Force a refresh-token flow and return the new access token.
|
|
434
|
+
*/
|
|
435
|
+
async refreshAccessToken() {
|
|
436
|
+
if (this.injectedToken) {
|
|
437
|
+
return this.injectedToken;
|
|
438
|
+
}
|
|
439
|
+
this.assertBrowserContext();
|
|
440
|
+
(0, supertokens_js_1.ensureCookieSessionSupport)(this.apiUrl, () => this.markUnauthenticated());
|
|
441
|
+
const refreshed = await session_1.default.attemptRefreshingSession();
|
|
442
|
+
if (!refreshed) {
|
|
443
|
+
throw new Error("Session refresh failed");
|
|
444
|
+
}
|
|
445
|
+
const token = await session_1.default.getAccessToken();
|
|
446
|
+
if (!token) {
|
|
447
|
+
throw new Error("Token unavailable");
|
|
448
|
+
}
|
|
449
|
+
return token;
|
|
450
|
+
}
|
|
269
451
|
/**
|
|
270
452
|
* Build request headers for an API call.
|
|
271
453
|
* Uses Bearer token if one was injected, otherwise omits Authorization
|
|
@@ -324,17 +506,55 @@ class AuthManager {
|
|
|
324
506
|
markUnauthenticated() {
|
|
325
507
|
this.setState({ status: "unauthenticated", user: null });
|
|
326
508
|
}
|
|
509
|
+
/**
|
|
510
|
+
* Sign out the current user session.
|
|
511
|
+
* Returns true when the session is no longer active.
|
|
512
|
+
*/
|
|
513
|
+
async signOut() {
|
|
514
|
+
if (this.injectedToken) {
|
|
515
|
+
this.clearInjectedToken();
|
|
516
|
+
this.markUnauthenticated();
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
this.assertBrowserContext();
|
|
520
|
+
(0, supertokens_js_1.ensureCookieSessionSupport)(this.apiUrl, () => this.markUnauthenticated());
|
|
521
|
+
try {
|
|
522
|
+
await session_1.default.signOut();
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
// continue with raw fallback
|
|
526
|
+
}
|
|
527
|
+
if (await this.isAuthenticatedViaCookie()) {
|
|
528
|
+
try {
|
|
529
|
+
await this.rawSignOutViaBackend();
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
// best effort fallback only
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const isAuthenticated = await this.isAuthenticatedViaCookie();
|
|
536
|
+
if (!isAuthenticated) {
|
|
537
|
+
this.markUnauthenticated();
|
|
538
|
+
}
|
|
539
|
+
return !isAuthenticated;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Build auth URL for login/signup/custom auth sub-path.
|
|
543
|
+
*/
|
|
544
|
+
getAuthUrl(options = {}) {
|
|
545
|
+
return buildAuthUrl(this.authUrl, options);
|
|
546
|
+
}
|
|
327
547
|
/**
|
|
328
548
|
* Redirect to the auth service, passing the current URL as redirect_uri.
|
|
329
549
|
* After the user authenticates, the auth service should redirect back to
|
|
330
550
|
* the original URL and set the session cookie.
|
|
331
551
|
*/
|
|
332
|
-
redirectToAuth() {
|
|
552
|
+
redirectToAuth(options = {}) {
|
|
333
553
|
if (typeof window === "undefined") {
|
|
334
554
|
return;
|
|
335
555
|
}
|
|
336
|
-
const redirectUri =
|
|
337
|
-
window.location.href =
|
|
556
|
+
const redirectUri = options.redirectUri ?? window.location.href;
|
|
557
|
+
window.location.href = this.getAuthUrl({ ...options, redirectUri });
|
|
338
558
|
}
|
|
339
559
|
}
|
|
340
560
|
exports.AuthManager = AuthManager;
|
package/dist/browser.d.ts
CHANGED
package/dist/browser.js
CHANGED
package/dist/client.d.ts
CHANGED
|
@@ -21,6 +21,9 @@ import { WorkflowsNamespace } from "./namespaces/workflows.js";
|
|
|
21
21
|
export type { LemmaConfig };
|
|
22
22
|
export { AuthManager };
|
|
23
23
|
export type { AuthState, AuthListener };
|
|
24
|
+
interface LemmaClientInternalOptions {
|
|
25
|
+
authManager?: AuthManager;
|
|
26
|
+
}
|
|
24
27
|
export declare class LemmaClient {
|
|
25
28
|
private readonly _config;
|
|
26
29
|
private readonly _podId;
|
|
@@ -48,7 +51,7 @@ export declare class LemmaClient {
|
|
|
48
51
|
readonly podMembers: PodMembersNamespace;
|
|
49
52
|
readonly organizations: OrganizationsNamespace;
|
|
50
53
|
readonly podSurfaces: PodSurfacesNamespace;
|
|
51
|
-
constructor(overrides?: Partial<LemmaConfig
|
|
54
|
+
constructor(overrides?: Partial<LemmaConfig>, internalOptions?: LemmaClientInternalOptions);
|
|
52
55
|
/** Change the active pod ID for subsequent calls. */
|
|
53
56
|
setPodId(podId: string): void;
|
|
54
57
|
/** Return a new client scoped to a specific pod, sharing auth state. */
|
package/dist/client.js
CHANGED
|
@@ -49,11 +49,11 @@ export class LemmaClient {
|
|
|
49
49
|
podMembers;
|
|
50
50
|
organizations;
|
|
51
51
|
podSurfaces;
|
|
52
|
-
constructor(overrides = {}) {
|
|
52
|
+
constructor(overrides = {}, internalOptions = {}) {
|
|
53
53
|
this._config = resolveConfig(overrides);
|
|
54
54
|
this._currentPodId = this._config.podId;
|
|
55
55
|
this._podId = this._config.podId;
|
|
56
|
-
this.auth = new AuthManager(this._config.apiUrl, this._config.authUrl);
|
|
56
|
+
this.auth = internalOptions.authManager ?? new AuthManager(this._config.apiUrl, this._config.authUrl);
|
|
57
57
|
this._http = new HttpClient(this._config.apiUrl, this.auth);
|
|
58
58
|
this._generated = new GeneratedClientAdapter(this._config.apiUrl, this.auth);
|
|
59
59
|
const podIdFn = () => {
|
|
@@ -88,7 +88,7 @@ export class LemmaClient {
|
|
|
88
88
|
}
|
|
89
89
|
/** Return a new client scoped to a specific pod, sharing auth state. */
|
|
90
90
|
withPod(podId) {
|
|
91
|
-
return new LemmaClient({ ...this._config, podId });
|
|
91
|
+
return new LemmaClient({ ...this._config, podId }, { authManager: this.auth });
|
|
92
92
|
}
|
|
93
93
|
get podId() {
|
|
94
94
|
return this._currentPodId;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { LemmaClient } from "./client.js";
|
|
2
2
|
export type { LemmaConfig } from "./client.js";
|
|
3
|
-
export { AuthManager } from "./auth.js";
|
|
4
|
-
export type { AuthState, AuthListener, AuthStatus, UserInfo } from "./auth.js";
|
|
3
|
+
export { AuthManager, buildAuthUrl, resolveSafeRedirectUri } from "./auth.js";
|
|
4
|
+
export type { AuthState, AuthListener, AuthStatus, UserInfo, AuthRedirectMode, BuildAuthUrlOptions, ResolveSafeRedirectUriOptions, } from "./auth.js";
|
|
5
5
|
export { ApiError } from "./http.js";
|
|
6
6
|
export * from "./types.js";
|
|
7
7
|
export { readSSE, parseSSEJson } from "./streams.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { LemmaClient } from "./client.js";
|
|
2
|
-
export { AuthManager } from "./auth.js";
|
|
2
|
+
export { AuthManager, buildAuthUrl, resolveSafeRedirectUri } from "./auth.js";
|
|
3
3
|
export { ApiError } from "./http.js";
|
|
4
4
|
export * from "./types.js";
|
|
5
5
|
export { readSSE, parseSSEJson } from "./streams.js";
|
package/dist/react/useAuth.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { LemmaClient } from "../client.js";
|
|
2
|
-
import type { AuthState } from "../auth.js";
|
|
2
|
+
import type { AuthState, BuildAuthUrlOptions } from "../auth.js";
|
|
3
|
+
type RedirectToAuthOptions = Omit<BuildAuthUrlOptions, "redirectUri"> & {
|
|
4
|
+
redirectUri?: string;
|
|
5
|
+
};
|
|
3
6
|
export interface UseAuthResult {
|
|
4
7
|
status: AuthState["status"];
|
|
5
8
|
user: AuthState["user"];
|
|
6
9
|
isLoading: boolean;
|
|
7
10
|
isAuthenticated: boolean;
|
|
8
|
-
redirectToAuth: () => void;
|
|
11
|
+
redirectToAuth: (options?: RedirectToAuthOptions) => void;
|
|
9
12
|
}
|
|
10
13
|
/**
|
|
11
14
|
* React hook for subscribing to Lemma auth state.
|
|
@@ -14,3 +17,4 @@ export interface UseAuthResult {
|
|
|
14
17
|
* const { isAuthenticated, isLoading, redirectToAuth } = useAuth(client);
|
|
15
18
|
*/
|
|
16
19
|
export declare function useAuth(client: LemmaClient): UseAuthResult;
|
|
20
|
+
export {};
|
package/dist/react/useAuth.js
CHANGED
|
@@ -24,6 +24,6 @@ export function useAuth(client) {
|
|
|
24
24
|
user: state.user,
|
|
25
25
|
isLoading: state.status === "loading",
|
|
26
26
|
isAuthenticated: state.status === "authenticated",
|
|
27
|
-
redirectToAuth: () => client.auth.redirectToAuth(),
|
|
27
|
+
redirectToAuth: (options) => client.auth.redirectToAuth(options),
|
|
28
28
|
};
|
|
29
29
|
}
|