@scryme/chat 0.0.1
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 +166 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +2 -0
- package/dist/custom-instance.d.ts +5 -0
- package/dist/custom-instance.js +100 -0
- package/dist/generated/v3-client.d.ts +7879 -0
- package/dist/generated/v3-client.js +6762 -0
- package/dist/generated/v3-server.d.ts +1679 -0
- package/dist/generated/v3-server.js +2050 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/sdk.d.ts +78 -0
- package/dist/sdk.js +421 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +3 -0
- package/package.json +50 -0
- package/src/__tests__/sdk.test.ts +227 -0
- package/src/client.ts +2 -0
- package/src/custom-instance.ts +118 -0
- package/src/generated/v3-client.ts +17309 -0
- package/src/generated/v3-server.ts +5225 -0
- package/src/index.ts +3 -0
- package/src/sdk.ts +298 -0
- package/src/server.ts +3 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { ScrymeSDK } from '../sdk';
|
|
4
|
+
|
|
5
|
+
vi.mock('axios', async () => {
|
|
6
|
+
const actual = await vi.importActual<typeof axios>('axios');
|
|
7
|
+
return {
|
|
8
|
+
default: {
|
|
9
|
+
...actual,
|
|
10
|
+
create: vi.fn(() => ({
|
|
11
|
+
interceptors: {
|
|
12
|
+
request: { use: vi.fn(), eject: vi.fn() },
|
|
13
|
+
response: { use: vi.fn(), eject: vi.fn() },
|
|
14
|
+
},
|
|
15
|
+
defaults: { headers: {} },
|
|
16
|
+
})),
|
|
17
|
+
post: vi.fn(),
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Mock the generated v3-server APIs
|
|
23
|
+
vi.mock('../generated/v3-server', () => {
|
|
24
|
+
return {
|
|
25
|
+
getSkyrmeChatAPI: vi.fn(() => ({
|
|
26
|
+
v3WorkspacesControllerGetWorkspaces: vi.fn(async (options) => {
|
|
27
|
+
return { success: true, data: { workspaces: [] }, options };
|
|
28
|
+
}),
|
|
29
|
+
v3WorkspacesControllerGetWorkspaceBySlug: vi.fn(async (slug, options) => {
|
|
30
|
+
return { success: true, data: { workspace: { slug } }, options };
|
|
31
|
+
}),
|
|
32
|
+
channelsControllerGetWorkspaceChannels: vi.fn(async (slug, options) => {
|
|
33
|
+
return { success: true, data: { channels: [] }, slug, options };
|
|
34
|
+
}),
|
|
35
|
+
channelsControllerCreateChannel: vi.fn(async (slug, data, options) => {
|
|
36
|
+
return { success: true, data: { slug, data }, options };
|
|
37
|
+
}),
|
|
38
|
+
channelsControllerGetMessages: vi.fn(async (channelId, params, options) => {
|
|
39
|
+
return { success: true, channelId, params, options };
|
|
40
|
+
}),
|
|
41
|
+
channelsControllerCreateMessage: vi.fn(async (channelId, options) => {
|
|
42
|
+
return { success: true, channelId, options };
|
|
43
|
+
}),
|
|
44
|
+
})),
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('ScrymeSDK', () => {
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('Initialization', () => {
|
|
54
|
+
it('should initialize with default baseURL', () => {
|
|
55
|
+
const sdk = new ScrymeSDK();
|
|
56
|
+
expect(sdk.baseURL).toContain('http://localhost:3000');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should initialize with custom baseURL', () => {
|
|
60
|
+
const sdk = new ScrymeSDK({ baseURL: 'https://custom-api.com/' });
|
|
61
|
+
expect(sdk.baseURL).toBe('https://custom-api.com');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should accept clientId and clientSecret options', () => {
|
|
65
|
+
const sdk = new ScrymeSDK({
|
|
66
|
+
clientId: 'test-client-id',
|
|
67
|
+
clientSecret: 'test-client-secret',
|
|
68
|
+
});
|
|
69
|
+
expect(sdk['clientId']).toBe('test-client-id');
|
|
70
|
+
expect(sdk['clientSecret']).toBe('test-client-secret');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should accept static token option', async () => {
|
|
74
|
+
const sdk = new ScrymeSDK({ token: 'my-static-token' });
|
|
75
|
+
const token = await sdk.getOrFetchToken();
|
|
76
|
+
expect(token).toBe('my-static-token');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('OAuth Token Retrieval & Caching', () => {
|
|
81
|
+
it('should fetch token via client_credentials and cache it', async () => {
|
|
82
|
+
const sdk = new ScrymeSDK({
|
|
83
|
+
baseURL: 'https://api.test.com',
|
|
84
|
+
clientId: 'id',
|
|
85
|
+
clientSecret: 'sec',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
vi.mocked(axios.post).mockResolvedValueOnce({
|
|
89
|
+
data: {
|
|
90
|
+
success: true,
|
|
91
|
+
data: {
|
|
92
|
+
access_token: 'fetched-m2m-token',
|
|
93
|
+
expires_in: 3600,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const token1 = await sdk.getOrFetchToken();
|
|
99
|
+
expect(token1).toBe('fetched-m2m-token');
|
|
100
|
+
expect(axios.post).toHaveBeenCalledTimes(1);
|
|
101
|
+
expect(axios.post).toHaveBeenCalledWith(
|
|
102
|
+
'https://api.test.com/api/v3/oauth/token',
|
|
103
|
+
{
|
|
104
|
+
client_id: 'id',
|
|
105
|
+
client_secret: 'sec',
|
|
106
|
+
grant_type: 'client_credentials',
|
|
107
|
+
},
|
|
108
|
+
expect.any(Object)
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Second call should return cached token without calling axios again
|
|
112
|
+
const token2 = await sdk.getOrFetchToken();
|
|
113
|
+
expect(token2).toBe('fetched-m2m-token');
|
|
114
|
+
expect(axios.post).toHaveBeenCalledTimes(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should refetch token if expired', async () => {
|
|
118
|
+
const sdk = new ScrymeSDK({
|
|
119
|
+
baseURL: 'https://api.test.com',
|
|
120
|
+
clientId: 'id',
|
|
121
|
+
clientSecret: 'sec',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
vi.mocked(axios.post)
|
|
125
|
+
.mockResolvedValueOnce({
|
|
126
|
+
data: {
|
|
127
|
+
success: true,
|
|
128
|
+
data: {
|
|
129
|
+
access_token: 'first-token',
|
|
130
|
+
expires_in: -10, // Expired immediately
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
.mockResolvedValueOnce({
|
|
135
|
+
data: {
|
|
136
|
+
success: true,
|
|
137
|
+
data: {
|
|
138
|
+
access_token: 'second-token',
|
|
139
|
+
expires_in: 3600,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const token1 = await sdk.getOrFetchToken();
|
|
145
|
+
expect(token1).toBe('first-token');
|
|
146
|
+
|
|
147
|
+
const token2 = await sdk.getOrFetchToken();
|
|
148
|
+
expect(token2).toBe('second-token');
|
|
149
|
+
expect(axios.post).toHaveBeenCalledTimes(2);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('Dynamic Proxy (sdk.raw)', () => {
|
|
154
|
+
it('should proxy calls and inject Bearer token and baseURL', async () => {
|
|
155
|
+
const sdk = new ScrymeSDK({
|
|
156
|
+
baseURL: 'https://api.test.com',
|
|
157
|
+
token: 'active-token',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const result = await sdk.raw.v3WorkspacesControllerGetWorkspaces() as any;
|
|
161
|
+
expect(result).toBeDefined();
|
|
162
|
+
expect(result.options).toBeDefined();
|
|
163
|
+
expect(result.options.baseURL).toBe('https://api.test.com/api');
|
|
164
|
+
expect(result.options.headers.Authorization).toBe('Bearer active-token');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should merge user-provided headers with injected config', async () => {
|
|
168
|
+
const sdk = new ScrymeSDK({
|
|
169
|
+
baseURL: 'https://api.test.com',
|
|
170
|
+
token: 'active-token',
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Passing options as the last parameter (since v3WorkspacesControllerGetWorkspaces has arity 1)
|
|
174
|
+
const result = await sdk.raw.v3WorkspacesControllerGetWorkspaces({
|
|
175
|
+
headers: {
|
|
176
|
+
'X-Custom-Header': 'CustomValue',
|
|
177
|
+
},
|
|
178
|
+
} as any) as any;
|
|
179
|
+
|
|
180
|
+
expect(result.options.headers.Authorization).toBe('Bearer active-token');
|
|
181
|
+
expect(result.options.headers['X-Custom-Header']).toBe('CustomValue');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('Nested DX Helper Namespaces', () => {
|
|
186
|
+
it('should support workspace namespace', async () => {
|
|
187
|
+
const sdk = new ScrymeSDK({
|
|
188
|
+
baseURL: 'https://api.test.com',
|
|
189
|
+
token: 'active-token',
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const listRes = await sdk.workspace.list() as any;
|
|
193
|
+
expect(listRes.success).toBe(true);
|
|
194
|
+
|
|
195
|
+
const getRes = await sdk.workspace.get('acme') as any;
|
|
196
|
+
expect(getRes.data.workspace.slug).toBe('acme');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should support workspace channels namespace', async () => {
|
|
200
|
+
const sdk = new ScrymeSDK({
|
|
201
|
+
baseURL: 'https://api.test.com',
|
|
202
|
+
token: 'active-token',
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const channelsRes = await sdk.workspace.channels.list('acme-corp') as any;
|
|
206
|
+
expect(channelsRes.slug).toBe('acme-corp');
|
|
207
|
+
|
|
208
|
+
const createRes = await sdk.workspace.channels.create('acme-corp', { name: 'general' }) as any;
|
|
209
|
+
expect(createRes.data.slug).toBe('acme-corp');
|
|
210
|
+
expect(createRes.data.data.name).toBe('general');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should support channel and message namespaces', async () => {
|
|
214
|
+
const sdk = new ScrymeSDK({
|
|
215
|
+
baseURL: 'https://api.test.com',
|
|
216
|
+
token: 'active-token',
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const messagesRes = await sdk.channel.message.list('chan_123', { limit: 10 }) as any;
|
|
220
|
+
expect(messagesRes.channelId).toBe('chan_123');
|
|
221
|
+
expect(messagesRes.params.limit).toBe(10);
|
|
222
|
+
|
|
223
|
+
const createMsgRes = await sdk.channel.message.create('chan_123') as any;
|
|
224
|
+
expect(createMsgRes.channelId).toBe('chan_123');
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import axios, { AxiosRequestConfig, AxiosError } from 'axios';
|
|
2
|
+
|
|
3
|
+
// Helper to safely access env variables across Vite, Next.js and React Native
|
|
4
|
+
const getEnv = (name: string) => {
|
|
5
|
+
const g = globalThis as typeof globalThis & {
|
|
6
|
+
process?: { env?: Record<string, string> };
|
|
7
|
+
import?: { meta?: { env?: Record<string, string> } };
|
|
8
|
+
__env__?: Record<string, string>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Try various common locations for env variables
|
|
12
|
+
// Avoid explicit import.meta to prevent TS1470
|
|
13
|
+
const env = g.process?.env || g.import?.meta?.env || g.__env__;
|
|
14
|
+
|
|
15
|
+
if (!env) return undefined;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
env[name] || env[`VITE_${name}`] || env[`NEXT_PUBLIC_${name}`] || env[`EXPO_PUBLIC_${name}`] || env[`TAURI_${name}`]
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const getBaseURL = () => {
|
|
23
|
+
let url = '';
|
|
24
|
+
if (typeof window !== 'undefined') {
|
|
25
|
+
const customUrl = window.localStorage.getItem('CUSTOM_API_URL');
|
|
26
|
+
if (customUrl) {
|
|
27
|
+
url = customUrl;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (!url) {
|
|
31
|
+
const isProd =
|
|
32
|
+
(typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'production') ||
|
|
33
|
+
getEnv('NODE_ENV') === 'production' ||
|
|
34
|
+
(typeof window !== 'undefined' && window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1');
|
|
35
|
+
url = getEnv('API_URL') || getEnv('NEXT_PUBLIC_API_URL') || (isProd ? 'https://api.chat.scryme.tech' : 'http://localhost:3000');
|
|
36
|
+
}
|
|
37
|
+
return url.replace(/\/$/, '') + '/api';
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const AXIOS_INSTANCE = axios.create({
|
|
41
|
+
baseURL: getBaseURL(),
|
|
42
|
+
timeout: 10000,
|
|
43
|
+
withCredentials: true,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
AXIOS_INSTANCE.interceptors.request.use(config => {
|
|
47
|
+
if (typeof window !== 'undefined') {
|
|
48
|
+
const getCookie = (name: string) => {
|
|
49
|
+
const value = `; ${document.cookie}`;
|
|
50
|
+
const parts = value.split(`; ${name}=`);
|
|
51
|
+
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
|
52
|
+
return null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
let token =
|
|
56
|
+
window.localStorage.getItem('better-auth.session-token') ||
|
|
57
|
+
window.localStorage.getItem('better-auth.session_token') ||
|
|
58
|
+
window.localStorage.getItem('bearer_token');
|
|
59
|
+
|
|
60
|
+
if (!token) {
|
|
61
|
+
token =
|
|
62
|
+
getCookie('better-auth.session_token') ||
|
|
63
|
+
getCookie('better-auth.session-token') ||
|
|
64
|
+
getCookie('bearer_token');
|
|
65
|
+
if (token) {
|
|
66
|
+
window.localStorage.setItem('better-auth.session_token', token);
|
|
67
|
+
window.localStorage.setItem('better-auth.session-token', token);
|
|
68
|
+
window.localStorage.setItem('bearer_token', token);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
// Keep everything in sync
|
|
72
|
+
if (!window.localStorage.getItem('bearer_token')) {
|
|
73
|
+
window.localStorage.setItem('bearer_token', token);
|
|
74
|
+
}
|
|
75
|
+
if (!window.localStorage.getItem('better-auth.session_token')) {
|
|
76
|
+
window.localStorage.setItem('better-auth.session_token', token);
|
|
77
|
+
}
|
|
78
|
+
if (!window.localStorage.getItem('better-auth.session-token')) {
|
|
79
|
+
window.localStorage.setItem('better-auth.session-token', token);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (token) {
|
|
84
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return config;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export const customInstance = <T>(
|
|
91
|
+
config: AxiosRequestConfig,
|
|
92
|
+
options?: AxiosRequestConfig
|
|
93
|
+
): Promise<T> => {
|
|
94
|
+
const source = axios.CancelToken.source();
|
|
95
|
+
|
|
96
|
+
// Merge headers carefully so that options.headers does not overwrite config.headers completely
|
|
97
|
+
const mergedHeaders = {
|
|
98
|
+
...config.headers,
|
|
99
|
+
...options?.headers,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const promise = AXIOS_INSTANCE({
|
|
103
|
+
...config,
|
|
104
|
+
...options,
|
|
105
|
+
headers: mergedHeaders,
|
|
106
|
+
cancelToken: source.token,
|
|
107
|
+
}).then(({ data }) => data);
|
|
108
|
+
|
|
109
|
+
// @ts-ignore
|
|
110
|
+
promise.cancel = () => {
|
|
111
|
+
source.cancel('Query was cancelled by React Query');
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return promise;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export type ErrorType<Error> = AxiosError<Error>;
|
|
118
|
+
export type BodyType<Body> = Body;
|