@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.
@@ -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 API key via Sec-WebSocket-Protocol header only — never as URL query param
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
- protocols.push(`aerostack-key.${this.apiKey}`);
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) {