@tthr/vue 0.0.84 → 0.0.86
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/nuxt/module.js +2 -2
- package/nuxt/module.ts +2 -2
- package/nuxt/runtime/composables.ts +638 -0
- package/nuxt/runtime/plugin.client.ts +34 -0
- package/nuxt/runtime/server/mutation.post.js +30 -364
- package/nuxt/runtime/server/mutation.post.ts +53 -0
- package/nuxt/runtime/server/plugins/cron.ts +377 -0
- package/nuxt/runtime/server/query.post.js +27 -362
- package/nuxt/runtime/server/query.post.ts +51 -0
- package/nuxt/runtime/server/utils/handler.js +318 -0
- package/nuxt/runtime/server/utils/handler.ts +375 -0
- package/nuxt/runtime/server/utils/tether.js +187 -210
- package/nuxt/runtime/server/utils/tether.ts +292 -0
- package/package.json +7 -9
- package/dist/nuxt.d.ts +0 -14
- package/dist/nuxt.d.ts.map +0 -1
- package/dist/nuxt.js +0 -48
- package/dist/nuxt.js.map +0 -1
- package/dist/runtime/composables.d.ts +0 -73
- package/dist/runtime/composables.d.ts.map +0 -1
- package/dist/runtime/composables.js +0 -112
- package/dist/runtime/composables.js.map +0 -1
- package/dist/runtime/plugin.d.ts +0 -11
- package/dist/runtime/plugin.d.ts.map +0 -1
- package/dist/runtime/plugin.js +0 -33
- package/dist/runtime/plugin.js.map +0 -1
- package/nuxt/runtime/composables.d.ts +0 -142
- package/nuxt/runtime/composables.d.ts.map +0 -1
- package/nuxt/runtime/composables.js +0 -480
- package/nuxt/runtime/composables.js.map +0 -1
- package/nuxt/runtime/plugin.client.d.ts +0 -17
- package/nuxt/runtime/plugin.client.d.ts.map +0 -1
- package/nuxt/runtime/plugin.client.js +0 -20
- package/nuxt/runtime/plugin.client.js.map +0 -1
- package/nuxt/runtime/server/plugins/cron.d.ts +0 -38
- package/nuxt/runtime/server/plugins/cron.d.ts.map +0 -1
- package/nuxt/runtime/server/plugins/cron.js.map +0 -1
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared handler utilities for Tether server routes
|
|
3
|
+
*
|
|
4
|
+
* Provides function registry loading, database proxy creation,
|
|
5
|
+
* auth context building, and logging — used by both query and mutation handlers.
|
|
6
|
+
*/
|
|
7
|
+
import { createError, getHeader, getCookie } from 'h3';
|
|
8
|
+
import { useRuntimeConfig } from '#imports';
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Logging
|
|
11
|
+
// ============================================================================
|
|
12
|
+
let verboseLogging = false;
|
|
13
|
+
const PREFIX = '[Tether]';
|
|
14
|
+
export const log = {
|
|
15
|
+
debug: (...args) => { if (verboseLogging)
|
|
16
|
+
console.log(PREFIX, ...args); },
|
|
17
|
+
info: (...args) => console.log(PREFIX, ...args),
|
|
18
|
+
warn: (...args) => console.warn(PREFIX, ...args),
|
|
19
|
+
error: (...args) => console.error(PREFIX, ...args),
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Read and validate Tether config from runtime config + env vars.
|
|
23
|
+
* Throws H3 errors if required values are missing.
|
|
24
|
+
*/
|
|
25
|
+
export function getConfig() {
|
|
26
|
+
const config = useRuntimeConfig();
|
|
27
|
+
verboseLogging = config.tether?.verbose || process.env.TETHER_VERBOSE === 'true';
|
|
28
|
+
const apiKey = config.tether?.apiKey || process.env.TETHER_API_KEY;
|
|
29
|
+
const url = config.tether?.url || process.env.TETHER_URL || 'https://tether-api.strands.gg';
|
|
30
|
+
const projectId = config.tether?.projectId || process.env.TETHER_PROJECT_ID;
|
|
31
|
+
if (!apiKey) {
|
|
32
|
+
throw createError({
|
|
33
|
+
statusCode: 500,
|
|
34
|
+
message: 'Tether API key not configured. Set TETHER_API_KEY environment variable.',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (!projectId) {
|
|
38
|
+
throw createError({
|
|
39
|
+
statusCode: 500,
|
|
40
|
+
message: 'Tether project ID not configured.',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return { apiKey, url, projectId };
|
|
44
|
+
}
|
|
45
|
+
let functionRegistry = null;
|
|
46
|
+
let registryError = null;
|
|
47
|
+
/**
|
|
48
|
+
* Load the user's custom functions from ~/tether/functions (once).
|
|
49
|
+
*/
|
|
50
|
+
async function loadFunctionRegistry() {
|
|
51
|
+
if (functionRegistry !== null || registryError !== null)
|
|
52
|
+
return;
|
|
53
|
+
try {
|
|
54
|
+
log.debug('Loading custom functions from ~~/tether/functions/index.ts');
|
|
55
|
+
const functions = await import('~~/tether/functions/index.ts').catch((err) => {
|
|
56
|
+
log.debug('Failed to import functions:', err.message);
|
|
57
|
+
return null;
|
|
58
|
+
});
|
|
59
|
+
if (functions) {
|
|
60
|
+
log.debug('Loaded function modules:', Object.keys(functions));
|
|
61
|
+
functionRegistry = functions;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
log.debug('No custom functions found, will proxy all requests');
|
|
65
|
+
functionRegistry = {};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
70
|
+
log.debug('Could not load custom functions:', err.message);
|
|
71
|
+
registryError = err;
|
|
72
|
+
functionRegistry = {};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Look up a function by name (e.g. "channel.getMyChannels").
|
|
77
|
+
* Returns null if no custom function is registered.
|
|
78
|
+
*/
|
|
79
|
+
export async function lookupFunction(name) {
|
|
80
|
+
await loadFunctionRegistry();
|
|
81
|
+
if (!functionRegistry || !name)
|
|
82
|
+
return null;
|
|
83
|
+
const parts = name.split('.');
|
|
84
|
+
if (parts.length !== 2)
|
|
85
|
+
return null;
|
|
86
|
+
const [moduleName, fnName] = parts;
|
|
87
|
+
const mod = functionRegistry[moduleName];
|
|
88
|
+
if (!mod)
|
|
89
|
+
return null;
|
|
90
|
+
const fn = mod[fnName];
|
|
91
|
+
if (fn && typeof fn === 'object' && typeof fn.handler === 'function') {
|
|
92
|
+
return fn;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Database Proxy
|
|
98
|
+
// ============================================================================
|
|
99
|
+
/**
|
|
100
|
+
* Create a database proxy that routes calls to Tether's CRUD endpoints.
|
|
101
|
+
*/
|
|
102
|
+
export function createDatabaseProxy(apiKey, url, projectId) {
|
|
103
|
+
return new Proxy({}, {
|
|
104
|
+
get(_target, tableName) {
|
|
105
|
+
const makeRequest = async (operation, args) => {
|
|
106
|
+
const response = await fetch(`${url}/api/v1/projects/${projectId}/query`, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: {
|
|
109
|
+
'Content-Type': 'application/json',
|
|
110
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
111
|
+
},
|
|
112
|
+
body: JSON.stringify({
|
|
113
|
+
function: `${tableName}.${operation}`,
|
|
114
|
+
args,
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
119
|
+
throw new Error(error.error || `Database operation failed: ${tableName}.${operation}`);
|
|
120
|
+
}
|
|
121
|
+
const result = await response.json();
|
|
122
|
+
return result.data;
|
|
123
|
+
};
|
|
124
|
+
const makeMutation = async (operation, args) => {
|
|
125
|
+
const response = await fetch(`${url}/api/v1/projects/${projectId}/mutation`, {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: {
|
|
128
|
+
'Content-Type': 'application/json',
|
|
129
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
130
|
+
},
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
function: `${tableName}.${operation}`,
|
|
133
|
+
args,
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
138
|
+
throw new Error(error.error || `Database operation failed: ${tableName}.${operation}`);
|
|
139
|
+
}
|
|
140
|
+
const result = await response.json();
|
|
141
|
+
return result.data;
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
findMany: async (options) => {
|
|
145
|
+
const args = {};
|
|
146
|
+
if (options?.where)
|
|
147
|
+
args.where = options.where;
|
|
148
|
+
if (options?.limit)
|
|
149
|
+
args.limit = options.limit;
|
|
150
|
+
if (options?.offset)
|
|
151
|
+
args.offset = options.offset;
|
|
152
|
+
if (options?.orderBy) {
|
|
153
|
+
if (typeof options.orderBy === 'string') {
|
|
154
|
+
args.orderBy = options.orderBy;
|
|
155
|
+
}
|
|
156
|
+
else if (typeof options.orderBy === 'object') {
|
|
157
|
+
const [column, dir] = Object.entries(options.orderBy)[0] || [];
|
|
158
|
+
if (column) {
|
|
159
|
+
args.orderBy = column;
|
|
160
|
+
args.orderDir = dir?.toUpperCase?.() || 'ASC';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (options?.orderDir)
|
|
165
|
+
args.orderDir = options.orderDir;
|
|
166
|
+
return makeRequest('list', args);
|
|
167
|
+
},
|
|
168
|
+
findFirst: async (options) => {
|
|
169
|
+
const args = { limit: 1 };
|
|
170
|
+
if (options?.where)
|
|
171
|
+
args.where = options.where;
|
|
172
|
+
const results = await makeRequest('list', args);
|
|
173
|
+
return results?.[0] ?? null;
|
|
174
|
+
},
|
|
175
|
+
findUnique: async (options) => {
|
|
176
|
+
const args = { limit: 1 };
|
|
177
|
+
if (options?.where)
|
|
178
|
+
args.where = options.where;
|
|
179
|
+
const results = await makeRequest('list', args);
|
|
180
|
+
return results?.[0] ?? null;
|
|
181
|
+
},
|
|
182
|
+
findById: async (id) => {
|
|
183
|
+
return makeRequest('get', { id });
|
|
184
|
+
},
|
|
185
|
+
count: async (options) => {
|
|
186
|
+
const args = {};
|
|
187
|
+
if (options?.where)
|
|
188
|
+
args.where = options.where;
|
|
189
|
+
const result = await makeRequest('count', args);
|
|
190
|
+
return result?.count ?? 0;
|
|
191
|
+
},
|
|
192
|
+
insert: async (data) => {
|
|
193
|
+
return makeMutation('create', { data });
|
|
194
|
+
},
|
|
195
|
+
insertMany: async (items) => {
|
|
196
|
+
const results = [];
|
|
197
|
+
for (const data of items) {
|
|
198
|
+
const result = await makeMutation('create', { data });
|
|
199
|
+
results.push(result);
|
|
200
|
+
}
|
|
201
|
+
return results;
|
|
202
|
+
},
|
|
203
|
+
create: async (options) => {
|
|
204
|
+
return makeMutation('create', { data: options.data });
|
|
205
|
+
},
|
|
206
|
+
update: async (options) => {
|
|
207
|
+
const id = options.where?._id ?? options.where?.id;
|
|
208
|
+
if (!id) {
|
|
209
|
+
throw new Error('Update requires an _id in the where clause');
|
|
210
|
+
}
|
|
211
|
+
const result = await makeMutation('update', { id, data: options.data });
|
|
212
|
+
return result?.rowsAffected ?? 0;
|
|
213
|
+
},
|
|
214
|
+
upsert: async (options) => {
|
|
215
|
+
const results = await makeRequest('list', { where: options.where, limit: 1 }).catch(() => []);
|
|
216
|
+
const existing = results?.[0] ?? null;
|
|
217
|
+
if (existing) {
|
|
218
|
+
const id = existing._id ?? existing.id;
|
|
219
|
+
if (!id) {
|
|
220
|
+
throw new Error('Found record has no _id or id field for update');
|
|
221
|
+
}
|
|
222
|
+
await makeMutation('update', { id, data: options.update });
|
|
223
|
+
return { ...existing, ...options.update };
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
return makeMutation('create', { data: options.create });
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
delete: async (options) => {
|
|
230
|
+
const id = options.where?._id ?? options.where?.id;
|
|
231
|
+
if (id) {
|
|
232
|
+
const result = await makeMutation('delete', { id });
|
|
233
|
+
return result?.rowsAffected ?? 0;
|
|
234
|
+
}
|
|
235
|
+
const result = await makeMutation('deleteMany', { where: options.where });
|
|
236
|
+
return result?.rowsAffected ?? 0;
|
|
237
|
+
},
|
|
238
|
+
deleteById: async (id) => {
|
|
239
|
+
const result = await makeMutation('delete', { id });
|
|
240
|
+
return (result?.rowsAffected ?? 0) > 0;
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Build auth context by extracting and validating tokens from the request.
|
|
248
|
+
*/
|
|
249
|
+
export async function buildAuthContext(event, url, projectId) {
|
|
250
|
+
let userIdentity = null;
|
|
251
|
+
const authHeader = getHeader(event, 'authorization');
|
|
252
|
+
const strandsToken = getHeader(event, 'x-strands-token');
|
|
253
|
+
const cookieToken = getCookie(event, 'strands_oauth_token');
|
|
254
|
+
if (strandsToken || cookieToken || authHeader?.startsWith('Bearer ')) {
|
|
255
|
+
const token = strandsToken || cookieToken || authHeader?.replace('Bearer ', '');
|
|
256
|
+
if (token) {
|
|
257
|
+
try {
|
|
258
|
+
const authResponse = await fetch(`${url}/api/v1/projects/${projectId}/auth/validate`, {
|
|
259
|
+
method: 'POST',
|
|
260
|
+
headers: { 'Content-Type': 'application/json' },
|
|
261
|
+
body: JSON.stringify({ accessToken: token }),
|
|
262
|
+
});
|
|
263
|
+
if (authResponse.ok) {
|
|
264
|
+
const authData = await authResponse.json();
|
|
265
|
+
if (authData.valid) {
|
|
266
|
+
userIdentity = {
|
|
267
|
+
subject: authData.userId,
|
|
268
|
+
email: authData.email,
|
|
269
|
+
name: authData.name,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch (authError) {
|
|
275
|
+
const err = authError instanceof Error ? authError : new Error(String(authError));
|
|
276
|
+
log.warn('Auth validation failed:', err.message);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const auth = {
|
|
281
|
+
getUserIdentity: async () => userIdentity,
|
|
282
|
+
};
|
|
283
|
+
const ctx = {
|
|
284
|
+
auth: {
|
|
285
|
+
userId: userIdentity?.subject ?? null,
|
|
286
|
+
claims: userIdentity ?? {},
|
|
287
|
+
},
|
|
288
|
+
userId: userIdentity?.subject ?? null,
|
|
289
|
+
};
|
|
290
|
+
return { auth, ctx };
|
|
291
|
+
}
|
|
292
|
+
// ============================================================================
|
|
293
|
+
// Proxy to Tether API
|
|
294
|
+
// ============================================================================
|
|
295
|
+
/**
|
|
296
|
+
* Proxy a request directly to the Tether API (for generic CRUD when no custom function exists).
|
|
297
|
+
*/
|
|
298
|
+
export async function proxyToTetherApi(config, endpoint, functionName, args) {
|
|
299
|
+
const response = await fetch(`${config.url}/api/v1/projects/${config.projectId}/${endpoint}`, {
|
|
300
|
+
method: 'POST',
|
|
301
|
+
headers: {
|
|
302
|
+
'Content-Type': 'application/json',
|
|
303
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
304
|
+
},
|
|
305
|
+
body: JSON.stringify({
|
|
306
|
+
function: functionName,
|
|
307
|
+
args,
|
|
308
|
+
}),
|
|
309
|
+
});
|
|
310
|
+
if (!response.ok) {
|
|
311
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
312
|
+
throw createError({
|
|
313
|
+
statusCode: response.status,
|
|
314
|
+
message: error.error || `${endpoint === 'query' ? 'Query' : 'Mutation'} failed`,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return response.json();
|
|
318
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared handler utilities for Tether server routes
|
|
3
|
+
*
|
|
4
|
+
* Provides function registry loading, database proxy creation,
|
|
5
|
+
* auth context building, and logging — used by both query and mutation handlers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createError, getHeader, getCookie, type H3Event } from 'h3';
|
|
9
|
+
import { useRuntimeConfig } from '#imports';
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Logging
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
let verboseLogging = false;
|
|
16
|
+
const PREFIX = '[Tether]';
|
|
17
|
+
|
|
18
|
+
export const log = {
|
|
19
|
+
debug: (...args: unknown[]) => { if (verboseLogging) console.log(PREFIX, ...args); },
|
|
20
|
+
info: (...args: unknown[]) => console.log(PREFIX, ...args),
|
|
21
|
+
warn: (...args: unknown[]) => console.warn(PREFIX, ...args),
|
|
22
|
+
error: (...args: unknown[]) => console.error(PREFIX, ...args),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Config
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
export interface TetherServerConfig {
|
|
30
|
+
apiKey: string;
|
|
31
|
+
url: string;
|
|
32
|
+
projectId: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read and validate Tether config from runtime config + env vars.
|
|
37
|
+
* Throws H3 errors if required values are missing.
|
|
38
|
+
*/
|
|
39
|
+
export function getConfig(): TetherServerConfig {
|
|
40
|
+
const config = useRuntimeConfig();
|
|
41
|
+
verboseLogging = config.tether?.verbose || process.env.TETHER_VERBOSE === 'true';
|
|
42
|
+
|
|
43
|
+
const apiKey = config.tether?.apiKey || process.env.TETHER_API_KEY;
|
|
44
|
+
const url = config.tether?.url || process.env.TETHER_URL || 'https://tether-api.strands.gg';
|
|
45
|
+
const projectId = config.tether?.projectId || process.env.TETHER_PROJECT_ID;
|
|
46
|
+
|
|
47
|
+
if (!apiKey) {
|
|
48
|
+
throw createError({
|
|
49
|
+
statusCode: 500,
|
|
50
|
+
message: 'Tether API key not configured. Set TETHER_API_KEY environment variable.',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!projectId) {
|
|
55
|
+
throw createError({
|
|
56
|
+
statusCode: 500,
|
|
57
|
+
message: 'Tether project ID not configured.',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { apiKey, url, projectId };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Function Registry
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
interface TetherFunction {
|
|
69
|
+
handler: (ctx: unknown) => Promise<unknown>;
|
|
70
|
+
[key: string]: unknown;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type FunctionRegistry = Record<string, Record<string, TetherFunction>>;
|
|
74
|
+
|
|
75
|
+
let functionRegistry: FunctionRegistry | null = null;
|
|
76
|
+
let registryError: Error | null = null;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Load the user's custom functions from ~/tether/functions (once).
|
|
80
|
+
*/
|
|
81
|
+
async function loadFunctionRegistry(): Promise<void> {
|
|
82
|
+
if (functionRegistry !== null || registryError !== null) return;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
log.debug('Loading custom functions from ~~/tether/functions/index.ts');
|
|
86
|
+
const functions = await import('~~/tether/functions/index.ts').catch((err: Error) => {
|
|
87
|
+
log.debug('Failed to import functions:', err.message);
|
|
88
|
+
return null;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (functions) {
|
|
92
|
+
log.debug('Loaded function modules:', Object.keys(functions));
|
|
93
|
+
functionRegistry = functions as FunctionRegistry;
|
|
94
|
+
} else {
|
|
95
|
+
log.debug('No custom functions found, will proxy all requests');
|
|
96
|
+
functionRegistry = {};
|
|
97
|
+
}
|
|
98
|
+
} catch (error: unknown) {
|
|
99
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
100
|
+
log.debug('Could not load custom functions:', err.message);
|
|
101
|
+
registryError = err;
|
|
102
|
+
functionRegistry = {};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Look up a function by name (e.g. "channel.getMyChannels").
|
|
108
|
+
* Returns null if no custom function is registered.
|
|
109
|
+
*/
|
|
110
|
+
export async function lookupFunction(name: string): Promise<TetherFunction | null> {
|
|
111
|
+
await loadFunctionRegistry();
|
|
112
|
+
|
|
113
|
+
if (!functionRegistry || !name) return null;
|
|
114
|
+
|
|
115
|
+
const parts = name.split('.');
|
|
116
|
+
if (parts.length !== 2) return null;
|
|
117
|
+
|
|
118
|
+
const [moduleName, fnName] = parts;
|
|
119
|
+
const mod = functionRegistry[moduleName];
|
|
120
|
+
if (!mod) return null;
|
|
121
|
+
|
|
122
|
+
const fn = mod[fnName];
|
|
123
|
+
if (fn && typeof fn === 'object' && typeof fn.handler === 'function') {
|
|
124
|
+
return fn;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// Database Proxy
|
|
132
|
+
// ============================================================================
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create a database proxy that routes calls to Tether's CRUD endpoints.
|
|
136
|
+
*/
|
|
137
|
+
export function createDatabaseProxy(apiKey: string, url: string, projectId: string) {
|
|
138
|
+
return new Proxy({} as Record<string, unknown>, {
|
|
139
|
+
get(_target, tableName: string) {
|
|
140
|
+
const makeRequest = async (operation: string, args: Record<string, unknown>) => {
|
|
141
|
+
const response = await fetch(`${url}/api/v1/projects/${projectId}/query`, {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers: {
|
|
144
|
+
'Content-Type': 'application/json',
|
|
145
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
146
|
+
},
|
|
147
|
+
body: JSON.stringify({
|
|
148
|
+
function: `${tableName}.${operation}`,
|
|
149
|
+
args,
|
|
150
|
+
}),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
155
|
+
throw new Error(error.error || `Database operation failed: ${tableName}.${operation}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const result = await response.json();
|
|
159
|
+
return result.data;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const makeMutation = async (operation: string, args: Record<string, unknown>) => {
|
|
163
|
+
const response = await fetch(`${url}/api/v1/projects/${projectId}/mutation`, {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: {
|
|
166
|
+
'Content-Type': 'application/json',
|
|
167
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
168
|
+
},
|
|
169
|
+
body: JSON.stringify({
|
|
170
|
+
function: `${tableName}.${operation}`,
|
|
171
|
+
args,
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (!response.ok) {
|
|
176
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
177
|
+
throw new Error(error.error || `Database operation failed: ${tableName}.${operation}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const result = await response.json();
|
|
181
|
+
return result.data;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
findMany: async (options?: { where?: Record<string, unknown>; limit?: number; offset?: number; orderBy?: string | Record<string, string>; orderDir?: string }) => {
|
|
186
|
+
const args: Record<string, unknown> = {};
|
|
187
|
+
if (options?.where) args.where = options.where;
|
|
188
|
+
if (options?.limit) args.limit = options.limit;
|
|
189
|
+
if (options?.offset) args.offset = options.offset;
|
|
190
|
+
if (options?.orderBy) {
|
|
191
|
+
if (typeof options.orderBy === 'string') {
|
|
192
|
+
args.orderBy = options.orderBy;
|
|
193
|
+
} else if (typeof options.orderBy === 'object') {
|
|
194
|
+
const [column, dir] = Object.entries(options.orderBy)[0] || [];
|
|
195
|
+
if (column) {
|
|
196
|
+
args.orderBy = column;
|
|
197
|
+
args.orderDir = dir?.toUpperCase?.() || 'ASC';
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (options?.orderDir) args.orderDir = options.orderDir;
|
|
202
|
+
return makeRequest('list', args);
|
|
203
|
+
},
|
|
204
|
+
findFirst: async (options?: { where?: Record<string, unknown> }) => {
|
|
205
|
+
const args: Record<string, unknown> = { limit: 1 };
|
|
206
|
+
if (options?.where) args.where = options.where;
|
|
207
|
+
const results = await makeRequest('list', args);
|
|
208
|
+
return results?.[0] ?? null;
|
|
209
|
+
},
|
|
210
|
+
findUnique: async (options?: { where?: Record<string, unknown> }) => {
|
|
211
|
+
const args: Record<string, unknown> = { limit: 1 };
|
|
212
|
+
if (options?.where) args.where = options.where;
|
|
213
|
+
const results = await makeRequest('list', args);
|
|
214
|
+
return results?.[0] ?? null;
|
|
215
|
+
},
|
|
216
|
+
findById: async (id: unknown) => {
|
|
217
|
+
return makeRequest('get', { id });
|
|
218
|
+
},
|
|
219
|
+
count: async (options?: { where?: Record<string, unknown> }) => {
|
|
220
|
+
const args: Record<string, unknown> = {};
|
|
221
|
+
if (options?.where) args.where = options.where;
|
|
222
|
+
const result = await makeRequest('count', args);
|
|
223
|
+
return result?.count ?? 0;
|
|
224
|
+
},
|
|
225
|
+
insert: async (data: unknown) => {
|
|
226
|
+
return makeMutation('create', { data });
|
|
227
|
+
},
|
|
228
|
+
insertMany: async (items: unknown[]) => {
|
|
229
|
+
const results: unknown[] = [];
|
|
230
|
+
for (const data of items) {
|
|
231
|
+
const result = await makeMutation('create', { data });
|
|
232
|
+
results.push(result);
|
|
233
|
+
}
|
|
234
|
+
return results;
|
|
235
|
+
},
|
|
236
|
+
create: async (options: { data: unknown }) => {
|
|
237
|
+
return makeMutation('create', { data: options.data });
|
|
238
|
+
},
|
|
239
|
+
update: async (options: { where: Record<string, unknown>; data: unknown }) => {
|
|
240
|
+
const id = options.where?._id ?? options.where?.id;
|
|
241
|
+
if (!id) {
|
|
242
|
+
throw new Error('Update requires an _id in the where clause');
|
|
243
|
+
}
|
|
244
|
+
const result = await makeMutation('update', { id, data: options.data });
|
|
245
|
+
return result?.rowsAffected ?? 0;
|
|
246
|
+
},
|
|
247
|
+
upsert: async (options: { where: Record<string, unknown>; create: unknown; update: unknown }) => {
|
|
248
|
+
const results = await makeRequest('list', { where: options.where, limit: 1 }).catch(() => []);
|
|
249
|
+
const existing = results?.[0] ?? null;
|
|
250
|
+
|
|
251
|
+
if (existing) {
|
|
252
|
+
const id = existing._id ?? existing.id;
|
|
253
|
+
if (!id) {
|
|
254
|
+
throw new Error('Found record has no _id or id field for update');
|
|
255
|
+
}
|
|
256
|
+
await makeMutation('update', { id, data: options.update });
|
|
257
|
+
return { ...existing, ...options.update };
|
|
258
|
+
} else {
|
|
259
|
+
return makeMutation('create', { data: options.create });
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
delete: async (options: { where: Record<string, unknown> }) => {
|
|
263
|
+
const id = options.where?._id ?? options.where?.id;
|
|
264
|
+
if (id) {
|
|
265
|
+
const result = await makeMutation('delete', { id });
|
|
266
|
+
return result?.rowsAffected ?? 0;
|
|
267
|
+
}
|
|
268
|
+
const result = await makeMutation('deleteMany', { where: options.where });
|
|
269
|
+
return result?.rowsAffected ?? 0;
|
|
270
|
+
},
|
|
271
|
+
deleteById: async (id: unknown) => {
|
|
272
|
+
const result = await makeMutation('delete', { id });
|
|
273
|
+
return (result?.rowsAffected ?? 0) > 0;
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ============================================================================
|
|
281
|
+
// Auth Context
|
|
282
|
+
// ============================================================================
|
|
283
|
+
|
|
284
|
+
interface UserIdentity {
|
|
285
|
+
subject: string;
|
|
286
|
+
email?: string;
|
|
287
|
+
name?: string;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Build auth context by extracting and validating tokens from the request.
|
|
292
|
+
*/
|
|
293
|
+
export async function buildAuthContext(event: H3Event, url: string, projectId: string) {
|
|
294
|
+
let userIdentity: UserIdentity | null = null;
|
|
295
|
+
|
|
296
|
+
const authHeader = getHeader(event, 'authorization');
|
|
297
|
+
const strandsToken = getHeader(event, 'x-strands-token');
|
|
298
|
+
const cookieToken = getCookie(event, 'strands_oauth_token');
|
|
299
|
+
|
|
300
|
+
if (strandsToken || cookieToken || authHeader?.startsWith('Bearer ')) {
|
|
301
|
+
const token = strandsToken || cookieToken || authHeader?.replace('Bearer ', '');
|
|
302
|
+
if (token) {
|
|
303
|
+
try {
|
|
304
|
+
const authResponse = await fetch(`${url}/api/v1/projects/${projectId}/auth/validate`, {
|
|
305
|
+
method: 'POST',
|
|
306
|
+
headers: { 'Content-Type': 'application/json' },
|
|
307
|
+
body: JSON.stringify({ accessToken: token }),
|
|
308
|
+
});
|
|
309
|
+
if (authResponse.ok) {
|
|
310
|
+
const authData = await authResponse.json();
|
|
311
|
+
if (authData.valid) {
|
|
312
|
+
userIdentity = {
|
|
313
|
+
subject: authData.userId,
|
|
314
|
+
email: authData.email,
|
|
315
|
+
name: authData.name,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} catch (authError: unknown) {
|
|
320
|
+
const err = authError instanceof Error ? authError : new Error(String(authError));
|
|
321
|
+
log.warn('Auth validation failed:', err.message);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const auth = {
|
|
327
|
+
getUserIdentity: async () => userIdentity,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const ctx = {
|
|
331
|
+
auth: {
|
|
332
|
+
userId: userIdentity?.subject ?? null,
|
|
333
|
+
claims: userIdentity ?? {},
|
|
334
|
+
},
|
|
335
|
+
userId: userIdentity?.subject ?? null,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
return { auth, ctx };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ============================================================================
|
|
342
|
+
// Proxy to Tether API
|
|
343
|
+
// ============================================================================
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Proxy a request directly to the Tether API (for generic CRUD when no custom function exists).
|
|
347
|
+
*/
|
|
348
|
+
export async function proxyToTetherApi(
|
|
349
|
+
config: TetherServerConfig,
|
|
350
|
+
endpoint: 'query' | 'mutation',
|
|
351
|
+
functionName: string,
|
|
352
|
+
args: unknown
|
|
353
|
+
) {
|
|
354
|
+
const response = await fetch(`${config.url}/api/v1/projects/${config.projectId}/${endpoint}`, {
|
|
355
|
+
method: 'POST',
|
|
356
|
+
headers: {
|
|
357
|
+
'Content-Type': 'application/json',
|
|
358
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
359
|
+
},
|
|
360
|
+
body: JSON.stringify({
|
|
361
|
+
function: functionName,
|
|
362
|
+
args,
|
|
363
|
+
}),
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
if (!response.ok) {
|
|
367
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
368
|
+
throw createError({
|
|
369
|
+
statusCode: response.status,
|
|
370
|
+
message: error.error || `${endpoint === 'query' ? 'Query' : 'Mutation'} failed`,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return response.json();
|
|
375
|
+
}
|