@tthr/vue 0.0.90 → 0.1.0
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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/nuxt/module.js +2 -22
- package/nuxt/module.ts +2 -25
- package/nuxt/runtime/server/mutation.post.js +6 -26
- package/nuxt/runtime/server/query.post.js +5 -23
- package/nuxt/runtime/server/utils/handler.js +26 -243
- package/nuxt/server-src/mutation.post.ts +6 -35
- package/nuxt/server-src/query.post.ts +5 -32
- package/nuxt/server-src/utils/handler.ts +26 -266
- package/package.json +3 -8
package/dist/index.d.ts
CHANGED
|
@@ -139,7 +139,7 @@ export declare function $query<TArgs, TResult>(query: QueryFunction<TArgs, TResu
|
|
|
139
139
|
* ```
|
|
140
140
|
*/
|
|
141
141
|
export declare function $mutation<TArgs, TResult>(mutation: MutationFunction<TArgs, TResult>, args?: TArgs, options?: UseMutationOptions): Promise<TResult>;
|
|
142
|
-
export type { TetherClientOptions, MutationOptions } from '@tthr/client';
|
|
142
|
+
export type { TetherClientOptions, AuthTokenSource, MutationOptions } from '@tthr/client';
|
|
143
143
|
/**
|
|
144
144
|
* Tether Nuxt/Vue project configuration
|
|
145
145
|
*/
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/nuxt/module.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Automatically configures Tether for Nuxt applications.
|
|
5
5
|
* All API calls are proxied through Nuxt server routes to keep API keys secure.
|
|
6
|
+
* Function execution happens on Tether's Deno runtime — the Nuxt server is a thin proxy.
|
|
6
7
|
*
|
|
7
8
|
* Usage in nuxt.config.ts:
|
|
8
9
|
* ```ts
|
|
@@ -114,36 +115,15 @@ export default defineNuxtModule({
|
|
|
114
115
|
name: 'useTetherServer',
|
|
115
116
|
from: resolver.resolve('./runtime/server/utils/tether.js'),
|
|
116
117
|
},
|
|
117
|
-
{
|
|
118
|
-
name: 'registerCronHandler',
|
|
119
|
-
from: resolver.resolve('./runtime/server/plugins/cron.js'),
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
name: 'unregisterCronHandler',
|
|
123
|
-
from: resolver.resolve('./runtime/server/plugins/cron.js'),
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
name: 'getCronHandlers',
|
|
127
|
-
from: resolver.resolve('./runtime/server/plugins/cron.js'),
|
|
128
|
-
},
|
|
129
118
|
]);
|
|
130
119
|
// Ensure the runtime files are transpiled
|
|
131
120
|
nuxt.options.build.transpile = nuxt.options.build.transpile || [];
|
|
132
121
|
nuxt.options.build.transpile.push('@tthr/vue');
|
|
133
|
-
//
|
|
134
|
-
// This auto-connects to Tether when the server starts
|
|
122
|
+
// Ensure Nitro inlines @tthr/vue from node_modules
|
|
135
123
|
nuxt.hook('nitro:config', (nitroConfig) => {
|
|
136
|
-
nitroConfig.plugins = nitroConfig.plugins || [];
|
|
137
|
-
nitroConfig.plugins.push(resolver.resolve('./runtime/server/plugins/cron.js'));
|
|
138
|
-
// Ensure Nitro inlines and transpiles the plugin from node_modules
|
|
139
124
|
nitroConfig.externals = nitroConfig.externals || {};
|
|
140
125
|
nitroConfig.externals.inline = nitroConfig.externals.inline || [];
|
|
141
126
|
nitroConfig.externals.inline.push('@tthr/vue');
|
|
142
|
-
// Also inline the user's tether/functions directory so Nitro transpiles TypeScript
|
|
143
|
-
nitroConfig.externals.inline.push(`${nuxt.options.rootDir}/tether`);
|
|
144
|
-
// Externalize 'ws' so it's not bundled - needed for WebSocket in Node.js
|
|
145
|
-
nitroConfig.externals.external = nitroConfig.externals.external || [];
|
|
146
|
-
nitroConfig.externals.external.push('ws');
|
|
147
127
|
});
|
|
148
128
|
},
|
|
149
129
|
});
|
package/nuxt/module.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Automatically configures Tether for Nuxt applications.
|
|
5
5
|
* All API calls are proxied through Nuxt server routes to keep API keys secure.
|
|
6
|
+
* Function execution happens on Tether's Deno runtime — the Nuxt server is a thin proxy.
|
|
6
7
|
*
|
|
7
8
|
* Usage in nuxt.config.ts:
|
|
8
9
|
* ```ts
|
|
@@ -144,41 +145,17 @@ export default defineNuxtModule<TetherModuleOptions>({
|
|
|
144
145
|
name: 'useTetherServer',
|
|
145
146
|
from: resolver.resolve('./runtime/server/utils/tether.js'),
|
|
146
147
|
},
|
|
147
|
-
{
|
|
148
|
-
name: 'registerCronHandler',
|
|
149
|
-
from: resolver.resolve('./runtime/server/plugins/cron.js'),
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
name: 'unregisterCronHandler',
|
|
153
|
-
from: resolver.resolve('./runtime/server/plugins/cron.js'),
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
name: 'getCronHandlers',
|
|
157
|
-
from: resolver.resolve('./runtime/server/plugins/cron.js'),
|
|
158
|
-
},
|
|
159
148
|
]);
|
|
160
149
|
|
|
161
150
|
// Ensure the runtime files are transpiled
|
|
162
151
|
nuxt.options.build.transpile = nuxt.options.build.transpile || [];
|
|
163
152
|
nuxt.options.build.transpile.push('@tthr/vue');
|
|
164
153
|
|
|
165
|
-
//
|
|
166
|
-
// This auto-connects to Tether when the server starts
|
|
154
|
+
// Ensure Nitro inlines @tthr/vue from node_modules
|
|
167
155
|
nuxt.hook('nitro:config' as any, (nitroConfig: any) => {
|
|
168
|
-
nitroConfig.plugins = nitroConfig.plugins || [];
|
|
169
|
-
nitroConfig.plugins.push(resolver.resolve('./runtime/server/plugins/cron.js'));
|
|
170
|
-
|
|
171
|
-
// Ensure Nitro inlines and transpiles the plugin from node_modules
|
|
172
156
|
nitroConfig.externals = nitroConfig.externals || {};
|
|
173
157
|
nitroConfig.externals.inline = nitroConfig.externals.inline || [];
|
|
174
158
|
nitroConfig.externals.inline.push('@tthr/vue');
|
|
175
|
-
|
|
176
|
-
// Also inline the user's tether/functions directory so Nitro transpiles TypeScript
|
|
177
|
-
nitroConfig.externals.inline.push(`${nuxt.options.rootDir}/tether`);
|
|
178
|
-
|
|
179
|
-
// Externalize 'ws' so it's not bundled - needed for WebSocket in Node.js
|
|
180
|
-
nitroConfig.externals.external = nitroConfig.externals.external || [];
|
|
181
|
-
nitroConfig.externals.external.push('ws');
|
|
182
159
|
});
|
|
183
160
|
},
|
|
184
161
|
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Server-side mutation handler
|
|
3
|
-
*
|
|
3
|
+
* Proxies all requests to Tether API — functions execute on Tether's Deno runtime
|
|
4
4
|
*/
|
|
5
|
-
import { defineEventHandler, readBody, createError
|
|
6
|
-
import { getConfig,
|
|
5
|
+
import { defineEventHandler, readBody, createError } from 'h3';
|
|
6
|
+
import { getConfig, proxyToTetherApi, getAuthHeaders, log } from './utils/handler.js';
|
|
7
7
|
export default defineEventHandler(async (event) => {
|
|
8
8
|
const config = getConfig();
|
|
9
9
|
const body = await readBody(event);
|
|
@@ -13,27 +13,7 @@ export default defineEventHandler(async (event) => {
|
|
|
13
13
|
message: 'Missing "function" in request body',
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
log.debug(`Executing custom mutation: ${body.function}`);
|
|
21
|
-
const db = createDatabaseProxy(config.apiKey, config.url, config.projectId);
|
|
22
|
-
const { auth, ctx } = await buildAuthContext(event, config.url, config.projectId);
|
|
23
|
-
const result = await customFn.handler({ db, ctx, auth, args: body.args ?? {} });
|
|
24
|
-
return { data: result };
|
|
25
|
-
}
|
|
26
|
-
catch (error) {
|
|
27
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
28
|
-
log.error(`Mutation ${body.function} failed:`, err);
|
|
29
|
-
// Return JSON error response instead of throwing (avoids proxy HTML error pages)
|
|
30
|
-
setResponseStatus(event, 400);
|
|
31
|
-
return {
|
|
32
|
-
error: true,
|
|
33
|
-
message: err.message || 'Mutation execution failed',
|
|
34
|
-
function: body.function,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return proxyToTetherApi(config, 'mutation', body.function, body.args);
|
|
16
|
+
log.debug(`Mutation: ${body.function}`);
|
|
17
|
+
const authHeaders = getAuthHeaders(event);
|
|
18
|
+
return proxyToTetherApi(config, 'mutation', body.function, body.args, authHeaders);
|
|
39
19
|
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Server-side query handler
|
|
3
|
-
*
|
|
3
|
+
* Proxies all requests to Tether API — functions execute on Tether's Deno runtime
|
|
4
4
|
*/
|
|
5
5
|
import { defineEventHandler, readBody, createError } from 'h3';
|
|
6
|
-
import { getConfig,
|
|
6
|
+
import { getConfig, proxyToTetherApi, getAuthHeaders, log } from './utils/handler.js';
|
|
7
7
|
export default defineEventHandler(async (event) => {
|
|
8
8
|
const config = getConfig();
|
|
9
9
|
const body = await readBody(event);
|
|
@@ -13,25 +13,7 @@ export default defineEventHandler(async (event) => {
|
|
|
13
13
|
message: 'Missing "function" in request body',
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
const db = createDatabaseProxy(config.apiKey, config.url, config.projectId);
|
|
21
|
-
const { auth, ctx } = await buildAuthContext(event, config.url, config.projectId);
|
|
22
|
-
log.debug(`Executing custom function: ${body.function}`);
|
|
23
|
-
const result = await customFn.handler({ db, ctx, auth, args: body.args ?? {} });
|
|
24
|
-
log.debug(`Function ${body.function} completed`);
|
|
25
|
-
return { data: result };
|
|
26
|
-
}
|
|
27
|
-
catch (error) {
|
|
28
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
29
|
-
log.error(`Function ${body.function} failed:`, err.message);
|
|
30
|
-
throw createError({
|
|
31
|
-
statusCode: 500,
|
|
32
|
-
message: err.message || 'Function execution failed',
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return proxyToTetherApi(config, 'query', body.function, body.args);
|
|
16
|
+
log.debug(`Query: ${body.function}`);
|
|
17
|
+
const authHeaders = getAuthHeaders(event);
|
|
18
|
+
return proxyToTetherApi(config, 'query', body.function, body.args, authHeaders);
|
|
37
19
|
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared handler utilities for Tether server routes
|
|
3
3
|
*
|
|
4
|
-
* Provides
|
|
5
|
-
*
|
|
4
|
+
* Provides config loading, auth header extraction, and API proxying.
|
|
5
|
+
* All function execution happens on Tether's Deno runtime — the Nuxt
|
|
6
|
+
* server only proxies requests to keep the API key server-side.
|
|
6
7
|
*/
|
|
7
8
|
import { createError, getHeader, getCookie } from 'h3';
|
|
8
9
|
import { useRuntimeConfig } from '#imports';
|
|
@@ -42,265 +43,47 @@ export function getConfig() {
|
|
|
42
43
|
}
|
|
43
44
|
return { apiKey, url, projectId };
|
|
44
45
|
}
|
|
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
46
|
// ============================================================================
|
|
97
|
-
//
|
|
47
|
+
// Auth Header Extraction
|
|
98
48
|
// ============================================================================
|
|
99
49
|
/**
|
|
100
|
-
*
|
|
50
|
+
* Extract auth-related headers from the incoming request to forward to Tether API.
|
|
51
|
+
* This allows the Deno runtime to identify the end user.
|
|
101
52
|
*/
|
|
102
|
-
export function
|
|
103
|
-
|
|
104
|
-
|
|
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');
|
|
53
|
+
export function getAuthHeaders(event) {
|
|
54
|
+
const headers = {};
|
|
55
|
+
// Forward Strands auth token
|
|
252
56
|
const strandsToken = getHeader(event, 'x-strands-token');
|
|
57
|
+
if (strandsToken) {
|
|
58
|
+
headers['x-auth-token'] = strandsToken;
|
|
59
|
+
}
|
|
60
|
+
// Forward cookie-based auth token
|
|
253
61
|
const cookieToken = getCookie(event, 'strands_oauth_token');
|
|
254
|
-
if (
|
|
255
|
-
|
|
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
|
-
}
|
|
62
|
+
if (cookieToken) {
|
|
63
|
+
headers['x-auth-token'] = cookieToken;
|
|
279
64
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
auth
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
},
|
|
288
|
-
userId: userIdentity?.subject ?? null,
|
|
289
|
-
};
|
|
290
|
-
return { auth, ctx };
|
|
65
|
+
// Forward Bearer token from Authorization header (end-user token, not API key)
|
|
66
|
+
const authHeader = getHeader(event, 'authorization');
|
|
67
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
68
|
+
const token = authHeader.replace('Bearer ', '');
|
|
69
|
+
headers['x-auth-token'] = token;
|
|
70
|
+
}
|
|
71
|
+
return headers;
|
|
291
72
|
}
|
|
292
73
|
// ============================================================================
|
|
293
74
|
// Proxy to Tether API
|
|
294
75
|
// ============================================================================
|
|
295
76
|
/**
|
|
296
|
-
* Proxy a request
|
|
77
|
+
* Proxy a request to the Tether API.
|
|
78
|
+
* Uses the API key for authentication and forwards any end-user auth headers.
|
|
297
79
|
*/
|
|
298
|
-
export async function proxyToTetherApi(config, endpoint, functionName, args) {
|
|
80
|
+
export async function proxyToTetherApi(config, endpoint, functionName, args, extraHeaders) {
|
|
299
81
|
const response = await fetch(`${config.url}/api/v1/projects/${config.projectId}/${endpoint}`, {
|
|
300
82
|
method: 'POST',
|
|
301
83
|
headers: {
|
|
302
84
|
'Content-Type': 'application/json',
|
|
303
85
|
'Authorization': `Bearer ${config.apiKey}`,
|
|
86
|
+
...extraHeaders,
|
|
304
87
|
},
|
|
305
88
|
body: JSON.stringify({
|
|
306
89
|
function: functionName,
|
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Server-side mutation handler
|
|
3
|
-
*
|
|
3
|
+
* Proxies all requests to Tether API — functions execute on Tether's Deno runtime
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { defineEventHandler, readBody, createError
|
|
7
|
-
import {
|
|
8
|
-
getConfig,
|
|
9
|
-
lookupFunction,
|
|
10
|
-
createDatabaseProxy,
|
|
11
|
-
buildAuthContext,
|
|
12
|
-
proxyToTetherApi,
|
|
13
|
-
log,
|
|
14
|
-
} from './utils/handler.js';
|
|
6
|
+
import { defineEventHandler, readBody, createError } from 'h3';
|
|
7
|
+
import { getConfig, proxyToTetherApi, getAuthHeaders, log } from './utils/handler.js';
|
|
15
8
|
|
|
16
9
|
export default defineEventHandler(async (event) => {
|
|
17
10
|
const config = getConfig();
|
|
@@ -24,30 +17,8 @@ export default defineEventHandler(async (event) => {
|
|
|
24
17
|
});
|
|
25
18
|
}
|
|
26
19
|
|
|
27
|
-
|
|
28
|
-
log.debug(`Mutation: ${body.function}, custom function found: ${!!customFn}`);
|
|
20
|
+
log.debug(`Mutation: ${body.function}`);
|
|
29
21
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
log.debug(`Executing custom mutation: ${body.function}`);
|
|
33
|
-
const db = createDatabaseProxy(config.apiKey, config.url, config.projectId);
|
|
34
|
-
const { auth, ctx } = await buildAuthContext(event, config.url, config.projectId);
|
|
35
|
-
|
|
36
|
-
const result = await customFn.handler({ db, ctx, auth, args: body.args ?? {} });
|
|
37
|
-
|
|
38
|
-
return { data: result };
|
|
39
|
-
} catch (error: unknown) {
|
|
40
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
41
|
-
log.error(`Mutation ${body.function} failed:`, err);
|
|
42
|
-
// Return JSON error response instead of throwing (avoids proxy HTML error pages)
|
|
43
|
-
setResponseStatus(event, 400);
|
|
44
|
-
return {
|
|
45
|
-
error: true,
|
|
46
|
-
message: err.message || 'Mutation execution failed',
|
|
47
|
-
function: body.function,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return proxyToTetherApi(config, 'mutation', body.function, body.args);
|
|
22
|
+
const authHeaders = getAuthHeaders(event);
|
|
23
|
+
return proxyToTetherApi(config, 'mutation', body.function, body.args, authHeaders);
|
|
53
24
|
});
|
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Server-side query handler
|
|
3
|
-
*
|
|
3
|
+
* Proxies all requests to Tether API — functions execute on Tether's Deno runtime
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { defineEventHandler, readBody, createError } from 'h3';
|
|
7
|
-
import {
|
|
8
|
-
getConfig,
|
|
9
|
-
lookupFunction,
|
|
10
|
-
createDatabaseProxy,
|
|
11
|
-
buildAuthContext,
|
|
12
|
-
proxyToTetherApi,
|
|
13
|
-
log,
|
|
14
|
-
} from './utils/handler.js';
|
|
7
|
+
import { getConfig, proxyToTetherApi, getAuthHeaders, log } from './utils/handler.js';
|
|
15
8
|
|
|
16
9
|
export default defineEventHandler(async (event) => {
|
|
17
10
|
const config = getConfig();
|
|
@@ -24,28 +17,8 @@ export default defineEventHandler(async (event) => {
|
|
|
24
17
|
});
|
|
25
18
|
}
|
|
26
19
|
|
|
27
|
-
|
|
28
|
-
log.debug(`Query: ${body.function}, custom function found: ${!!customFn}`);
|
|
20
|
+
log.debug(`Query: ${body.function}`);
|
|
29
21
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const db = createDatabaseProxy(config.apiKey, config.url, config.projectId);
|
|
33
|
-
const { auth, ctx } = await buildAuthContext(event, config.url, config.projectId);
|
|
34
|
-
|
|
35
|
-
log.debug(`Executing custom function: ${body.function}`);
|
|
36
|
-
const result = await customFn.handler({ db, ctx, auth, args: body.args ?? {} });
|
|
37
|
-
log.debug(`Function ${body.function} completed`);
|
|
38
|
-
|
|
39
|
-
return { data: result };
|
|
40
|
-
} catch (error: unknown) {
|
|
41
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
42
|
-
log.error(`Function ${body.function} failed:`, err.message);
|
|
43
|
-
throw createError({
|
|
44
|
-
statusCode: 500,
|
|
45
|
-
message: err.message || 'Function execution failed',
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return proxyToTetherApi(config, 'query', body.function, body.args);
|
|
22
|
+
const authHeaders = getAuthHeaders(event);
|
|
23
|
+
return proxyToTetherApi(config, 'query', body.function, body.args, authHeaders);
|
|
51
24
|
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared handler utilities for Tether server routes
|
|
3
3
|
*
|
|
4
|
-
* Provides
|
|
5
|
-
*
|
|
4
|
+
* Provides config loading, auth header extraction, and API proxying.
|
|
5
|
+
* All function execution happens on Tether's Deno runtime — the Nuxt
|
|
6
|
+
* server only proxies requests to keep the API key server-side.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { createError, getHeader, getCookie, type H3Event } from 'h3';
|
|
@@ -62,280 +63,36 @@ export function getConfig(): TetherServerConfig {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
// ============================================================================
|
|
65
|
-
//
|
|
66
|
+
// Auth Header Extraction
|
|
66
67
|
// ============================================================================
|
|
67
68
|
|
|
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
69
|
/**
|
|
79
|
-
*
|
|
70
|
+
* Extract auth-related headers from the incoming request to forward to Tether API.
|
|
71
|
+
* This allows the Deno runtime to identify the end user.
|
|
80
72
|
*/
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
});
|
|
73
|
+
export function getAuthHeaders(event: H3Event): Record<string, string> {
|
|
74
|
+
const headers: Record<string, string> = {};
|
|
90
75
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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 = {};
|
|
76
|
+
// Forward Strands auth token
|
|
77
|
+
const strandsToken = getHeader(event, 'x-strands-token');
|
|
78
|
+
if (strandsToken) {
|
|
79
|
+
headers['x-auth-token'] = strandsToken;
|
|
103
80
|
}
|
|
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
81
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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;
|
|
82
|
+
// Forward cookie-based auth token
|
|
83
|
+
const cookieToken = getCookie(event, 'strands_oauth_token');
|
|
84
|
+
if (cookieToken) {
|
|
85
|
+
headers['x-auth-token'] = cookieToken;
|
|
125
86
|
}
|
|
126
87
|
|
|
127
|
-
|
|
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
|
-
|
|
88
|
+
// Forward Bearer token from Authorization header (end-user token, not API key)
|
|
296
89
|
const authHeader = getHeader(event, 'authorization');
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
}
|
|
90
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
91
|
+
const token = authHeader.replace('Bearer ', '');
|
|
92
|
+
headers['x-auth-token'] = token;
|
|
324
93
|
}
|
|
325
94
|
|
|
326
|
-
|
|
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 };
|
|
95
|
+
return headers;
|
|
339
96
|
}
|
|
340
97
|
|
|
341
98
|
// ============================================================================
|
|
@@ -343,19 +100,22 @@ export async function buildAuthContext(event: H3Event, url: string, projectId: s
|
|
|
343
100
|
// ============================================================================
|
|
344
101
|
|
|
345
102
|
/**
|
|
346
|
-
* Proxy a request
|
|
103
|
+
* Proxy a request to the Tether API.
|
|
104
|
+
* Uses the API key for authentication and forwards any end-user auth headers.
|
|
347
105
|
*/
|
|
348
106
|
export async function proxyToTetherApi(
|
|
349
107
|
config: TetherServerConfig,
|
|
350
108
|
endpoint: 'query' | 'mutation',
|
|
351
109
|
functionName: string,
|
|
352
|
-
args: unknown
|
|
110
|
+
args: unknown,
|
|
111
|
+
extraHeaders?: Record<string, string>,
|
|
353
112
|
) {
|
|
354
113
|
const response = await fetch(`${config.url}/api/v1/projects/${config.projectId}/${endpoint}`, {
|
|
355
114
|
method: 'POST',
|
|
356
115
|
headers: {
|
|
357
116
|
'Content-Type': 'application/json',
|
|
358
117
|
'Authorization': `Bearer ${config.apiKey}`,
|
|
118
|
+
...extraHeaders,
|
|
359
119
|
},
|
|
360
120
|
body: JSON.stringify({
|
|
361
121
|
function: functionName,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tthr/vue",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Tether Vue/Nuxt SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -17,9 +17,6 @@
|
|
|
17
17
|
"./nuxt": {
|
|
18
18
|
"import": "./nuxt/module.ts"
|
|
19
19
|
},
|
|
20
|
-
"./nuxt/runtime/server/plugins/cron": {
|
|
21
|
-
"import": "./nuxt/runtime/server/plugins/cron.js"
|
|
22
|
-
},
|
|
23
20
|
"./nuxt/runtime/server/utils/tether": {
|
|
24
21
|
"import": "./nuxt/runtime/server/utils/tether.js"
|
|
25
22
|
}
|
|
@@ -37,12 +34,10 @@
|
|
|
37
34
|
],
|
|
38
35
|
"dependencies": {
|
|
39
36
|
"@nuxt/kit": "^3.14.0",
|
|
40
|
-
"@tthr/client": "
|
|
41
|
-
"@tthr/server": "
|
|
42
|
-
"ws": "^8.18.0"
|
|
37
|
+
"@tthr/client": "0.1.0",
|
|
38
|
+
"@tthr/server": "0.0.17"
|
|
43
39
|
},
|
|
44
40
|
"devDependencies": {
|
|
45
|
-
"@types/ws": "^8.5.13",
|
|
46
41
|
"typescript": "^5.7.2"
|
|
47
42
|
},
|
|
48
43
|
"peerDependencies": {
|