@tthr/vue 0.0.13 → 0.0.14

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.ts CHANGED
@@ -19,7 +19,7 @@
19
19
  * - TETHER_API_KEY: Your project's API key (required, kept server-side)
20
20
  */
21
21
 
22
- import { defineNuxtModule, addPlugin, createResolver, addImports, addComponent, addServerHandler } from '@nuxt/kit';
22
+ import { defineNuxtModule, addPlugin, createResolver, addImports, addComponent, addServerHandler, addServerImports } from '@nuxt/kit';
23
23
 
24
24
  export interface TetherModuleOptions {
25
25
  /** Project ID from Tether dashboard */
@@ -101,5 +101,13 @@ export default defineNuxtModule<TetherModuleOptions>({
101
101
  name: 'TetherWelcome',
102
102
  filePath: resolver.resolve('./runtime/components/TetherWelcome.vue'),
103
103
  });
104
+
105
+ // Auto-import server utilities (available in server/ directory)
106
+ addServerImports([
107
+ {
108
+ name: 'useTetherServer',
109
+ from: resolver.resolve('./runtime/server/utils/tether'),
110
+ },
111
+ ]);
104
112
  },
105
113
  });
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Server-side Tether utilities for Nuxt
3
+ *
4
+ * Use these in your Nuxt server endpoints (e.g., server/api/*.ts)
5
+ * to query and mutate Tether data with full type safety.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * // server/api/my-endpoint.ts
10
+ * export default defineEventHandler(async (event) => {
11
+ * const tether = useTetherServer(event);
12
+ *
13
+ * const todos = await tether.query('getTodos', { completed: false });
14
+ * await tether.mutation('createTodo', { title: 'New todo' });
15
+ *
16
+ * return { todos };
17
+ * });
18
+ * ```
19
+ */
20
+
21
+ import { useRuntimeConfig, createError, type H3Event } from '#imports';
22
+
23
+ export interface TetherServerClient {
24
+ /**
25
+ * Execute a query on the Tether server
26
+ */
27
+ query: <TResult = unknown>(
28
+ name: string,
29
+ args?: Record<string, unknown>
30
+ ) => Promise<TResult>;
31
+
32
+ /**
33
+ * Execute a mutation on the Tether server
34
+ */
35
+ mutation: <TResult = unknown>(
36
+ name: string,
37
+ args?: Record<string, unknown>
38
+ ) => Promise<TResult>;
39
+
40
+ /**
41
+ * File storage operations
42
+ */
43
+ storage: {
44
+ /**
45
+ * Generate a presigned upload URL
46
+ */
47
+ generateUploadUrl: (options: {
48
+ filename: string;
49
+ contentType: string;
50
+ metadata?: Record<string, unknown>;
51
+ }) => Promise<{ assetId: string; uploadUrl: string; expiresAt: string }>;
52
+
53
+ /**
54
+ * Confirm an upload has completed
55
+ */
56
+ confirmUpload: (assetId: string, sha256?: string) => Promise<AssetMetadata>;
57
+
58
+ /**
59
+ * Get a presigned download URL for an asset
60
+ */
61
+ getUrl: (assetId: string) => Promise<string>;
62
+
63
+ /**
64
+ * Get metadata for an asset
65
+ */
66
+ getMetadata: (assetId: string) => Promise<AssetMetadata>;
67
+
68
+ /**
69
+ * Delete an asset
70
+ */
71
+ delete: (assetId: string) => Promise<void>;
72
+
73
+ /**
74
+ * List assets with optional pagination and filtering
75
+ */
76
+ list: (options?: {
77
+ limit?: number;
78
+ offset?: number;
79
+ contentType?: string;
80
+ }) => Promise<{ assets: AssetMetadata[]; totalCount: number }>;
81
+ };
82
+
83
+ /**
84
+ * The project ID
85
+ */
86
+ projectId: string;
87
+
88
+ /**
89
+ * The API URL
90
+ */
91
+ url: string;
92
+ }
93
+
94
+ export interface AssetMetadata {
95
+ id: string;
96
+ filename: string;
97
+ contentType: string;
98
+ size: number;
99
+ sha256?: string;
100
+ createdAt: string;
101
+ metadata?: Record<string, unknown>;
102
+ }
103
+
104
+ /**
105
+ * Get a Tether client for use in server-side code
106
+ *
107
+ * @param event - The H3 event (optional, used for request context)
108
+ * @returns A Tether client with query, mutation, and storage methods
109
+ */
110
+ export function useTetherServer(_event?: H3Event): TetherServerClient {
111
+ const config = useRuntimeConfig();
112
+
113
+ const apiKey = config.tether?.apiKey || process.env.TETHER_API_KEY;
114
+ const url = config.tether?.url || process.env.TETHER_URL || 'https://tether-api.strands.gg';
115
+ const projectId = config.tether?.projectId || process.env.TETHER_PROJECT_ID;
116
+
117
+ if (!apiKey) {
118
+ throw createError({
119
+ statusCode: 500,
120
+ message: 'Tether API key not configured. Set TETHER_API_KEY environment variable.',
121
+ });
122
+ }
123
+
124
+ if (!projectId) {
125
+ throw createError({
126
+ statusCode: 500,
127
+ message: 'Tether project ID not configured. Set TETHER_PROJECT_ID or configure in nuxt.config.ts.',
128
+ });
129
+ }
130
+
131
+ const headers = {
132
+ 'Content-Type': 'application/json',
133
+ 'Authorization': `Bearer ${apiKey}`,
134
+ };
135
+
136
+ async function query<TResult = unknown>(
137
+ name: string,
138
+ args?: Record<string, unknown>
139
+ ): Promise<TResult> {
140
+ const response = await fetch(`${url}/api/v1/projects/${projectId}/query`, {
141
+ method: 'POST',
142
+ headers,
143
+ body: JSON.stringify({ function: name, args }),
144
+ });
145
+
146
+ if (!response.ok) {
147
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
148
+ throw createError({
149
+ statusCode: response.status,
150
+ message: error.error || `Query '${name}' failed`,
151
+ });
152
+ }
153
+
154
+ const result = await response.json();
155
+ return result.data as TResult;
156
+ }
157
+
158
+ async function mutation<TResult = unknown>(
159
+ name: string,
160
+ args?: Record<string, unknown>
161
+ ): Promise<TResult> {
162
+ const response = await fetch(`${url}/api/v1/projects/${projectId}/mutation`, {
163
+ method: 'POST',
164
+ headers,
165
+ body: JSON.stringify({ function: name, args }),
166
+ });
167
+
168
+ if (!response.ok) {
169
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
170
+ throw createError({
171
+ statusCode: response.status,
172
+ message: error.error || `Mutation '${name}' failed`,
173
+ });
174
+ }
175
+
176
+ const result = await response.json();
177
+ return result.data as TResult;
178
+ }
179
+
180
+ const storage = {
181
+ async generateUploadUrl(options: {
182
+ filename: string;
183
+ contentType: string;
184
+ metadata?: Record<string, unknown>;
185
+ }) {
186
+ const response = await fetch(`${url}/api/v1/projects/${projectId}/assets/upload-url`, {
187
+ method: 'POST',
188
+ headers,
189
+ body: JSON.stringify({
190
+ filename: options.filename,
191
+ content_type: options.contentType,
192
+ metadata: options.metadata,
193
+ }),
194
+ });
195
+
196
+ if (!response.ok) {
197
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
198
+ throw createError({
199
+ statusCode: response.status,
200
+ message: error.error || 'Failed to generate upload URL',
201
+ });
202
+ }
203
+
204
+ const result = await response.json();
205
+ return {
206
+ assetId: result.asset_id,
207
+ uploadUrl: result.upload_url,
208
+ expiresAt: result.expires_at,
209
+ };
210
+ },
211
+
212
+ async confirmUpload(assetId: string, sha256?: string): Promise<AssetMetadata> {
213
+ const response = await fetch(`${url}/api/v1/projects/${projectId}/assets/${assetId}/complete`, {
214
+ method: 'POST',
215
+ headers,
216
+ body: JSON.stringify({ sha256 }),
217
+ });
218
+
219
+ if (!response.ok) {
220
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
221
+ throw createError({
222
+ statusCode: response.status,
223
+ message: error.error || 'Failed to confirm upload',
224
+ });
225
+ }
226
+
227
+ const result = await response.json();
228
+ return mapAssetMetadata(result.asset);
229
+ },
230
+
231
+ async getUrl(assetId: string): Promise<string> {
232
+ const response = await fetch(`${url}/api/v1/projects/${projectId}/assets/${assetId}/url`, {
233
+ method: 'GET',
234
+ headers,
235
+ });
236
+
237
+ if (!response.ok) {
238
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
239
+ throw createError({
240
+ statusCode: response.status,
241
+ message: error.error || 'Failed to get asset URL',
242
+ });
243
+ }
244
+
245
+ const result = await response.json();
246
+ return result.url;
247
+ },
248
+
249
+ async getMetadata(assetId: string): Promise<AssetMetadata> {
250
+ const response = await fetch(`${url}/api/v1/projects/${projectId}/assets/${assetId}`, {
251
+ method: 'GET',
252
+ headers,
253
+ });
254
+
255
+ if (!response.ok) {
256
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
257
+ throw createError({
258
+ statusCode: response.status,
259
+ message: error.error || 'Failed to get asset metadata',
260
+ });
261
+ }
262
+
263
+ const result = await response.json();
264
+ return mapAssetMetadata(result.asset);
265
+ },
266
+
267
+ async delete(assetId: string): Promise<void> {
268
+ const response = await fetch(`${url}/api/v1/projects/${projectId}/assets/${assetId}`, {
269
+ method: 'DELETE',
270
+ headers,
271
+ });
272
+
273
+ if (!response.ok) {
274
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
275
+ throw createError({
276
+ statusCode: response.status,
277
+ message: error.error || 'Failed to delete asset',
278
+ });
279
+ }
280
+ },
281
+
282
+ async list(options?: {
283
+ limit?: number;
284
+ offset?: number;
285
+ contentType?: string;
286
+ }): Promise<{ assets: AssetMetadata[]; totalCount: number }> {
287
+ const params = new URLSearchParams();
288
+ if (options?.limit) params.set('limit', String(options.limit));
289
+ if (options?.offset) params.set('offset', String(options.offset));
290
+ if (options?.contentType) params.set('content_type', options.contentType);
291
+
292
+ const queryString = params.toString();
293
+ const fetchUrl = `${url}/api/v1/projects/${projectId}/assets${queryString ? `?${queryString}` : ''}`;
294
+
295
+ const response = await fetch(fetchUrl, {
296
+ method: 'GET',
297
+ headers,
298
+ });
299
+
300
+ if (!response.ok) {
301
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
302
+ throw createError({
303
+ statusCode: response.status,
304
+ message: error.error || 'Failed to list assets',
305
+ });
306
+ }
307
+
308
+ const result = await response.json();
309
+ return {
310
+ assets: result.assets.map(mapAssetMetadata),
311
+ totalCount: result.total_count,
312
+ };
313
+ },
314
+ };
315
+
316
+ return {
317
+ query,
318
+ mutation,
319
+ storage,
320
+ projectId,
321
+ url,
322
+ };
323
+ }
324
+
325
+ /**
326
+ * Map snake_case API response to camelCase
327
+ */
328
+ function mapAssetMetadata(asset: Record<string, unknown>): AssetMetadata {
329
+ return {
330
+ id: asset.id as string,
331
+ filename: asset.filename as string,
332
+ contentType: asset.content_type as string,
333
+ size: asset.size as number,
334
+ sha256: asset.sha256 as string | undefined,
335
+ createdAt: asset.created_at as string,
336
+ metadata: asset.metadata as Record<string, unknown> | undefined,
337
+ };
338
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tthr/vue",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "Tether Vue/Nuxt SDK",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",