@aerostack/sdk-web 0.8.4 → 0.8.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/dist/commonjs/__tests__/realtime.test.d.ts +2 -0
- package/dist/commonjs/__tests__/realtime.test.d.ts.map +1 -0
- package/dist/commonjs/__tests__/realtime.test.js +326 -0
- package/dist/commonjs/__tests__/realtime.test.js.map +1 -0
- package/dist/commonjs/__tests__/sdk.test.d.ts +2 -0
- package/dist/commonjs/__tests__/sdk.test.d.ts.map +1 -0
- package/dist/commonjs/__tests__/sdk.test.js +159 -0
- package/dist/commonjs/__tests__/sdk.test.js.map +1 -0
- package/dist/commonjs/realtime.d.ts +2 -0
- package/dist/commonjs/realtime.d.ts.map +1 -1
- package/dist/commonjs/realtime.js +11 -5
- package/dist/commonjs/realtime.js.map +1 -1
- package/dist/esm/__tests__/realtime.test.d.ts +2 -0
- package/dist/esm/__tests__/realtime.test.d.ts.map +1 -0
- package/dist/esm/__tests__/realtime.test.js +324 -0
- package/dist/esm/__tests__/realtime.test.js.map +1 -0
- package/dist/esm/__tests__/sdk.test.d.ts +2 -0
- package/dist/esm/__tests__/sdk.test.d.ts.map +1 -0
- package/dist/esm/__tests__/sdk.test.js +157 -0
- package/dist/esm/__tests__/sdk.test.js.map +1 -0
- package/dist/esm/realtime.d.ts +2 -0
- package/dist/esm/realtime.d.ts.map +1 -1
- package/dist/esm/realtime.js +11 -5
- package/dist/esm/realtime.js.map +1 -1
- package/examples/e2e/__tests__/e2e.test.ts +69 -0
- package/examples/e2e/package.json +15 -0
- package/examples/e2e/vitest.config.ts +8 -0
- package/package.json +2 -2
- package/src/__tests__/realtime.test.ts +388 -0
- package/src/__tests__/sdk.test.ts +181 -0
- package/src/realtime.ts +10 -5
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock the generated APIs and realtime
|
|
4
|
+
vi.mock('../_generated/index.js', () => {
|
|
5
|
+
class MockConfiguration {
|
|
6
|
+
basePath: string;
|
|
7
|
+
headers: Record<string, string>;
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
constructor(opts: any = {}) {
|
|
10
|
+
this.basePath = opts.basePath || '';
|
|
11
|
+
this.headers = opts.headers || {};
|
|
12
|
+
this.apiKey = opts.apiKey;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
Configuration: MockConfiguration,
|
|
17
|
+
AuthenticationApi: vi.fn().mockImplementation(() => ({ auth: true })),
|
|
18
|
+
AIApi: vi.fn().mockImplementation(() => ({ ai: true })),
|
|
19
|
+
StorageApi: vi.fn().mockImplementation(() => ({ storage: true })),
|
|
20
|
+
DatabaseApi: vi.fn().mockImplementation(() => ({
|
|
21
|
+
dbQuery: vi.fn().mockResolvedValue({ results: [] }),
|
|
22
|
+
})),
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
vi.mock('../realtime.js', () => {
|
|
27
|
+
return {
|
|
28
|
+
RealtimeClient: vi.fn().mockImplementation(() => ({
|
|
29
|
+
connect: vi.fn(),
|
|
30
|
+
disconnect: vi.fn(),
|
|
31
|
+
channel: vi.fn(),
|
|
32
|
+
})),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
import { SDK, Aerostack, createClient } from '../sdk.js';
|
|
37
|
+
|
|
38
|
+
describe('Web SDK', () => {
|
|
39
|
+
describe('constructor', () => {
|
|
40
|
+
it('should initialize with default options', () => {
|
|
41
|
+
const sdk = new SDK();
|
|
42
|
+
expect(sdk).toBeDefined();
|
|
43
|
+
expect(sdk.auth).toBeDefined();
|
|
44
|
+
expect(sdk.ai).toBeDefined();
|
|
45
|
+
expect(sdk.storage).toBeDefined();
|
|
46
|
+
expect(sdk.database).toBeDefined();
|
|
47
|
+
expect(sdk.realtime).toBeDefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should accept apiKey option', () => {
|
|
51
|
+
const sdk = new SDK({ apiKey: 'my-key' });
|
|
52
|
+
expect(sdk).toBeDefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should accept apiKeyAuth alias', () => {
|
|
56
|
+
const sdk = new SDK({ apiKeyAuth: 'my-key' });
|
|
57
|
+
expect(sdk).toBeDefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should accept serverUrl option', () => {
|
|
61
|
+
const sdk = new SDK({ serverUrl: 'https://custom.com/v1' });
|
|
62
|
+
expect(sdk).toBeDefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should accept serverURL alias', () => {
|
|
66
|
+
const sdk = new SDK({ serverURL: 'https://custom.com/v1' });
|
|
67
|
+
expect(sdk).toBeDefined();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should not expose queue, services, gateway, cache (client-safe)', () => {
|
|
71
|
+
const sdk = new SDK() as any;
|
|
72
|
+
expect(sdk.queue).toBeUndefined();
|
|
73
|
+
expect(sdk.services).toBeUndefined();
|
|
74
|
+
expect(sdk.gateway).toBeUndefined();
|
|
75
|
+
expect(sdk.cache).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('DatabaseFacade', () => {
|
|
80
|
+
it('should execute a query', async () => {
|
|
81
|
+
const sdk = new SDK({ apiKey: 'key' });
|
|
82
|
+
const result = await sdk.database.dbQuery({
|
|
83
|
+
dbQueryRequest: { sql: 'SELECT 1', params: [] },
|
|
84
|
+
});
|
|
85
|
+
expect(result).toEqual({ results: [] });
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('setApiKey', () => {
|
|
90
|
+
it('should recreate service instances', () => {
|
|
91
|
+
const sdk = new SDK({ apiKey: 'old-key' });
|
|
92
|
+
sdk.setApiKey('new-key');
|
|
93
|
+
expect(sdk.auth).toBeDefined();
|
|
94
|
+
expect(sdk.ai).toBeDefined();
|
|
95
|
+
expect(sdk.storage).toBeDefined();
|
|
96
|
+
expect(sdk.database).toBeDefined();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('streamGateway', () => {
|
|
101
|
+
it('should make POST to gateway endpoint', async () => {
|
|
102
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
103
|
+
ok: true,
|
|
104
|
+
body: createMockStream('data: {"choices":[{"delta":{"content":"Hi"}}]}\n\ndata: [DONE]\n\n'),
|
|
105
|
+
});
|
|
106
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
107
|
+
|
|
108
|
+
const sdk = new SDK({ serverUrl: 'https://api.test.com/v1' });
|
|
109
|
+
const result = await sdk.streamGateway({
|
|
110
|
+
apiSlug: 'bot',
|
|
111
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
112
|
+
consumerKey: 'ask_live_123',
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(result.text).toBe('Hi');
|
|
116
|
+
expect(mockFetch.mock.calls[0][0]).toBe('https://api.test.com/api/gateway/bot/v1/chat/completions');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should throw on non-OK response', async () => {
|
|
120
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
121
|
+
ok: false,
|
|
122
|
+
status: 401,
|
|
123
|
+
json: vi.fn().mockResolvedValue({ error: 'Unauthorized' }),
|
|
124
|
+
});
|
|
125
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
126
|
+
|
|
127
|
+
const sdk = new SDK({ serverUrl: 'https://api.test.com/v1' });
|
|
128
|
+
await expect(sdk.streamGateway({
|
|
129
|
+
apiSlug: 'bot',
|
|
130
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
131
|
+
})).rejects.toThrow('Unauthorized');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should estimate tokens when usage not provided', async () => {
|
|
135
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
136
|
+
ok: true,
|
|
137
|
+
body: createMockStream('data: {"choices":[{"delta":{"content":"Hello world"}}]}\n\ndata: [DONE]\n\n'),
|
|
138
|
+
});
|
|
139
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
140
|
+
|
|
141
|
+
const sdk = new SDK({ serverUrl: 'https://api.test.com/v1' });
|
|
142
|
+
const result = await sdk.streamGateway({
|
|
143
|
+
apiSlug: 'bot',
|
|
144
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(result.tokensUsed).toBeGreaterThan(0);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('Aerostack alias', () => {
|
|
152
|
+
it('should be the same as SDK', () => {
|
|
153
|
+
expect(Aerostack).toBe(SDK);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('createClient', () => {
|
|
158
|
+
it('should return an SDK instance', () => {
|
|
159
|
+
const client = createClient({ apiKey: 'key' });
|
|
160
|
+
expect(client).toBeInstanceOf(SDK);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
function createMockStream(text: string) {
|
|
166
|
+
const encoder = new TextEncoder();
|
|
167
|
+
const data = encoder.encode(text);
|
|
168
|
+
let read = false;
|
|
169
|
+
return {
|
|
170
|
+
getReader: () => ({
|
|
171
|
+
read: async () => {
|
|
172
|
+
if (!read) {
|
|
173
|
+
read = true;
|
|
174
|
+
return { done: false, value: data };
|
|
175
|
+
}
|
|
176
|
+
return { done: true, value: undefined };
|
|
177
|
+
},
|
|
178
|
+
cancel: vi.fn(),
|
|
179
|
+
}),
|
|
180
|
+
};
|
|
181
|
+
}
|
package/src/realtime.ts
CHANGED
|
@@ -86,6 +86,7 @@ export class RealtimeSubscription<T = any> {
|
|
|
86
86
|
});
|
|
87
87
|
this.isSubscribed = false;
|
|
88
88
|
this.callbacks.clear();
|
|
89
|
+
this.client._removeSubscription(this.topic);
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
// ─── Phase 1: Pub/Sub — Publish custom events ─────────────────────────
|
|
@@ -228,14 +229,13 @@ export class RealtimeClient {
|
|
|
228
229
|
const url = new URL(this.baseUrl);
|
|
229
230
|
url.searchParams.set('projectId', this.projectId);
|
|
230
231
|
if (this.userId) url.searchParams.set('userId', this.userId);
|
|
231
|
-
if (this.token) url.searchParams.set('token', this.token);
|
|
232
232
|
|
|
233
|
-
// SECURITY: Pass
|
|
233
|
+
// SECURITY: Pass credentials via Sec-WebSocket-Protocol header — never as URL query params
|
|
234
234
|
// (URL params appear in CDN logs, browser history, and Referer headers).
|
|
235
235
|
const protocols: string[] = [];
|
|
236
|
-
if (this.apiKey) {
|
|
237
|
-
|
|
238
|
-
|
|
236
|
+
if (this.apiKey) protocols.push(`aerostack-key.${this.apiKey}`);
|
|
237
|
+
if (this.token) protocols.push(`aerostack-token.${this.token}`);
|
|
238
|
+
if (protocols.length > 0) protocols.push('aerostack-v1');
|
|
239
239
|
|
|
240
240
|
this.ws = protocols.length > 0
|
|
241
241
|
? new WebSocket(url.toString(), protocols)
|
|
@@ -325,6 +325,11 @@ export class RealtimeClient {
|
|
|
325
325
|
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
+
/** @internal — Remove a subscription from the map (called on unsubscribe) */
|
|
329
|
+
_removeSubscription(topic: string): void {
|
|
330
|
+
this.subscriptions.delete(topic);
|
|
331
|
+
}
|
|
332
|
+
|
|
328
333
|
/** @internal */
|
|
329
334
|
_send(data: any) {
|
|
330
335
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|