@qelos/sdk 3.11.10 → 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.
- package/README.md +31 -0
- package/dist/__tests__/blueprints.types.test-d.d.ts +28 -0
- package/dist/__tests__/blueprints.types.test-d.js +11 -0
- package/dist/__tests__/blueprints.types.test-d.js.map +1 -0
- package/dist/administrator/events.d.ts +5 -0
- package/dist/administrator/events.js +8 -0
- package/dist/administrator/events.js.map +1 -1
- package/dist/administrator/workspaces.d.ts +8 -1
- package/dist/administrator/workspaces.js +7 -2
- package/dist/administrator/workspaces.js.map +1 -1
- package/dist/ai/agents.d.ts +56 -0
- package/dist/ai/agents.js +116 -0
- package/dist/ai/agents.js.map +1 -0
- package/dist/ai/index.d.ts +9 -3
- package/dist/ai/index.js +8 -1
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/types.d.ts +61 -0
- package/dist/authentication.d.ts +64 -1
- package/dist/authentication.js +137 -0
- package/dist/authentication.js.map +1 -1
- package/dist/base-sdk.d.ts +3 -0
- package/dist/base-sdk.js +16 -2
- package/dist/base-sdk.js.map +1 -1
- package/dist/blueprints-entities.d.ts +26 -0
- package/dist/blueprints-entities.js +91 -5
- package/dist/blueprints-entities.js.map +1 -1
- package/dist/blueprints.d.ts +3 -0
- package/dist/blueprints.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/workspaces.d.ts +16 -0
- package/dist/workspaces.js +40 -0
- package/dist/workspaces.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/blueprints.types.test-d.ts +49 -0
- package/src/administrator/events.ts +6 -1
- package/src/administrator/workspaces.ts +10 -2
- package/src/ai/agents.ts +171 -0
- package/src/ai/index.ts +22 -4
- package/src/ai/types.ts +66 -0
- package/src/authentication.ts +181 -1
- package/src/base-sdk.ts +16 -2
- package/src/blueprints-entities.ts +113 -5
- package/src/blueprints.ts +9 -2
- package/src/index.ts +26 -1
- package/src/workspaces.ts +63 -0
- package/test/authentication-social.test.ts +48 -0
- package/test/base-sdk.test.ts +26 -1
- package/test/index.test.ts +2 -1
- package/test/qelos-sdk-refresh.test.ts +2 -1
- 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 : ''));
|
|
@@ -9,8 +9,16 @@ export default class QlAdminWorkspaces extends BaseSDK {
|
|
|
9
9
|
super(options)
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
getList() {
|
|
13
|
-
|
|
12
|
+
getList(filters?: { 'members.user'?: string; labels?: string[]; name?: string; q?: string; _id?: string[]; select?: string }) {
|
|
13
|
+
const queryParams = filters
|
|
14
|
+
? `?${Object.entries(filters)
|
|
15
|
+
.filter(([, value]) => value !== undefined)
|
|
16
|
+
.map(([key, value]) =>
|
|
17
|
+
Array.isArray(value) ? `${key}=${value.join(',')}` : `${key}=${value}`
|
|
18
|
+
).join('&')}`
|
|
19
|
+
: '';
|
|
20
|
+
|
|
21
|
+
return this.callJsonApi<IWorkspace[]>(this.relativePath + '/all' + queryParams);
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
getEncryptedData<Z = any>(workspaceId: string, encryptedId: string = '') {
|
package/src/ai/agents.ts
ADDED
|
@@ -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
|
|
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
|
+
}
|
package/src/authentication.ts
CHANGED
|
@@ -43,10 +43,97 @@ export interface IApiToken {
|
|
|
43
43
|
tokenPrefix: string;
|
|
44
44
|
expiresAt: string;
|
|
45
45
|
lastUsedAt?: string;
|
|
46
|
-
|
|
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
|
}
|