@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.
@@ -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,2 @@
1
+ export * from './custom-instance';
2
+ export * from './generated/v3-client';
@@ -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;