@tthr/vue 0.0.41 → 0.0.45

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 CHANGED
@@ -18,7 +18,7 @@
18
18
  * Environment variables (in .env):
19
19
  * - TETHER_API_KEY: Your project's API key (required, kept server-side)
20
20
  */
21
- import { defineNuxtModule, addPlugin, createResolver, addImports, addComponent, addServerHandler, addServerImports } from '@nuxt/kit';
21
+ import { defineNuxtModule, addPlugin, createResolver, addImports, addComponent, addServerHandler, addServerImports, addTemplate } from '@nuxt/kit';
22
22
  export default defineNuxtModule({
23
23
  meta: {
24
24
  name: '@tthr/vue',
@@ -38,6 +38,7 @@ export default defineNuxtModule({
38
38
  const apiKey = process.env.TETHER_API_KEY || '';
39
39
  const url = options.url || process.env.TETHER_URL || 'https://tether-api.strands.gg';
40
40
  const projectId = options.projectId || process.env.TETHER_PROJECT_ID || '';
41
+ const verbose = options.verbose ?? (process.env.TETHER_VERBOSE === 'true');
41
42
  // Calculate WebSocket URL from HTTP URL
42
43
  const wsUrl = url.replace(/^https:/, 'wss:').replace(/^http:/, 'ws:');
43
44
  // Server-side config (includes API key - never exposed to client)
@@ -45,10 +46,12 @@ export default defineNuxtModule({
45
46
  // - NUXT_TETHER_API_KEY
46
47
  // - NUXT_TETHER_URL
47
48
  // - NUXT_TETHER_PROJECT_ID
49
+ // - NUXT_TETHER_VERBOSE
48
50
  nuxt.options.runtimeConfig.tether = {
49
51
  apiKey,
50
52
  url,
51
53
  projectId,
54
+ verbose,
52
55
  };
53
56
  // Public config (safe for client - no secrets)
54
57
  // Can be overridden at runtime via:
@@ -128,5 +131,144 @@ export default defineNuxtModule({
128
131
  nitroConfig.externals.inline = nitroConfig.externals.inline || [];
129
132
  nitroConfig.externals.inline.push('@tthr/vue');
130
133
  });
134
+ // Generate a server plugin to auto-register cron handlers from tether/functions
135
+ // This runs at build time and creates a plugin that imports user's functions
136
+ addTemplate({
137
+ filename: 'server/plugins/tether-functions.ts',
138
+ write: true,
139
+ getContents: () => `
140
+ /**
141
+ * Auto-generated plugin to register Tether functions as cron handlers
142
+ * This file is generated by @tthr/vue/nuxt module
143
+ */
144
+ import { defineNitroPlugin } from 'nitropack/runtime';
145
+ import { registerCronHandler } from '@tthr/vue/nuxt/runtime/server/plugins/cron.js';
146
+ import { useTetherServer } from '@tthr/vue/nuxt/runtime/server/utils/tether.js';
147
+
148
+ // Import all functions from tether/functions
149
+ // @ts-ignore - user's functions may not exist yet
150
+ import * as tetherFunctions from '~/tether/functions';
151
+
152
+ /**
153
+ * Create a simple logger for function execution
154
+ */
155
+ function createLogger(functionName: string) {
156
+ const prefix = \`[Tether] \${functionName}\`;
157
+ return {
158
+ log: (msg: string, data?: unknown) => console.log(prefix, msg, data !== undefined ? data : ''),
159
+ info: (msg: string, data?: unknown) => console.log(prefix, msg, data !== undefined ? data : ''),
160
+ warn: (msg: string, data?: unknown) => console.warn(prefix, msg, data !== undefined ? data : ''),
161
+ error: (msg: string, data?: unknown) => console.error(prefix, msg, data !== undefined ? data : ''),
162
+ debug: (msg: string, data?: unknown) => console.debug(prefix, msg, data !== undefined ? data : ''),
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Create the handler context for a function
168
+ */
169
+ function createHandlerContext(functionName: string, args: unknown) {
170
+ const tether = useTetherServer(null as any);
171
+ const log = createLogger(functionName);
172
+
173
+ // Create db proxy that uses tether server client
174
+ // TODO: This is a placeholder - proper db operations need to be implemented
175
+ const db = new Proxy({}, {
176
+ get(_, tableName: string) {
177
+ return {
178
+ findMany: async (options?: any) => {
179
+ // Call Tether API for db operations
180
+ return tether.query(\`_db.\${tableName}.findMany\`, options);
181
+ },
182
+ findFirst: async (options?: any) => {
183
+ return tether.query(\`_db.\${tableName}.findFirst\`, options);
184
+ },
185
+ findUnique: async (options?: any) => {
186
+ return tether.query(\`_db.\${tableName}.findUnique\`, options);
187
+ },
188
+ findById: async (id: unknown) => {
189
+ return tether.query(\`_db.\${tableName}.findById\`, { id });
190
+ },
191
+ count: async (options?: any) => {
192
+ return tether.query(\`_db.\${tableName}.count\`, options);
193
+ },
194
+ create: async (options: any) => {
195
+ return tether.mutation(\`_db.\${tableName}.create\`, options);
196
+ },
197
+ insert: async (data: any) => {
198
+ return tether.mutation(\`_db.\${tableName}.insert\`, { data });
199
+ },
200
+ insertMany: async (data: any[]) => {
201
+ return tether.mutation(\`_db.\${tableName}.insertMany\`, { data });
202
+ },
203
+ update: async (options: any) => {
204
+ return tether.mutation(\`_db.\${tableName}.update\`, options);
205
+ },
206
+ upsert: async (options: any) => {
207
+ return tether.mutation(\`_db.\${tableName}.upsert\`, options);
208
+ },
209
+ delete: async (options: any) => {
210
+ return tether.mutation(\`_db.\${tableName}.delete\`, options);
211
+ },
212
+ deleteById: async (id: unknown) => {
213
+ return tether.mutation(\`_db.\${tableName}.deleteById\`, { id });
214
+ },
215
+ };
216
+ },
217
+ });
218
+
219
+ // Create tether context for actions
220
+ const tetherContext = {
221
+ query: tether.query.bind(tether),
222
+ mutation: tether.mutation.bind(tether),
223
+ env: new Proxy({}, {
224
+ get(_, key: string) {
225
+ return process.env[key];
226
+ },
227
+ }),
228
+ };
229
+
230
+ return {
231
+ args,
232
+ db,
233
+ ctx: { auth: { userId: null, claims: {} }, userId: null },
234
+ log,
235
+ tether: tetherContext,
236
+ };
237
+ }
238
+
239
+ export default defineNitroPlugin(() => {
240
+ console.log('[Tether] Auto-registering functions from tether/functions...');
241
+
242
+ // Iterate through all exported modules
243
+ for (const [moduleName, moduleExports] of Object.entries(tetherFunctions)) {
244
+ if (!moduleExports || typeof moduleExports !== 'object') continue;
245
+
246
+ // Iterate through all exports in each module
247
+ for (const [fnName, fnDef] of Object.entries(moduleExports as Record<string, any>)) {
248
+ // Check if it's a Tether function definition (has handler property)
249
+ if (!fnDef || typeof fnDef !== 'object' || typeof fnDef.handler !== 'function') continue;
250
+
251
+ const fullName = \`\${moduleName}.\${fnName}\`;
252
+
253
+ // Register the cron handler
254
+ registerCronHandler(fullName, async (args: unknown) => {
255
+ const context = createHandlerContext(fullName, args);
256
+ return fnDef.handler(context);
257
+ });
258
+
259
+ console.log(\`[Tether] Registered handler: \${fullName}\`);
260
+ }
261
+ }
262
+
263
+ console.log('[Tether] Function registration complete');
264
+ });
265
+ `,
266
+ });
267
+ // Add the generated plugin to Nitro
268
+ nuxt.hook('nitro:config', (nitroConfig) => {
269
+ nitroConfig.plugins = nitroConfig.plugins || [];
270
+ // Add our generated plugin after the cron plugin
271
+ nitroConfig.plugins.push('#build/server/plugins/tether-functions');
272
+ });
131
273
  },
132
274
  });
package/nuxt/module.ts CHANGED
@@ -26,6 +26,8 @@ export interface TetherModuleOptions {
26
26
  projectId?: string;
27
27
  /** API endpoint (defaults to Tether Cloud) */
28
28
  url?: string;
29
+ /** Enable verbose logging for debugging WebSocket connections */
30
+ verbose?: boolean;
29
31
  }
30
32
 
31
33
  export default defineNuxtModule<TetherModuleOptions>({
@@ -48,6 +50,7 @@ export default defineNuxtModule<TetherModuleOptions>({
48
50
  const apiKey = process.env.TETHER_API_KEY || '';
49
51
  const url = options.url || process.env.TETHER_URL || 'https://tether-api.strands.gg';
50
52
  const projectId = options.projectId || process.env.TETHER_PROJECT_ID || '';
53
+ const verbose = options.verbose ?? (process.env.TETHER_VERBOSE === 'true');
51
54
 
52
55
  // Calculate WebSocket URL from HTTP URL
53
56
  const wsUrl = url.replace(/^https:/, 'wss:').replace(/^http:/, 'ws:');
@@ -57,17 +60,19 @@ export default defineNuxtModule<TetherModuleOptions>({
57
60
  // - NUXT_TETHER_API_KEY
58
61
  // - NUXT_TETHER_URL
59
62
  // - NUXT_TETHER_PROJECT_ID
60
- nuxt.options.runtimeConfig.tether = {
63
+ // - NUXT_TETHER_VERBOSE
64
+ (nuxt.options.runtimeConfig as any).tether = {
61
65
  apiKey,
62
66
  url,
63
67
  projectId,
68
+ verbose,
64
69
  };
65
70
 
66
71
  // Public config (safe for client - no secrets)
67
72
  // Can be overridden at runtime via:
68
73
  // - NUXT_PUBLIC_TETHER_PROJECT_ID
69
74
  // - NUXT_PUBLIC_TETHER_WS_URL
70
- nuxt.options.runtimeConfig.public.tether = {
75
+ (nuxt.options.runtimeConfig.public as any).tether = {
71
76
  projectId,
72
77
  wsUrl,
73
78
  };
@@ -140,7 +145,7 @@ export default defineNuxtModule<TetherModuleOptions>({
140
145
 
141
146
  // Add Nitro plugin for cron WebSocket connection
142
147
  // This auto-connects to Tether when the server starts
143
- nuxt.hook('nitro:config', (nitroConfig) => {
148
+ nuxt.hook('nitro:config' as any, (nitroConfig: any) => {
144
149
  nitroConfig.plugins = nitroConfig.plugins || [];
145
150
  // Use direct path - Nitro will handle the #imports resolution at build time
146
151
  nitroConfig.plugins.push(resolver.resolve('./runtime/server/plugins/cron.js'));
@@ -150,5 +155,146 @@ export default defineNuxtModule<TetherModuleOptions>({
150
155
  nitroConfig.externals.inline = nitroConfig.externals.inline || [];
151
156
  nitroConfig.externals.inline.push('@tthr/vue');
152
157
  });
158
+
159
+ // Generate a server plugin to auto-register cron handlers from tether/functions
160
+ // This runs at build time and creates a plugin that imports user's functions
161
+ addTemplate({
162
+ filename: 'server/plugins/tether-functions.ts',
163
+ write: true,
164
+ getContents: () => `
165
+ /**
166
+ * Auto-generated plugin to register Tether functions as cron handlers
167
+ * This file is generated by @tthr/vue/nuxt module
168
+ */
169
+ import { defineNitroPlugin } from 'nitropack/runtime';
170
+ import { registerCronHandler } from '@tthr/vue/nuxt/runtime/server/plugins/cron.js';
171
+ import { useTetherServer } from '@tthr/vue/nuxt/runtime/server/utils/tether.js';
172
+
173
+ // Import all functions from tether/functions
174
+ // @ts-ignore - user's functions may not exist yet
175
+ import * as tetherFunctions from '~/tether/functions';
176
+
177
+ /**
178
+ * Create a simple logger for function execution
179
+ */
180
+ function createLogger(functionName: string) {
181
+ const prefix = \`[Tether] \${functionName}\`;
182
+ return {
183
+ log: (msg: string, data?: unknown) => console.log(prefix, msg, data !== undefined ? data : ''),
184
+ info: (msg: string, data?: unknown) => console.log(prefix, msg, data !== undefined ? data : ''),
185
+ warn: (msg: string, data?: unknown) => console.warn(prefix, msg, data !== undefined ? data : ''),
186
+ error: (msg: string, data?: unknown) => console.error(prefix, msg, data !== undefined ? data : ''),
187
+ debug: (msg: string, data?: unknown) => console.debug(prefix, msg, data !== undefined ? data : ''),
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Create the handler context for a function
193
+ */
194
+ function createHandlerContext(functionName: string, args: unknown) {
195
+ const tether = useTetherServer(null as any);
196
+ const log = createLogger(functionName);
197
+
198
+ // Create db proxy that uses tether server client
199
+ // TODO: This is a placeholder - proper db operations need to be implemented
200
+ const db = new Proxy({}, {
201
+ get(_, tableName: string) {
202
+ return {
203
+ findMany: async (options?: any) => {
204
+ // Call Tether API for db operations
205
+ return tether.query(\`_db.\${tableName}.findMany\`, options);
206
+ },
207
+ findFirst: async (options?: any) => {
208
+ return tether.query(\`_db.\${tableName}.findFirst\`, options);
209
+ },
210
+ findUnique: async (options?: any) => {
211
+ return tether.query(\`_db.\${tableName}.findUnique\`, options);
212
+ },
213
+ findById: async (id: unknown) => {
214
+ return tether.query(\`_db.\${tableName}.findById\`, { id });
215
+ },
216
+ count: async (options?: any) => {
217
+ return tether.query(\`_db.\${tableName}.count\`, options);
218
+ },
219
+ create: async (options: any) => {
220
+ return tether.mutation(\`_db.\${tableName}.create\`, options);
221
+ },
222
+ insert: async (data: any) => {
223
+ return tether.mutation(\`_db.\${tableName}.insert\`, { data });
224
+ },
225
+ insertMany: async (data: any[]) => {
226
+ return tether.mutation(\`_db.\${tableName}.insertMany\`, { data });
227
+ },
228
+ update: async (options: any) => {
229
+ return tether.mutation(\`_db.\${tableName}.update\`, options);
230
+ },
231
+ upsert: async (options: any) => {
232
+ return tether.mutation(\`_db.\${tableName}.upsert\`, options);
233
+ },
234
+ delete: async (options: any) => {
235
+ return tether.mutation(\`_db.\${tableName}.delete\`, options);
236
+ },
237
+ deleteById: async (id: unknown) => {
238
+ return tether.mutation(\`_db.\${tableName}.deleteById\`, { id });
239
+ },
240
+ };
241
+ },
242
+ });
243
+
244
+ // Create tether context for actions
245
+ const tetherContext = {
246
+ query: tether.query.bind(tether),
247
+ mutation: tether.mutation.bind(tether),
248
+ env: new Proxy({}, {
249
+ get(_, key: string) {
250
+ return process.env[key];
251
+ },
252
+ }),
253
+ };
254
+
255
+ return {
256
+ args,
257
+ db,
258
+ ctx: { auth: { userId: null, claims: {} }, userId: null },
259
+ log,
260
+ tether: tetherContext,
261
+ };
262
+ }
263
+
264
+ export default defineNitroPlugin(() => {
265
+ console.log('[Tether] Auto-registering functions from tether/functions...');
266
+
267
+ // Iterate through all exported modules
268
+ for (const [moduleName, moduleExports] of Object.entries(tetherFunctions)) {
269
+ if (!moduleExports || typeof moduleExports !== 'object') continue;
270
+
271
+ // Iterate through all exports in each module
272
+ for (const [fnName, fnDef] of Object.entries(moduleExports as Record<string, any>)) {
273
+ // Check if it's a Tether function definition (has handler property)
274
+ if (!fnDef || typeof fnDef !== 'object' || typeof fnDef.handler !== 'function') continue;
275
+
276
+ const fullName = \`\${moduleName}.\${fnName}\`;
277
+
278
+ // Register the cron handler
279
+ registerCronHandler(fullName, async (args: unknown) => {
280
+ const context = createHandlerContext(fullName, args);
281
+ return fnDef.handler(context);
282
+ });
283
+
284
+ console.log(\`[Tether] Registered handler: \${fullName}\`);
285
+ }
286
+ }
287
+
288
+ console.log('[Tether] Function registration complete');
289
+ });
290
+ `,
291
+ });
292
+
293
+ // Add the generated plugin to Nitro
294
+ nuxt.hook('nitro:config' as any, (nitroConfig: any) => {
295
+ nitroConfig.plugins = nitroConfig.plugins || [];
296
+ // Add our generated plugin after the cron plugin
297
+ nitroConfig.plugins.push('#build/server/plugins/tether-functions');
298
+ });
153
299
  },
154
300
  });
@@ -10,6 +10,16 @@ import { useRuntimeConfig } from '#imports';
10
10
  let functionRegistry = null;
11
11
  let registryError = null;
12
12
 
13
+ // Verbose logging support
14
+ let verboseLogging = false;
15
+ const PREFIX = '[Tether]';
16
+ const log = {
17
+ debug: (...args) => { if (verboseLogging) console.log(PREFIX, ...args); },
18
+ info: (...args) => console.log(PREFIX, ...args),
19
+ warn: (...args) => console.warn(PREFIX, ...args),
20
+ error: (...args) => console.error(PREFIX, ...args),
21
+ };
22
+
13
23
  /**
14
24
  * Try to load the user's custom functions from ~/tether/functions
15
25
  */
@@ -21,23 +31,23 @@ async function loadFunctionRegistry() {
21
31
  try {
22
32
  // Try to import the user's functions index
23
33
  // This path is relative to the Nuxt app's server runtime
24
- console.log('[Tether] Loading custom functions from ~~/tether/functions/index.ts');
34
+ log.debug('Loading custom functions from ~~/tether/functions/index.ts');
25
35
  const functions = await import('~~/tether/functions/index.ts').catch((err) => {
26
- console.warn('[Tether] Failed to import functions:', err.message);
36
+ log.debug('Failed to import functions:', err.message);
27
37
  return null;
28
38
  });
29
39
 
30
40
  if (functions) {
31
- console.log('[Tether] Loaded function modules:', Object.keys(functions));
41
+ log.debug('Loaded function modules:', Object.keys(functions));
32
42
  functionRegistry = functions;
33
43
  } else {
34
44
  // No custom functions found - that's OK, we'll proxy everything
35
- console.log('[Tether] No custom functions found, will proxy all requests');
45
+ log.debug('No custom functions found, will proxy all requests');
36
46
  functionRegistry = {};
37
47
  }
38
48
  } catch (error) {
39
49
  // Failed to load - we'll proxy everything to Tether API
40
- console.warn('[Tether] Could not load custom functions:', error.message);
50
+ log.debug('Could not load custom functions:', error.message);
41
51
  registryError = error;
42
52
  functionRegistry = {};
43
53
  }
@@ -217,6 +227,7 @@ function createDatabaseProxy(apiKey, url, projectId) {
217
227
 
218
228
  export default defineEventHandler(async (event) => {
219
229
  const config = useRuntimeConfig();
230
+ verboseLogging = config.tether?.verbose || process.env.TETHER_VERBOSE === 'true';
220
231
 
221
232
  // Get API key from runtime config (populated from TETHER_API_KEY env var)
222
233
  const apiKey = config.tether?.apiKey || process.env.TETHER_API_KEY;
@@ -251,12 +262,12 @@ export default defineEventHandler(async (event) => {
251
262
 
252
263
  // Try to find a custom function
253
264
  const customFn = lookupFunction(body.function);
254
- console.log(`[Tether] Mutation: ${body.function}, custom function found: ${!!customFn}`);
265
+ log.debug(`Mutation: ${body.function}, custom function found: ${!!customFn}`);
255
266
 
256
267
  if (customFn) {
257
268
  // Execute locally with database proxy
258
269
  try {
259
- console.log(`[Tether] Executing custom mutation: ${body.function}`);
270
+ log.debug(`Executing custom mutation: ${body.function}`);
260
271
  const db = createDatabaseProxy(apiKey, url, projectId);
261
272
 
262
273
  // Create auth context - try to get user identity from request
@@ -288,7 +299,7 @@ export default defineEventHandler(async (event) => {
288
299
  }
289
300
  }
290
301
  } catch (authError) {
291
- console.warn('[Tether] Auth validation failed:', authError.message);
302
+ log.warn('Auth validation failed:', authError.message);
292
303
  // Auth validation failed - continue without identity
293
304
  }
294
305
  }
@@ -315,7 +326,7 @@ export default defineEventHandler(async (event) => {
315
326
 
316
327
  return { data: result };
317
328
  } catch (error) {
318
- console.error(`[Tether] Mutation ${body.function} failed:`, error);
329
+ log.error(`Mutation ${body.function} failed:`, error);
319
330
  // Return JSON error response instead of throwing (avoids proxy HTML error pages)
320
331
  setResponseStatus(event, 400);
321
332
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"cron.d.ts","sourceRoot":"","sources":["cron.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAsBH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,GAC3C,IAAI,CAGN;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAEhE;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAE1C;;AAqPD,wBA+BG"}
1
+ {"version":3,"file":"cron.d.ts","sourceRoot":"","sources":["cron.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAoCH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,GAC3C,IAAI,CAGN;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAEhE;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAE1C;;AAyPD,wBAiCG"}
@@ -20,10 +20,24 @@ let heartbeatTimer = null;
20
20
  let heartbeatTimeoutTimer = null;
21
21
  let awaitingPong = false;
22
22
  let shouldReconnect = true;
23
+ let verboseLogging = false;
23
24
  const MAX_RECONNECT_ATTEMPTS = 10;
24
25
  const HEARTBEAT_INTERVAL = 20000; // 20 seconds - keeps connection alive through proxies with 30s idle timeout
25
26
  const HEARTBEAT_TIMEOUT = 10000;
26
27
  const RECONNECT_DELAY = 1000;
28
+ const PREFIX = '[Tether Cron]';
29
+ /** Simple logger that respects verbose flag */
30
+ const log = {
31
+ /** Debug messages - only shown when verbose is enabled */
32
+ debug: (...args) => { if (verboseLogging)
33
+ console.log(PREFIX, ...args); },
34
+ /** Info messages - always shown */
35
+ info: (...args) => console.log(PREFIX, ...args),
36
+ /** Warning messages - always shown */
37
+ warn: (...args) => console.warn(PREFIX, ...args),
38
+ /** Error messages - always shown */
39
+ error: (...args) => console.error(PREFIX, ...args),
40
+ };
27
41
  /**
28
42
  * Register a cron handler for a specific function
29
43
  *
@@ -40,7 +54,7 @@ const RECONNECT_DELAY = 1000;
40
54
  */
41
55
  export function registerCronHandler(functionName, handler) {
42
56
  cronHandlers.set(functionName, handler);
43
- console.log(`[Tether Cron] Registered handler for: ${functionName}`);
57
+ log.debug('Registered handler for:', functionName);
44
58
  }
45
59
  /**
46
60
  * Unregister a cron handler
@@ -97,22 +111,22 @@ async function reportExecution(config, trigger, result, durationMs) {
97
111
  }),
98
112
  });
99
113
  if (!response.ok) {
100
- console.error(`[Tether Cron] Failed to report execution: ${response.status} ${response.statusText}`);
114
+ log.error(`Failed to report execution: ${response.status} ${response.statusText}`);
101
115
  }
102
116
  else {
103
- console.log(`[Tether Cron] Reported execution ${trigger.executionId}: ${result.success ? 'success' : 'failed'}`);
117
+ log.debug(`Reported execution ${trigger.executionId}: ${result.success ? 'success' : 'failed'}`);
104
118
  }
105
119
  }
106
120
  catch (error) {
107
- console.error('[Tether Cron] Failed to report execution:', error);
121
+ log.error('Failed to report execution:', error);
108
122
  }
109
123
  }
110
124
  async function handleCronTrigger(config, trigger) {
111
- console.log(`[Tether Cron] Received trigger for ${trigger.functionName} (execution: ${trigger.executionId})`);
125
+ log.info(`Received trigger for ${trigger.functionName} (execution: ${trigger.executionId})`);
112
126
  const startTime = Date.now();
113
127
  const handler = cronHandlers.get(trigger.functionName);
114
128
  if (!handler) {
115
- console.warn(`[Tether Cron] No handler registered for: ${trigger.functionName}`);
129
+ log.warn(`No handler registered for: ${trigger.functionName}`);
116
130
  const durationMs = Date.now() - startTime;
117
131
  await reportExecution(config, trigger, {
118
132
  success: false,
@@ -131,7 +145,7 @@ async function handleCronTrigger(config, trigger) {
131
145
  catch (error) {
132
146
  const durationMs = Date.now() - startTime;
133
147
  const errorMessage = error instanceof Error ? error.message : String(error);
134
- console.error(`[Tether Cron] Handler error for ${trigger.functionName}:`, errorMessage);
148
+ log.error(`Handler error for ${trigger.functionName}:`, errorMessage);
135
149
  await reportExecution(config, trigger, {
136
150
  success: false,
137
151
  error: errorMessage,
@@ -143,17 +157,17 @@ function startHeartbeat() {
143
157
  heartbeatTimer = setInterval(() => {
144
158
  if (ws?.readyState === WebSocket.OPEN) {
145
159
  awaitingPong = true;
146
- console.log('[Tether Cron] Sending heartbeat ping');
160
+ log.debug('Sending heartbeat ping');
147
161
  ws.send(JSON.stringify({ type: 'ping' }));
148
162
  heartbeatTimeoutTimer = setTimeout(() => {
149
163
  if (awaitingPong) {
150
- console.warn('[Tether Cron] Heartbeat timeout - forcing reconnect');
164
+ log.warn('Heartbeat timeout - forcing reconnect');
151
165
  ws?.close();
152
166
  }
153
167
  }, HEARTBEAT_TIMEOUT);
154
168
  }
155
169
  else {
156
- console.warn(`[Tether Cron] Cannot send ping - WebSocket state: ${ws?.readyState}`);
170
+ log.warn(`Cannot send ping - WebSocket state: ${ws?.readyState}`);
157
171
  }
158
172
  }, HEARTBEAT_INTERVAL);
159
173
  }
@@ -180,7 +194,7 @@ function connect(config) {
180
194
  ws = new WebSocketImpl(url);
181
195
  ws.onopen = () => {
182
196
  reconnectAttempts = 0;
183
- console.log('[Tether Cron] WebSocket connected');
197
+ log.debug('WebSocket connected');
184
198
  };
185
199
  ws.onmessage = async (event) => {
186
200
  const data = typeof event.data === 'string' ? event.data : event.data.toString();
@@ -190,13 +204,13 @@ function connect(config) {
190
204
  case 'connected':
191
205
  connectionId = message.connection_id ?? null;
192
206
  startHeartbeat();
193
- console.log(`[Tether Cron] Connected with ID: ${connectionId}`);
207
+ log.info(`Connected with ID: ${connectionId}`);
194
208
  break;
195
209
  case 'cron_trigger':
196
210
  await handleCronTrigger(config, message);
197
211
  break;
198
212
  case 'pong':
199
- console.log('[Tether Cron] Received pong');
213
+ log.debug('Received pong');
200
214
  awaitingPong = false;
201
215
  if (heartbeatTimeoutTimer) {
202
216
  clearTimeout(heartbeatTimeoutTimer);
@@ -204,27 +218,27 @@ function connect(config) {
204
218
  }
205
219
  break;
206
220
  case 'error':
207
- console.error('[Tether Cron] Server error:', message.error);
221
+ log.error('Server error:', message.error);
208
222
  break;
209
223
  }
210
224
  }
211
225
  catch (e) {
212
- console.error('[Tether Cron] Failed to parse message:', e);
226
+ log.error('Failed to parse message:', e);
213
227
  }
214
228
  };
215
229
  ws.onerror = (error) => {
216
230
  const err = error instanceof Error ? error : new Error('WebSocket error');
217
- console.error('[Tether Cron] WebSocket error:', err.message);
231
+ log.error('WebSocket error:', err.message);
218
232
  };
219
233
  ws.onclose = (event) => {
220
234
  connectionId = null;
221
235
  stopHeartbeat();
222
- console.log(`[Tether Cron] WebSocket disconnected (code: ${event.code}, reason: ${event.reason || 'none'})`);
236
+ log.debug(`WebSocket disconnected (code: ${event.code}, reason: ${event.reason || 'none'})`);
223
237
  handleReconnect(config);
224
238
  };
225
239
  }
226
240
  catch (error) {
227
- console.error('[Tether Cron] Failed to connect:', error);
241
+ log.error('Failed to connect:', error);
228
242
  handleReconnect(config);
229
243
  }
230
244
  }
@@ -233,12 +247,12 @@ function handleReconnect(config) {
233
247
  return;
234
248
  }
235
249
  if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
236
- console.error('[Tether Cron] Max reconnection attempts reached');
250
+ log.error('Max reconnection attempts reached');
237
251
  return;
238
252
  }
239
253
  reconnectAttempts++;
240
254
  const delay = Math.min(RECONNECT_DELAY * Math.pow(2, reconnectAttempts - 1), 30000);
241
- console.log(`[Tether Cron] Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
255
+ log.debug(`Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
242
256
  setTimeout(() => {
243
257
  connect(config);
244
258
  }, delay);
@@ -246,15 +260,17 @@ function handleReconnect(config) {
246
260
  export default defineNitroPlugin((nitro) => {
247
261
  // Get config from runtime config
248
262
  const config = useRuntimeConfig();
249
- const apiKey = config.tether?.apiKey || process.env.TETHER_API_KEY;
250
- const url = config.tether?.url || process.env.TETHER_URL || 'https://tether-api.strands.gg';
251
- const projectId = config.tether?.projectId || process.env.TETHER_PROJECT_ID;
263
+ const tetherConfig = config.tether;
264
+ const apiKey = tetherConfig?.apiKey || process.env.TETHER_API_KEY;
265
+ const url = tetherConfig?.url || process.env.TETHER_URL || 'https://tether-api.strands.gg';
266
+ const projectId = tetherConfig?.projectId || process.env.TETHER_PROJECT_ID;
267
+ verboseLogging = tetherConfig?.verbose || process.env.TETHER_VERBOSE === 'true';
252
268
  // Only connect if we have all required config
253
269
  if (!apiKey || !projectId) {
254
- console.log('[Tether Cron] Missing config (apiKey or projectId) - cron connection disabled');
270
+ log.debug('Missing config (apiKey or projectId) - cron connection disabled');
255
271
  return;
256
272
  }
257
- console.log('[Tether Cron] Initialising cron connection...');
273
+ log.info('Initialising cron connection...');
258
274
  // Connect on next tick to allow handlers to be registered first
259
275
  process.nextTick(() => {
260
276
  connect({ url, projectId, apiKey });
@@ -267,7 +283,6 @@ export default defineNitroPlugin((nitro) => {
267
283
  ws.close();
268
284
  ws = null;
269
285
  }
270
- console.log('[Tether Cron] Connection closed');
286
+ log.debug('Connection closed');
271
287
  });
272
288
  });
273
- //# sourceMappingURL=cron.js.map
@@ -10,6 +10,16 @@ import { useRuntimeConfig } from '#imports';
10
10
  let functionRegistry = null;
11
11
  let registryError = null;
12
12
 
13
+ // Verbose logging support
14
+ let verboseLogging = false;
15
+ const PREFIX = '[Tether]';
16
+ const log = {
17
+ debug: (...args) => { if (verboseLogging) console.log(PREFIX, ...args); },
18
+ info: (...args) => console.log(PREFIX, ...args),
19
+ warn: (...args) => console.warn(PREFIX, ...args),
20
+ error: (...args) => console.error(PREFIX, ...args),
21
+ };
22
+
13
23
  /**
14
24
  * Try to load the user's custom functions from ~/tether/functions
15
25
  */
@@ -21,23 +31,23 @@ async function loadFunctionRegistry() {
21
31
  try {
22
32
  // Try to import the user's functions index
23
33
  // This path is relative to the Nuxt app's server runtime
24
- console.log('[Tether] Loading custom functions from ~~/tether/functions/index.ts');
34
+ log.debug('Loading custom functions from ~~/tether/functions/index.ts');
25
35
  const functions = await import('~~/tether/functions/index.ts').catch((err) => {
26
- console.warn('[Tether] Failed to import functions:', err.message);
36
+ log.debug('Failed to import functions:', err.message);
27
37
  return null;
28
38
  });
29
39
 
30
40
  if (functions) {
31
- console.log('[Tether] Loaded function modules:', Object.keys(functions));
41
+ log.debug('Loaded function modules:', Object.keys(functions));
32
42
  functionRegistry = functions;
33
43
  } else {
34
44
  // No custom functions found - that's OK, we'll proxy everything
35
- console.log('[Tether] No custom functions found, will proxy all requests');
45
+ log.debug('No custom functions found, will proxy all requests');
36
46
  functionRegistry = {};
37
47
  }
38
48
  } catch (error) {
39
49
  // Failed to load - we'll proxy everything to Tether API
40
- console.warn('[Tether] Could not load custom functions:', error.message);
50
+ log.debug('Could not load custom functions:', error.message);
41
51
  registryError = error;
42
52
  functionRegistry = {};
43
53
  }
@@ -217,6 +227,7 @@ function createDatabaseProxy(apiKey, url, projectId) {
217
227
 
218
228
  export default defineEventHandler(async (event) => {
219
229
  const config = useRuntimeConfig();
230
+ verboseLogging = config.tether?.verbose || process.env.TETHER_VERBOSE === 'true';
220
231
 
221
232
  // Get API key from runtime config (populated from TETHER_API_KEY env var)
222
233
  const apiKey = config.tether?.apiKey || process.env.TETHER_API_KEY;
@@ -251,7 +262,7 @@ export default defineEventHandler(async (event) => {
251
262
 
252
263
  // Try to find a custom function
253
264
  const customFn = lookupFunction(body.function);
254
- console.log(`[Tether] Query: ${body.function}, custom function found: ${!!customFn}`);
265
+ log.debug(`Query: ${body.function}, custom function found: ${!!customFn}`);
255
266
 
256
267
  if (customFn) {
257
268
  // Execute locally with database proxy
@@ -288,7 +299,7 @@ export default defineEventHandler(async (event) => {
288
299
  }
289
300
  }
290
301
  } catch (authError) {
291
- console.warn('[Tether] Auth validation failed:', authError.message);
302
+ log.warn('Auth validation failed:', authError.message);
292
303
  // Auth validation failed - continue without identity
293
304
  }
294
305
  }
@@ -306,18 +317,18 @@ export default defineEventHandler(async (event) => {
306
317
  userId: userIdentity?.subject ?? null,
307
318
  };
308
319
 
309
- console.log(`[Tether] Executing custom function: ${body.function}`);
320
+ log.debug(`Executing custom function: ${body.function}`);
310
321
  const result = await customFn.handler({
311
322
  db,
312
323
  ctx,
313
324
  auth,
314
325
  args: body.args ?? {},
315
326
  });
316
- console.log(`[Tether] Function ${body.function} completed`);
327
+ log.debug(`Function ${body.function} completed`);
317
328
 
318
329
  return { data: result };
319
330
  } catch (error) {
320
- console.error(`[Tether] Function ${body.function} failed:`, error.message || error);
331
+ log.error(`Function ${body.function} failed:`, error.message || error);
321
332
  throw createError({
322
333
  statusCode: 500,
323
334
  message: error.message || 'Function execution failed',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tthr/vue",
3
- "version": "0.0.41",
3
+ "version": "0.0.45",
4
4
  "description": "Tether Vue/Nuxt SDK",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",