@qelos/sdk 3.11.11 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +31 -0
  2. package/dist/__tests__/blueprints.types.test-d.d.ts +28 -0
  3. package/dist/__tests__/blueprints.types.test-d.js +11 -0
  4. package/dist/__tests__/blueprints.types.test-d.js.map +1 -0
  5. package/dist/administrator/events.d.ts +5 -0
  6. package/dist/administrator/events.js +8 -0
  7. package/dist/administrator/events.js.map +1 -1
  8. package/dist/ai/agents.d.ts +56 -0
  9. package/dist/ai/agents.js +116 -0
  10. package/dist/ai/agents.js.map +1 -0
  11. package/dist/ai/index.d.ts +9 -3
  12. package/dist/ai/index.js +8 -1
  13. package/dist/ai/index.js.map +1 -1
  14. package/dist/ai/types.d.ts +61 -0
  15. package/dist/authentication.d.ts +64 -1
  16. package/dist/authentication.js +137 -0
  17. package/dist/authentication.js.map +1 -1
  18. package/dist/base-sdk.d.ts +3 -0
  19. package/dist/base-sdk.js +16 -2
  20. package/dist/base-sdk.js.map +1 -1
  21. package/dist/blueprints-entities.d.ts +26 -0
  22. package/dist/blueprints-entities.js +91 -5
  23. package/dist/blueprints-entities.js.map +1 -1
  24. package/dist/blueprints.d.ts +3 -0
  25. package/dist/blueprints.js.map +1 -1
  26. package/dist/index.d.ts +5 -0
  27. package/dist/index.js +10 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/workspaces.d.ts +16 -0
  30. package/dist/workspaces.js +40 -0
  31. package/dist/workspaces.js.map +1 -1
  32. package/package.json +2 -2
  33. package/src/__tests__/blueprints.types.test-d.ts +49 -0
  34. package/src/administrator/events.ts +6 -1
  35. package/src/ai/agents.ts +171 -0
  36. package/src/ai/index.ts +22 -4
  37. package/src/ai/types.ts +66 -0
  38. package/src/authentication.ts +181 -1
  39. package/src/base-sdk.ts +16 -2
  40. package/src/blueprints-entities.ts +113 -5
  41. package/src/blueprints.ts +9 -2
  42. package/src/index.ts +26 -1
  43. package/src/workspaces.ts +63 -0
  44. package/test/authentication-social.test.ts +48 -0
  45. package/test/base-sdk.test.ts +26 -1
  46. package/test/index.test.ts +2 -1
  47. package/test/qelos-sdk-refresh.test.ts +2 -1
  48. package/test/workspaces.test.ts +170 -0
@@ -0,0 +1,49 @@
1
+ // Compile-only type test for `BlueprintEntitiesRegistry` augmentation
2
+ // and the overloaded `entitiesOf` signatures. Runs as part of `tsc --noEmit`
3
+ // (type-check / build); not executed by `node --test`.
4
+
5
+ import QlBlueprints from '../blueprints';
6
+ import type { IBaseBlueprintEntity } from '../blueprints-entities';
7
+ import type QlBlueprintEntities from '../blueprints-entities';
8
+
9
+ interface TodoProperties {
10
+ title: string;
11
+ done: boolean;
12
+ }
13
+
14
+ interface TodoEntity extends IBaseBlueprintEntity, TodoProperties {}
15
+
16
+ declare module '../blueprints' {
17
+ interface BlueprintEntitiesRegistry {
18
+ todo: TodoEntity;
19
+ }
20
+ }
21
+
22
+ type Equal<A, B> =
23
+ (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
24
+ type Expect<T extends true> = T;
25
+
26
+ declare const blueprints: QlBlueprints;
27
+
28
+ // 1. Registry-keyed lookup resolves to the augmented entity type.
29
+ const todos = blueprints.entitiesOf('todo');
30
+ type TodosCheck = Expect<Equal<typeof todos, QlBlueprintEntities<TodoEntity>>>;
31
+
32
+ // `find()` returns the merged entity shape from the registry.
33
+ declare const found: Awaited<ReturnType<typeof todos.find>>;
34
+ type FoundCheck = Expect<Equal<typeof found, (IBaseBlueprintEntity & TodoEntity)[]>>;
35
+
36
+ // 2. Explicit-generic fallback still compiles for unregistered keys.
37
+ interface AdHoc {
38
+ name: string;
39
+ }
40
+ const adhoc = blueprints.entitiesOf<AdHoc>('not-in-registry');
41
+ type AdHocCheck = Expect<Equal<typeof adhoc, QlBlueprintEntities<AdHoc>>>;
42
+
43
+ // 3. Plain string key with no generic falls back to `any` (legacy behavior).
44
+ const dynamicKey: string = 'todo';
45
+ const loose = blueprints.entitiesOf(dynamicKey);
46
+ type LooseCheck = Expect<Equal<typeof loose, QlBlueprintEntities<any>>>;
47
+
48
+ // Mark generated checks as used so the file remains a valid module.
49
+ export type _Checks = [TodosCheck, FoundCheck, AdHocCheck, LooseCheck];
@@ -4,6 +4,7 @@ import BaseSDK from '../base-sdk';
4
4
  export interface IQelosEvent {
5
5
  tenant: string;
6
6
  user?: string;
7
+ workspace?: string;
7
8
  source: string;
8
9
  kind: string;
9
10
  eventName: string;
@@ -19,15 +20,19 @@ export default class QlEvents extends BaseSDK {
19
20
  super(options)
20
21
  }
21
22
 
22
- getList(params?: { page?: number; kind?: string; eventName?: string; source?: string; user?: string; workspace?: string; period?: string }) {
23
+ getList(params?: { page?: number; limit?: number; kind?: string; eventName?: string; source?: string; user?: string; workspace?: string; search?: string; period?: string; from?: string; to?: string }) {
23
24
  const queryParams = new URLSearchParams();
24
25
  if (params?.page !== undefined) queryParams.append('page', params.page.toString());
26
+ if (params?.limit !== undefined) queryParams.append('limit', params.limit.toString());
25
27
  if (params?.kind) queryParams.append('kind', params.kind);
26
28
  if (params?.eventName) queryParams.append('eventName', params.eventName);
27
29
  if (params?.source) queryParams.append('source', params.source);
28
30
  if (params?.user) queryParams.append('user', params.user);
29
31
  if (params?.workspace) queryParams.append('workspace', params.workspace);
32
+ if (params?.search) queryParams.append('search', params.search);
30
33
  if (params?.period) queryParams.append('period', params.period);
34
+ if (params?.from) queryParams.append('from', params.from);
35
+ if (params?.to) queryParams.append('to', params.to);
31
36
 
32
37
  const queryString = queryParams.toString();
33
38
  return this.callJsonApi<IQelosEvent[]>(this.relativePath + (queryString ? '?' + queryString : ''));
@@ -0,0 +1,171 @@
1
+ import BaseSDK from '../base-sdk';
2
+ import { QelosSDKOptions } from '../types';
3
+ import ChatSDK from './chat';
4
+ import {
5
+ IAgent,
6
+ IAgentChatOptions,
7
+ IChatCompletionResponse,
8
+ ICreateAgentRequest,
9
+ IMessage,
10
+ ISSEStreamProcessor,
11
+ IUpdateAgentRequest,
12
+ } from '.';
13
+
14
+ export default class AgentsSDK extends BaseSDK {
15
+ private relativePath = '/api/ai/agents';
16
+
17
+ constructor(
18
+ options: QelosSDKOptions,
19
+ private chatSdk: ChatSDK,
20
+ ) {
21
+ super(options);
22
+ }
23
+
24
+ /**
25
+ * List available AI agents for the current tenant
26
+ */
27
+ list(query?: {
28
+ active?: boolean;
29
+ kind?: string;
30
+ limit?: number;
31
+ page?: number;
32
+ sort?: string;
33
+ [key: string]: string | number | boolean;
34
+ }): Promise<IAgent[]> {
35
+ return this.callJsonApi<IAgent[]>(`${this.relativePath}${this.getQueryParams(query)}`);
36
+ }
37
+
38
+ /**
39
+ * Get a specific agent by ID (model, tools, system prompt, etc.)
40
+ */
41
+ get(agentId: string): Promise<IAgent> {
42
+ return this.callJsonApi<IAgent>(`${this.relativePath}/${agentId}${this.getQueryParams({})}`);
43
+ }
44
+
45
+ /**
46
+ * Create a new agent (chat-completion integration)
47
+ */
48
+ create(data: ICreateAgentRequest): Promise<IAgent> {
49
+ return this.callJsonApi<IAgent>(`${this.relativePath}`, {
50
+ method: 'POST',
51
+ headers: { 'content-type': 'application/json' },
52
+ body: JSON.stringify(data),
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Update agent configuration
58
+ */
59
+ update(agentId: string, data: IUpdateAgentRequest): Promise<IAgent> {
60
+ return this.callJsonApi<IAgent>(`${this.relativePath}/${agentId}`, {
61
+ method: 'PUT',
62
+ headers: { 'content-type': 'application/json' },
63
+ body: JSON.stringify(data),
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Delete an agent
69
+ */
70
+ remove(agentId: string): Promise<{ success: boolean; message?: string }> {
71
+ return this.callJsonApi<{ success: boolean; message?: string }>(`${this.relativePath}/${agentId}`, {
72
+ method: 'DELETE',
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Send messages to an agent (non-streaming). The service creates a thread unless `threadId` is passed.
78
+ */
79
+ chat(
80
+ agentId: string,
81
+ message: string,
82
+ options: IAgentChatOptions = {},
83
+ ): Promise<IChatCompletionResponse> {
84
+ const { history, threadId, ...rest } = options;
85
+ const messages: IMessage[] = [...(history || []), { role: 'user', content: message }];
86
+ return this.callJsonApi<IChatCompletionResponse>(`${this.relativePath}/${agentId}/chat`, {
87
+ method: 'POST',
88
+ headers: { 'content-type': 'application/json' },
89
+ body: JSON.stringify({
90
+ ...rest,
91
+ messages,
92
+ ...(threadId ? { threadId } : {}),
93
+ }),
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Stream a response via GET + SSE (`/api/ai/agents/:id/chat`).
99
+ */
100
+ async streamChat(
101
+ agentId: string,
102
+ message: string,
103
+ options: IAgentChatOptions = {},
104
+ ): Promise<ISSEStreamProcessor> {
105
+ const stream = await this.openAgentChatStream(agentId, message, options);
106
+ return this.chatSdk.parseSSEStream(stream);
107
+ }
108
+
109
+ /**
110
+ * Continue in an existing thread (non-streaming)
111
+ */
112
+ chatInThread(
113
+ agentId: string,
114
+ threadId: string,
115
+ message: string,
116
+ options: IAgentChatOptions = {},
117
+ ): Promise<IChatCompletionResponse> {
118
+ return this.chat(agentId, message, { ...options, threadId });
119
+ }
120
+
121
+ /**
122
+ * Stream within an existing thread (GET + SSE)
123
+ */
124
+ async streamChatInThread(
125
+ agentId: string,
126
+ threadId: string,
127
+ message: string,
128
+ options: IAgentChatOptions = {},
129
+ ): Promise<ISSEStreamProcessor> {
130
+ const stream = await this.openAgentChatStream(agentId, message, { ...options, threadId });
131
+ return this.chatSdk.parseSSEStream(stream);
132
+ }
133
+
134
+ private async openAgentChatStream(
135
+ agentId: string,
136
+ message: string,
137
+ options: IAgentChatOptions,
138
+ ): Promise<ReadableStream<Uint8Array>> {
139
+ const { history, threadId, queryParams } = options;
140
+ const messages: IMessage[] = [...(history || []), { role: 'user', content: message }];
141
+ const params = new URLSearchParams();
142
+ params.set('messages', JSON.stringify(messages));
143
+ if (threadId) {
144
+ params.set('threadId', threadId);
145
+ }
146
+ if (queryParams) {
147
+ for (const [key, value] of Object.entries(queryParams)) {
148
+ if (value !== undefined && value !== null) {
149
+ params.set(key, String(value));
150
+ }
151
+ }
152
+ }
153
+ const q = params.toString();
154
+ const response = await this.callApi(`${this.relativePath}/${agentId}/chat?${q}`, {
155
+ method: 'GET',
156
+ headers: {
157
+ accept: 'text/event-stream',
158
+ },
159
+ });
160
+
161
+ if (!response.ok) {
162
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
163
+ }
164
+
165
+ if (!response.body) {
166
+ throw new Error('No response body for agent chat stream');
167
+ }
168
+
169
+ return response.body;
170
+ }
171
+ }
package/src/ai/index.ts CHANGED
@@ -3,6 +3,7 @@ import BaseSDK from '../base-sdk';
3
3
  import ThreadsSDK from './threads';
4
4
  import ChatSDK from './chat';
5
5
  import RagSDK from './rag';
6
+ import AgentsSDK from './agents';
6
7
  import {
7
8
  IMessage,
8
9
  IMessageSummary,
@@ -14,7 +15,12 @@ import {
14
15
  ICreateVectorStorageRequest,
15
16
  IUploadContentRequest,
16
17
  IClearStorageRequest,
17
- IVectorStore
18
+ IVectorStore,
19
+ IAgent,
20
+ IAgentChatOptions,
21
+ IAgentTool,
22
+ ICreateAgentRequest,
23
+ IUpdateAgentRequest,
18
24
  } from './types';
19
25
 
20
26
  export * from './types';
@@ -31,13 +37,19 @@ export {
31
37
  ICreateVectorStorageRequest,
32
38
  IUploadContentRequest,
33
39
  IClearStorageRequest,
34
- IVectorStore
40
+ IVectorStore,
41
+ IAgent,
42
+ IAgentChatOptions,
43
+ IAgentTool,
44
+ ICreateAgentRequest,
45
+ IUpdateAgentRequest,
35
46
  };
36
47
 
37
48
  /**
38
49
  * Qelos AI SDK - Main SDK class with three sub-SDKs
39
50
  *
40
- * This class provides access to AI functionality through three sub-SDKs:
51
+ * This class provides access to AI functionality through sub-SDKs:
52
+ * - `agents`: Agent management and agent-centric chat (recommended entry point)
41
53
  * - `threads`: Thread CRUD operations for managing conversation threads
42
54
  * - `chat`: Chat completion operations (streaming and non-streaming)
43
55
  * - `rag`: Vector storage management for Retrieval-Augmented Generation
@@ -80,9 +92,14 @@ export default class QlAI extends BaseSDK {
80
92
  */
81
93
  public rag: RagSDK;
82
94
 
95
+ /**
96
+ * Agents sub-SDK for managing AI agents and agent-centric chat
97
+ */
98
+ public agents: AgentsSDK;
99
+
83
100
  /**
84
101
  * Initialize the AI SDK with sub-SDKs
85
- *
102
+ *
86
103
  * @param options - SDK configuration options
87
104
  */
88
105
  constructor(options: QelosSDKOptions) {
@@ -90,6 +107,7 @@ export default class QlAI extends BaseSDK {
90
107
  this.threads = new ThreadsSDK(options);
91
108
  this.chat = new ChatSDK(options);
92
109
  this.rag = new RagSDK(options);
110
+ this.agents = new AgentsSDK(options, this.chat);
93
111
  }
94
112
 
95
113
  // Legacy methods for backward compatibility
package/src/ai/types.ts CHANGED
@@ -107,3 +107,69 @@ export interface IVectorStore {
107
107
  expirationAfterDays?: number;
108
108
  created: Date;
109
109
  }
110
+
111
+ export interface IAgentTool {
112
+ name: string;
113
+ description: string;
114
+ schema?: object;
115
+ }
116
+
117
+ export interface IAgent {
118
+ _id: string;
119
+ name: string;
120
+ description?: string;
121
+ model?: string;
122
+ kind?: string[];
123
+ active?: boolean;
124
+ tenant?: string;
125
+ user?: string;
126
+ workspace?: string;
127
+ systemPrompt?: string;
128
+ tools?: IAgentTool[];
129
+ temperature?: number;
130
+ maxTokens?: number;
131
+ triggerSource?: string;
132
+ targetSource?: string;
133
+ created?: Date;
134
+ updated?: Date;
135
+ }
136
+
137
+ /** Payload for `sdk.ai.agents.create()` — maps to a chat-completion integration */
138
+ export interface ICreateAgentRequest {
139
+ name: string;
140
+ model: string;
141
+ triggerSource: string;
142
+ targetSource: string;
143
+ systemPrompt?: string;
144
+ tools?: IAgentTool[];
145
+ temperature?: number;
146
+ maxTokens?: number;
147
+ workspace?: string;
148
+ active?: boolean;
149
+ }
150
+
151
+ /** Partial update for `sdk.ai.agents.update()` */
152
+ export interface IUpdateAgentRequest {
153
+ name?: string;
154
+ model?: string;
155
+ triggerSource?: string;
156
+ targetSource?: string;
157
+ systemPrompt?: string;
158
+ tools?: IAgentTool[];
159
+ temperature?: number;
160
+ maxTokens?: number;
161
+ workspace?: string;
162
+ active?: boolean;
163
+ }
164
+
165
+ export interface IAgentChatOptions extends Partial<Omit<IChatCompletionOptions, 'messages'>> {
166
+ /**
167
+ * Additional messages to prepend before the user message
168
+ * (e.g. system prompt overrides, prior context)
169
+ */
170
+ history?: IMessage[];
171
+ /**
172
+ * When set, reuses an existing thread created by a prior agent chat call.
173
+ */
174
+ threadId?: string;
175
+ }
@@ -43,10 +43,97 @@ export interface IApiToken {
43
43
  tokenPrefix: string;
44
44
  expiresAt: string;
45
45
  lastUsedAt?: string;
46
- workspace?: string;
46
+ /** Workspace scope when set; may be an id string or a populated `{ _id, name }` from list responses */
47
+ workspace?: string | { _id: string; name?: string } | null;
48
+ /** Successful API-token authentications (server-maintained). */
49
+ usageCount?: number;
47
50
  created: string;
48
51
  }
49
52
 
53
+ export type SocialProvider = 'linkedin' | 'facebook' | 'google' | 'github';
54
+
55
+ export interface SocialLoginOptions {
56
+ state?: string;
57
+ returnUrl?: string;
58
+ }
59
+
60
+ export interface SocialAuthCallbackPayload {
61
+ payload: BasicPayload & { workspace?: any };
62
+ headers: { 'set-cookie': string | null };
63
+ /**
64
+ * Present when the runtime exposes multiple `Set-Cookie` headers (Node fetch
65
+ * `getSetCookie()`). Use {@link getSocialAuthSetCookieParts} to apply cookies.
66
+ */
67
+ setCookieHeaders?: string[];
68
+ }
69
+
70
+ /** Callback URL or query bag carrying `rt` from `GET …/callback?rt=…`. */
71
+ export type SocialCallbackInput =
72
+ | string
73
+ | URL
74
+ | URLSearchParams
75
+ | Record<string, string | string[] | undefined>;
76
+
77
+ function collectSetCookieHeaders(res: Response): string[] | undefined {
78
+ const h = res.headers as Headers & { getSetCookie?: () => string[] };
79
+ if (typeof h.getSetCookie === 'function') {
80
+ const list = h.getSetCookie();
81
+ return list.length ? list : undefined;
82
+ }
83
+ const single = res.headers.get('set-cookie');
84
+ return single ? [single] : undefined;
85
+ }
86
+
87
+ /**
88
+ * Read the OAuth refresh token from a social callback request (query `rt`).
89
+ * Accepts a full URL, a {@link URL}, `URLSearchParams`, or a query object such
90
+ * as `req.query` from Next.js or Express.
91
+ */
92
+ export function parseSocialCallbackRefreshToken(input: SocialCallbackInput): string | undefined {
93
+ if (input instanceof URL) {
94
+ return input.searchParams.get('rt') ?? undefined;
95
+ }
96
+ if (typeof input === 'string') {
97
+ try {
98
+ const u = new URL(input, 'http://qelos.social-callback.invalid');
99
+ return u.searchParams.get('rt') ?? undefined;
100
+ } catch {
101
+ return undefined;
102
+ }
103
+ }
104
+ if (input instanceof URLSearchParams) {
105
+ return input.get('rt') ?? undefined;
106
+ }
107
+ const raw = input['rt'];
108
+ if (Array.isArray(raw)) {
109
+ return raw[0] && typeof raw[0] === 'string' ? raw[0] : undefined;
110
+ }
111
+ return typeof raw === 'string' ? raw : undefined;
112
+ }
113
+
114
+ /** Lists individual `Set-Cookie` values from an exchange result for writing to a response. */
115
+ export function getSocialAuthSetCookieParts(result: SocialAuthCallbackPayload): string[] {
116
+ if (result.setCookieHeaders?.length) {
117
+ return result.setCookieHeaders;
118
+ }
119
+ const h = result.headers['set-cookie'];
120
+ return h ? [h] : [];
121
+ }
122
+
123
+ /**
124
+ * Copies session cookies from {@link QlAuthentication.exchangeAuthCallback} /
125
+ * {@link QlAuthentication.socialCallback} onto an HTTP response (Express,
126
+ * Next.js Pages `NextApiResponse`, Node `ServerResponse`, etc.).
127
+ */
128
+ export function applySocialAuthCookiesToServerResponse(
129
+ res: { setHeader(name: string, value: string | string[]): void },
130
+ result: SocialAuthCallbackPayload,
131
+ ): void {
132
+ const parts = getSocialAuthSetCookieParts(result);
133
+ if (!parts.length) return;
134
+ res.setHeader('Set-Cookie', parts.length === 1 ? parts[0]! : parts);
135
+ }
136
+
50
137
  export default class QlAuthentication extends BaseSDK {
51
138
 
52
139
  #refreshToken: string;
@@ -155,6 +242,32 @@ export default class QlAuthentication extends BaseSDK {
155
242
  })
156
243
  }
157
244
 
245
+ async refreshCookieToken(cookieToken?: string) {
246
+ const headers: Record<string, string> = {};
247
+ if (cookieToken) {
248
+ headers.authorization = 'Bearer ' + cookieToken;
249
+ }
250
+ const res = await this.callApi('/api/cookie/refresh', {
251
+ method: 'post',
252
+ headers,
253
+ });
254
+ if (!res.ok) {
255
+ throw new Error('failed to refresh cookie token');
256
+ }
257
+ const body = (await res.json()) as {
258
+ payload: BasicPayload & {
259
+ cookieToken: string;
260
+ workspace?: { _id: string } | null;
261
+ };
262
+ };
263
+ return {
264
+ ...body,
265
+ headers: {
266
+ 'set-cookie': res.headers?.get('set-cookie'),
267
+ },
268
+ };
269
+ }
270
+
158
271
  async apiTokenSignin(apiToken: string): Promise<IUser> {
159
272
  this.#apiToken = apiToken;
160
273
  this.qlOptions.apiToken = apiToken;
@@ -183,6 +296,73 @@ export default class QlAuthentication extends BaseSDK {
183
296
  return this.callApi('/api/logout', { method: 'post' })
184
297
  }
185
298
 
299
+ getSocialLoginUrl(provider: SocialProvider, options: SocialLoginOptions = {}): string {
300
+ const params: Record<string, string> = {};
301
+ if (options.state) params.state = options.state;
302
+ if (options.returnUrl) params.returnUrl = options.returnUrl;
303
+ const qs = Object.keys(params).length ? `?${new URLSearchParams(params)}` : '';
304
+ return this.buildUrl(`/api/auth/${provider}${qs}`);
305
+ }
306
+
307
+ getSocialCallbackUrl(provider: SocialProvider): string {
308
+ return this.buildUrl(`/api/auth/${provider}/callback`);
309
+ }
310
+
311
+ startSocialLogin(provider: SocialProvider, options?: SocialLoginOptions): void {
312
+ const location = (globalThis as any).location;
313
+ if (!location) {
314
+ throw new Error('startSocialLogin requires a browser environment');
315
+ }
316
+ location.href = this.getSocialLoginUrl(provider, options);
317
+ }
318
+
319
+ async exchangeAuthCallback(refreshToken: string): Promise<SocialAuthCallbackPayload> {
320
+ if (!refreshToken) {
321
+ throw new Error('refresh token is required');
322
+ }
323
+ const res = await this.callApi(`/api/auth/callback?rt=${encodeURIComponent(refreshToken)}`, {
324
+ method: 'post',
325
+ cache: 'no-store',
326
+ });
327
+ if (!res.ok) {
328
+ let detail = '';
329
+ try {
330
+ detail = (await res.text()).slice(0, 512);
331
+ } catch {
332
+ /* ignore */
333
+ }
334
+ throw new Error(
335
+ `failed to exchange refresh token: HTTP ${res.status}${detail ? ` — ${detail}` : ''}`,
336
+ );
337
+ }
338
+ const body = (await res.json()) as { payload: BasicPayload & { workspace?: any } };
339
+ const cookieParts = collectSetCookieHeaders(res);
340
+ const legacyCookie =
341
+ cookieParts?.[0] ?? res.headers?.get('set-cookie');
342
+ return {
343
+ ...body,
344
+ headers: {
345
+ 'set-cookie': legacyCookie ?? null,
346
+ },
347
+ ...(cookieParts && cookieParts.length > 1
348
+ ? { setCookieHeaders: cookieParts }
349
+ : {}),
350
+ };
351
+ }
352
+
353
+ /**
354
+ * Full social callback: read `rt` from the request URL or query and exchange
355
+ * it for a session. Use with integrator routes that receive the provider
356
+ * callback with `?rt=` (refresh token) after OAuth.
357
+ */
358
+ async socialCallback(input: SocialCallbackInput): Promise<SocialAuthCallbackPayload> {
359
+ const refreshToken = parseSocialCallbackRefreshToken(input);
360
+ if (!refreshToken) {
361
+ throw new Error('missing rt query parameter');
362
+ }
363
+ return this.exchangeAuthCallback(refreshToken);
364
+ }
365
+
186
366
  getLoggedInUser() {
187
367
  return this.callJsonApi<IUser>('/api/me')
188
368
  }
package/src/base-sdk.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  import { FetchLike, QelosSDKOptions } from './types';
2
2
 
3
+ /** Path segment of a relative URL, without query or hash — used for auth routing in extraHeaders. */
4
+ export function relativeUrlPath(relativeUrl: string): string {
5
+ const q = relativeUrl.indexOf('?');
6
+ const h = relativeUrl.indexOf('#');
7
+ let end = relativeUrl.length;
8
+ if (q !== -1) end = Math.min(end, q);
9
+ if (h !== -1) end = Math.min(end, h);
10
+ return end === relativeUrl.length ? relativeUrl : relativeUrl.slice(0, end);
11
+ }
12
+
3
13
  const ERRORS = {
4
14
  EXTRA_HEADERS: 'could not get extra headers',
5
15
  FAILED_REFRESH_TOKEN: 'could not handle failed refresh token',
@@ -15,13 +25,17 @@ export default class BaseSDK {
15
25
  this.#fetch = qlOptions.fetch || globalThis.fetch;
16
26
  }
17
27
 
28
+ protected buildUrl(relativeUrl: string): string {
29
+ return (this.#appUrl || '') + relativeUrl;
30
+ }
31
+
18
32
  async callApi(relativeUrl: string, data?: RequestInit) {
19
33
  data = data || {};
20
34
  data.headers = data.headers || {};
21
35
 
22
36
  if (this.qlOptions.extraHeaders) {
23
37
  try {
24
- const extraHeaders = await this.qlOptions.extraHeaders(relativeUrl);
38
+ const extraHeaders = await this.qlOptions.extraHeaders(relativeUrlPath(relativeUrl));
25
39
  Object.assign(data.headers, extraHeaders);
26
40
  } catch (e) {
27
41
  throw new Error(ERRORS.EXTRA_HEADERS);
@@ -81,7 +95,7 @@ export default class BaseSDK {
81
95
 
82
96
  if (this.qlOptions.extraHeaders) {
83
97
  try {
84
- headers = await this.qlOptions.extraHeaders(relativeUrl, true);
98
+ headers = await this.qlOptions.extraHeaders(relativeUrlPath(relativeUrl), true);
85
99
  } catch {
86
100
  // Ignore error during refresh attempt
87
101
  }