@moogodev/client 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -2
- package/dist/client.d.ts +155 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +546 -0
- package/dist/errors.d.ts +84 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +246 -0
- package/dist/esm/client.d.ts +155 -0
- package/dist/esm/client.d.ts.map +1 -0
- package/dist/esm/client.js +541 -0
- package/dist/esm/errors.d.ts +84 -0
- package/dist/esm/errors.d.ts.map +1 -0
- package/dist/esm/errors.js +220 -0
- package/dist/esm/index.d.ts +35 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/types.d.ts +183 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +5 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.mjs +35 -0
- package/dist/types.d.ts +183 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/package.json +44 -4
- package/src/client.ts +720 -0
- package/src/errors.ts +259 -0
- package/src/index.test.ts +277 -0
- package/src/index.ts +91 -0
- package/src/types.ts +206 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +9 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Moogo JavaScript/TypeScript Client
|
|
4
|
+
* Official client for Moogo object storage API.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.MoogoClient = void 0;
|
|
8
|
+
exports.createClientFromEnv = createClientFromEnv;
|
|
9
|
+
const errors_js_1 = require("./errors.js");
|
|
10
|
+
/** Default timeout: 30 seconds */
|
|
11
|
+
const DEFAULT_TIMEOUT = 30_000;
|
|
12
|
+
/**
|
|
13
|
+
* Moogo client for object storage operations.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { MoogoClient } from '@moogodev/client';
|
|
18
|
+
*
|
|
19
|
+
* const client = new MoogoClient({
|
|
20
|
+
* baseUrl: 'https://api.moogo.dev',
|
|
21
|
+
* projectId: 'your-project-id',
|
|
22
|
+
* accessKeyId: 'your-access-key',
|
|
23
|
+
* secretKey: 'your-secret-key',
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* // Upload a file
|
|
27
|
+
* const response = await client.upload('avatars/user.png', fileBlob, {
|
|
28
|
+
* contentType: 'image/png',
|
|
29
|
+
* cacheControl: 'public, max-age=31536000, immutable',
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* // List objects
|
|
33
|
+
* const { objects, nextCursor } = await client.listObjects({ prefix: 'avatars/' });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
class MoogoClient {
|
|
37
|
+
baseUrl;
|
|
38
|
+
projectId;
|
|
39
|
+
accessKeyId;
|
|
40
|
+
secretKey;
|
|
41
|
+
fetchImpl;
|
|
42
|
+
timeout;
|
|
43
|
+
constructor(config) {
|
|
44
|
+
if (!config.baseUrl)
|
|
45
|
+
throw new Error('baseUrl is required');
|
|
46
|
+
if (!config.projectId)
|
|
47
|
+
throw new Error('projectId is required');
|
|
48
|
+
if (!config.accessKeyId)
|
|
49
|
+
throw new Error('accessKeyId is required');
|
|
50
|
+
if (!config.secretKey)
|
|
51
|
+
throw new Error('secretKey is required');
|
|
52
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, '');
|
|
53
|
+
this.projectId = config.projectId;
|
|
54
|
+
this.accessKeyId = config.accessKeyId;
|
|
55
|
+
this.secretKey = config.secretKey;
|
|
56
|
+
this.fetchImpl = config.fetch ?? fetch;
|
|
57
|
+
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
58
|
+
}
|
|
59
|
+
/** Build the bucket catalog path */
|
|
60
|
+
getCatalogPath() {
|
|
61
|
+
return `${this.baseUrl}/buckets/${this.projectId}`;
|
|
62
|
+
}
|
|
63
|
+
/** Build the credentials path */
|
|
64
|
+
getCredentialsPath() {
|
|
65
|
+
return `${this.baseUrl}/storage-credentials/${this.projectId}`;
|
|
66
|
+
}
|
|
67
|
+
/** Create authorization header using AWS Signature V4-like scheme */
|
|
68
|
+
async createAuthHeader(method, path, headers, body) {
|
|
69
|
+
const timestamp = new Date().toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
|
|
70
|
+
const date = timestamp.slice(0, 8);
|
|
71
|
+
const signedHeaders = Object.keys(headers)
|
|
72
|
+
.map(k => k.toLowerCase())
|
|
73
|
+
.sort()
|
|
74
|
+
.join(';');
|
|
75
|
+
const canonicalHeaders = Object.keys(headers)
|
|
76
|
+
.map(k => `${k.toLowerCase()}:${headers[k]}`)
|
|
77
|
+
.sort()
|
|
78
|
+
.join('\n') + '\n';
|
|
79
|
+
let bodyHash;
|
|
80
|
+
if (body instanceof Uint8Array) {
|
|
81
|
+
bodyHash = await this.hashBody(body);
|
|
82
|
+
}
|
|
83
|
+
else if (typeof body === 'string') {
|
|
84
|
+
bodyHash = await this.hashBody(new TextEncoder().encode(body));
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
bodyHash = await this.hashBody(new Uint8Array(0));
|
|
88
|
+
}
|
|
89
|
+
const canonicalRequest = [
|
|
90
|
+
method,
|
|
91
|
+
path,
|
|
92
|
+
'',
|
|
93
|
+
canonicalHeaders,
|
|
94
|
+
signedHeaders,
|
|
95
|
+
bodyHash,
|
|
96
|
+
].join('\n');
|
|
97
|
+
const stringToSign = [
|
|
98
|
+
'MOOGO-HMAC-SHA256',
|
|
99
|
+
timestamp,
|
|
100
|
+
`${date}/moogo/storage/moogo_request`,
|
|
101
|
+
await this.hashString(canonicalRequest),
|
|
102
|
+
].join('\n');
|
|
103
|
+
const signingKey = await this.getSigningKey(date);
|
|
104
|
+
const signature = await this.hmacSha256(signingKey, stringToSign);
|
|
105
|
+
return `MOOGO-HMAC-SHA256 Credential=${this.accessKeyId}/${date}/moogo/storage/moogo_request, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
106
|
+
}
|
|
107
|
+
async hashBody(body) {
|
|
108
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', body);
|
|
109
|
+
return Array.from(new Uint8Array(hashBuffer))
|
|
110
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
111
|
+
.join('');
|
|
112
|
+
}
|
|
113
|
+
async hashString(str) {
|
|
114
|
+
return this.hashBody(new TextEncoder().encode(str));
|
|
115
|
+
}
|
|
116
|
+
async getSigningKey(date) {
|
|
117
|
+
const encoder = new TextEncoder();
|
|
118
|
+
const secretKey = await crypto.subtle.importKey('raw', encoder.encode(this.secretKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
119
|
+
const kDate = await crypto.subtle.sign('HMAC', secretKey, encoder.encode(date));
|
|
120
|
+
const kRegion = await crypto.subtle.sign('HMAC', await crypto.subtle.importKey('raw', kDate, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']), encoder.encode('moogo'));
|
|
121
|
+
const kService = await crypto.subtle.sign('HMAC', await crypto.subtle.importKey('raw', kRegion, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']), encoder.encode('storage'));
|
|
122
|
+
const kSigning = await crypto.subtle.sign('HMAC', await crypto.subtle.importKey('raw', kService, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']), encoder.encode('moogo_request'));
|
|
123
|
+
return crypto.subtle.importKey('raw', kSigning, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
124
|
+
}
|
|
125
|
+
async hmacSha256(key, data) {
|
|
126
|
+
const signature = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(data));
|
|
127
|
+
return Array.from(new Uint8Array(signature))
|
|
128
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
129
|
+
.join('');
|
|
130
|
+
}
|
|
131
|
+
/** Execute a request with auth and error handling */
|
|
132
|
+
async request(method, path, options = {}) {
|
|
133
|
+
const url = new URL(path, this.baseUrl);
|
|
134
|
+
if (options.query) {
|
|
135
|
+
Object.entries(options.query).forEach(([k, v]) => {
|
|
136
|
+
if (v !== undefined && v !== null)
|
|
137
|
+
url.searchParams.set(k, v);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const requestHeaders = {
|
|
141
|
+
'X-Moogo-Access-Key-Id': this.accessKeyId,
|
|
142
|
+
'Content-Type': 'application/json',
|
|
143
|
+
...options.headers,
|
|
144
|
+
};
|
|
145
|
+
// Remove Content-Type for FormData (browser sets it with boundary)
|
|
146
|
+
if (options.body instanceof FormData) {
|
|
147
|
+
delete requestHeaders['Content-Type'];
|
|
148
|
+
}
|
|
149
|
+
const authHeader = await this.createAuthHeader(method, url.pathname + url.search, requestHeaders);
|
|
150
|
+
requestHeaders['Authorization'] = `Bearer ${authHeader}`;
|
|
151
|
+
const controller = new AbortController();
|
|
152
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
153
|
+
const signal = options.signal
|
|
154
|
+
? AbortSignal.any([options.signal, controller.signal])
|
|
155
|
+
: controller.signal;
|
|
156
|
+
try {
|
|
157
|
+
const response = await this.fetchImpl(url.toString(), {
|
|
158
|
+
method,
|
|
159
|
+
headers: requestHeaders,
|
|
160
|
+
body: options.body,
|
|
161
|
+
signal,
|
|
162
|
+
});
|
|
163
|
+
clearTimeout(timeoutId);
|
|
164
|
+
const contentType = response.headers.get('Content-Type') || '';
|
|
165
|
+
const isJson = contentType.includes('application/json');
|
|
166
|
+
let data;
|
|
167
|
+
if (isJson) {
|
|
168
|
+
data = await response.json();
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
data = await response.text();
|
|
172
|
+
}
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
throw (0, errors_js_1.createErrorFromResponse)(response, data);
|
|
175
|
+
}
|
|
176
|
+
return data;
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
clearTimeout(timeoutId);
|
|
180
|
+
if (error instanceof errors_js_1.MoogoError)
|
|
181
|
+
throw error;
|
|
182
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
183
|
+
throw new errors_js_1.MoogoError('timeout', 'Request timed out', 408);
|
|
184
|
+
}
|
|
185
|
+
throw new errors_js_1.MoogoError('network_error', `Network error: ${error}`, 0);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// ==================== Bucket Operations ====================
|
|
189
|
+
/** List all buckets in the project */
|
|
190
|
+
async listBuckets() {
|
|
191
|
+
const response = await this.request('GET', this.getCatalogPath());
|
|
192
|
+
return response.buckets;
|
|
193
|
+
}
|
|
194
|
+
/** Get bucket info including usage stats */
|
|
195
|
+
async getBucketInfo(bucketId) {
|
|
196
|
+
const response = await this.request('GET', `${this.getCatalogPath()}/${bucketId}`);
|
|
197
|
+
return response;
|
|
198
|
+
}
|
|
199
|
+
/** Create a new bucket */
|
|
200
|
+
async createBucket(request) {
|
|
201
|
+
const response = await this.request('POST', this.getCatalogPath(), { body: JSON.stringify(request) });
|
|
202
|
+
return response.bucket;
|
|
203
|
+
}
|
|
204
|
+
/** Update bucket settings */
|
|
205
|
+
async updateBucket(bucketId, request) {
|
|
206
|
+
const response = await this.request('PATCH', `${this.getCatalogPath()}/${bucketId}`, { body: JSON.stringify(request) });
|
|
207
|
+
return response.bucket;
|
|
208
|
+
}
|
|
209
|
+
/** Delete a bucket and all its objects */
|
|
210
|
+
async deleteBucket(bucketId) {
|
|
211
|
+
await this.request('DELETE', `${this.getCatalogPath()}/${bucketId}`);
|
|
212
|
+
}
|
|
213
|
+
/** Toggle bucket public/private (fans out to all objects) */
|
|
214
|
+
async setBucketPublic(bucketId, isPublic) {
|
|
215
|
+
const response = await this.request('POST', `${this.getCatalogPath()}/${bucketId}/public`, { body: JSON.stringify({ is_public: isPublic }) });
|
|
216
|
+
return response.bucket;
|
|
217
|
+
}
|
|
218
|
+
// ==================== Object Operations ====================
|
|
219
|
+
/**
|
|
220
|
+
* Upload an object to a bucket.
|
|
221
|
+
*
|
|
222
|
+
* @param key - Object key (path within bucket)
|
|
223
|
+
* @param data - File data (Blob, File, Uint8Array, ReadableStream, or string)
|
|
224
|
+
* @param options - Upload options
|
|
225
|
+
*/
|
|
226
|
+
async upload(key, data, options = {}) {
|
|
227
|
+
const { bucket = 'default', contentType, cacheControl, onProgress, signal } = options;
|
|
228
|
+
const path = `/p/${this.projectId}/${encodeURIComponent(key)}`;
|
|
229
|
+
const query = {};
|
|
230
|
+
if (bucket !== 'default')
|
|
231
|
+
query.bucket = bucket;
|
|
232
|
+
const requestHeaders = {};
|
|
233
|
+
if (contentType)
|
|
234
|
+
requestHeaders['Content-Type'] = contentType;
|
|
235
|
+
if (cacheControl)
|
|
236
|
+
requestHeaders['Cache-Control'] = cacheControl;
|
|
237
|
+
let body;
|
|
238
|
+
let contentLength;
|
|
239
|
+
if (data instanceof Blob || data instanceof File) {
|
|
240
|
+
body = data;
|
|
241
|
+
contentLength = data.size;
|
|
242
|
+
}
|
|
243
|
+
else if (data instanceof Uint8Array) {
|
|
244
|
+
body = new Blob([data]);
|
|
245
|
+
contentLength = data.length;
|
|
246
|
+
}
|
|
247
|
+
else if (typeof data === 'string') {
|
|
248
|
+
body = data;
|
|
249
|
+
contentLength = new TextEncoder().encode(data).length;
|
|
250
|
+
}
|
|
251
|
+
else if (data instanceof ReadableStream) {
|
|
252
|
+
// For streams, we can't know content-length upfront
|
|
253
|
+
body = data;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
throw new Error('Unsupported data type for upload');
|
|
257
|
+
}
|
|
258
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
259
|
+
Object.entries(query).forEach(([k, v]) => url.searchParams.set(k, v));
|
|
260
|
+
const authHeaders = {
|
|
261
|
+
'X-Moogo-Access-Key-Id': this.accessKeyId,
|
|
262
|
+
...requestHeaders,
|
|
263
|
+
};
|
|
264
|
+
const authHeader = await this.createAuthHeader('POST', url.pathname + url.search, authHeaders, body);
|
|
265
|
+
authHeaders['Authorization'] = `Bearer ${authHeader}`;
|
|
266
|
+
// Handle progress if callback provided and we have a known size
|
|
267
|
+
let finalBody = body;
|
|
268
|
+
if (onProgress && contentLength !== undefined && body instanceof Blob) {
|
|
269
|
+
finalBody = this.wrapWithProgress(body, contentLength, onProgress);
|
|
270
|
+
}
|
|
271
|
+
const controller = new AbortController();
|
|
272
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
273
|
+
const reqSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
274
|
+
try {
|
|
275
|
+
const response = await this.fetchImpl(url.toString(), {
|
|
276
|
+
method: 'POST',
|
|
277
|
+
headers: authHeaders,
|
|
278
|
+
body: finalBody,
|
|
279
|
+
signal: reqSignal,
|
|
280
|
+
});
|
|
281
|
+
clearTimeout(timeoutId);
|
|
282
|
+
const responseData = await response.json();
|
|
283
|
+
if (!response.ok) {
|
|
284
|
+
throw (0, errors_js_1.createErrorFromResponse)(response, responseData);
|
|
285
|
+
}
|
|
286
|
+
return responseData;
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
clearTimeout(timeoutId);
|
|
290
|
+
if (error instanceof errors_js_1.MoogoError)
|
|
291
|
+
throw error;
|
|
292
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
293
|
+
throw new errors_js_1.MoogoError('timeout', 'Upload timed out', 408);
|
|
294
|
+
}
|
|
295
|
+
throw new errors_js_1.MoogoError('network_error', `Upload failed: ${error}`, 0);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/** Wrap a Blob with progress tracking */
|
|
299
|
+
wrapWithProgress(blob, total, onProgress) {
|
|
300
|
+
let loaded = 0;
|
|
301
|
+
const reader = blob.stream().getReader();
|
|
302
|
+
return new ReadableStream({
|
|
303
|
+
async pull(controller) {
|
|
304
|
+
const { done, value } = await reader.read();
|
|
305
|
+
if (done) {
|
|
306
|
+
controller.close();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
loaded += value.byteLength;
|
|
310
|
+
onProgress(loaded, total);
|
|
311
|
+
controller.enqueue(value);
|
|
312
|
+
},
|
|
313
|
+
cancel() {
|
|
314
|
+
reader.cancel();
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
/** Download an object */
|
|
319
|
+
async download(key, options = {}) {
|
|
320
|
+
const { bucket = 'default', signal, range } = options;
|
|
321
|
+
const path = `/p/${this.projectId}/${encodeURIComponent(key)}`;
|
|
322
|
+
const query = {};
|
|
323
|
+
if (bucket !== 'default')
|
|
324
|
+
query.bucket = bucket;
|
|
325
|
+
const requestHeaders = {
|
|
326
|
+
'X-Moogo-Access-Key-Id': this.accessKeyId,
|
|
327
|
+
};
|
|
328
|
+
if (range) {
|
|
329
|
+
const end = range.end !== undefined ? range.end : '';
|
|
330
|
+
requestHeaders['Range'] = `bytes=${range.start}-${end}`;
|
|
331
|
+
}
|
|
332
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
333
|
+
Object.entries(query).forEach(([k, v]) => url.searchParams.set(k, v));
|
|
334
|
+
const authHeader = await this.createAuthHeader('GET', url.pathname + url.search, requestHeaders);
|
|
335
|
+
requestHeaders['Authorization'] = `Bearer ${authHeader}`;
|
|
336
|
+
const controller = new AbortController();
|
|
337
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
338
|
+
const reqSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
339
|
+
try {
|
|
340
|
+
const response = await this.fetchImpl(url.toString(), {
|
|
341
|
+
method: 'GET',
|
|
342
|
+
headers: requestHeaders,
|
|
343
|
+
signal: reqSignal,
|
|
344
|
+
});
|
|
345
|
+
clearTimeout(timeoutId);
|
|
346
|
+
if (!response.ok) {
|
|
347
|
+
const data = await response.json().catch(() => ({}));
|
|
348
|
+
throw (0, errors_js_1.createErrorFromResponse)(response, data);
|
|
349
|
+
}
|
|
350
|
+
return response;
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
clearTimeout(timeoutId);
|
|
354
|
+
if (error instanceof errors_js_1.MoogoError)
|
|
355
|
+
throw error;
|
|
356
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
357
|
+
throw new errors_js_1.MoogoError('timeout', 'Download timed out', 408);
|
|
358
|
+
}
|
|
359
|
+
throw new errors_js_1.MoogoError('network_error', `Download failed: ${error}`, 0);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/** Download object as blob */
|
|
363
|
+
async downloadBlob(key, options) {
|
|
364
|
+
const response = await this.download(key, options);
|
|
365
|
+
return response.blob();
|
|
366
|
+
}
|
|
367
|
+
/** Download object as text */
|
|
368
|
+
async downloadText(key, options) {
|
|
369
|
+
const response = await this.download(key, options);
|
|
370
|
+
return response.text();
|
|
371
|
+
}
|
|
372
|
+
/** Download object as ArrayBuffer */
|
|
373
|
+
async downloadArrayBuffer(key, options) {
|
|
374
|
+
const response = await this.download(key, options);
|
|
375
|
+
return response.arrayBuffer();
|
|
376
|
+
}
|
|
377
|
+
/** Get object metadata (HEAD request) */
|
|
378
|
+
async headObject(key, options = {}) {
|
|
379
|
+
const { bucket = 'default', signal } = options;
|
|
380
|
+
const path = `/p/${this.projectId}/${encodeURIComponent(key)}`;
|
|
381
|
+
const query = {};
|
|
382
|
+
if (bucket !== 'default')
|
|
383
|
+
query.bucket = bucket;
|
|
384
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
385
|
+
Object.entries(query).forEach(([k, v]) => url.searchParams.set(k, v));
|
|
386
|
+
const requestHeaders = {
|
|
387
|
+
'X-Moogo-Access-Key-Id': this.accessKeyId,
|
|
388
|
+
};
|
|
389
|
+
const authHeader = await this.createAuthHeader('HEAD', url.pathname + url.search, requestHeaders);
|
|
390
|
+
requestHeaders['Authorization'] = `Bearer ${authHeader}`;
|
|
391
|
+
const controller = new AbortController();
|
|
392
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
393
|
+
const reqSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
394
|
+
try {
|
|
395
|
+
const response = await this.fetchImpl(url.toString(), {
|
|
396
|
+
method: 'HEAD',
|
|
397
|
+
headers: requestHeaders,
|
|
398
|
+
signal: reqSignal,
|
|
399
|
+
});
|
|
400
|
+
clearTimeout(timeoutId);
|
|
401
|
+
if (response.status === 404) {
|
|
402
|
+
return { headers: response.headers, exists: false };
|
|
403
|
+
}
|
|
404
|
+
if (!response.ok) {
|
|
405
|
+
const data = await response.json().catch(() => ({}));
|
|
406
|
+
throw (0, errors_js_1.createErrorFromResponse)(response, data);
|
|
407
|
+
}
|
|
408
|
+
return { headers: response.headers, exists: true };
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
clearTimeout(timeoutId);
|
|
412
|
+
if (error instanceof errors_js_1.MoogoError)
|
|
413
|
+
throw error;
|
|
414
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
415
|
+
throw new errors_js_1.MoogoError('timeout', 'HEAD request timed out', 408);
|
|
416
|
+
}
|
|
417
|
+
throw new errors_js_1.MoogoError('network_error', `HEAD failed: ${error}`, 0);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/** Delete an object */
|
|
421
|
+
async deleteObject(key, options = {}) {
|
|
422
|
+
const { bucket = 'default', signal } = options;
|
|
423
|
+
const path = `/p/${this.projectId}/${encodeURIComponent(key)}`;
|
|
424
|
+
const query = {};
|
|
425
|
+
if (bucket !== 'default')
|
|
426
|
+
query.bucket = bucket;
|
|
427
|
+
await this.request('DELETE', path, { query, signal });
|
|
428
|
+
}
|
|
429
|
+
/** List objects in a bucket */
|
|
430
|
+
async listObjects(query = {}) {
|
|
431
|
+
const params = {};
|
|
432
|
+
if (query.bucket)
|
|
433
|
+
params.bucket = query.bucket;
|
|
434
|
+
if (query.prefix)
|
|
435
|
+
params.prefix = query.prefix;
|
|
436
|
+
if (query.search)
|
|
437
|
+
params.search = query.search;
|
|
438
|
+
if (query.order_by)
|
|
439
|
+
params.order = query.order_by;
|
|
440
|
+
if (query.order_dir)
|
|
441
|
+
params.dir = query.order_dir;
|
|
442
|
+
if (query.limit)
|
|
443
|
+
params.limit = String(query.limit);
|
|
444
|
+
if (query.cursor)
|
|
445
|
+
params.cursor = query.cursor;
|
|
446
|
+
return this.request('GET', `/p/${this.projectId}/bucket`, { query: params });
|
|
447
|
+
}
|
|
448
|
+
/** Get a presigned URL for an object */
|
|
449
|
+
async getPresignedUrl(key, request, options = {}) {
|
|
450
|
+
const { bucket = 'default', signal } = options;
|
|
451
|
+
const path = `/p/${this.projectId}/${encodeURIComponent(key)}/sign`;
|
|
452
|
+
const query = {};
|
|
453
|
+
if (bucket !== 'default')
|
|
454
|
+
query.bucket = bucket;
|
|
455
|
+
return this.request('POST', path, {
|
|
456
|
+
query,
|
|
457
|
+
body: JSON.stringify(request),
|
|
458
|
+
signal,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
/** Create a signed upload URL (returns upload URL + object metadata) */
|
|
462
|
+
async createSignedUpload(key, request, options = {}) {
|
|
463
|
+
const { bucket = 'default', signal } = options;
|
|
464
|
+
const path = `/p/${this.projectId}/${encodeURIComponent(key)}/sign-upload`;
|
|
465
|
+
const query = {};
|
|
466
|
+
if (bucket !== 'default')
|
|
467
|
+
query.bucket = bucket;
|
|
468
|
+
return this.request('POST', path, {
|
|
469
|
+
query,
|
|
470
|
+
body: JSON.stringify(request),
|
|
471
|
+
signal,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
/** Update object metadata (visibility, key) */
|
|
475
|
+
async updateObject(key, updates, options = {}) {
|
|
476
|
+
const { bucket = 'default', signal } = options;
|
|
477
|
+
const path = `/p/${this.projectId}/${encodeURIComponent(key)}`;
|
|
478
|
+
const query = {};
|
|
479
|
+
if (bucket !== 'default')
|
|
480
|
+
query.bucket = bucket;
|
|
481
|
+
const response = await this.request('PATCH', path, { query, body: JSON.stringify(updates), signal });
|
|
482
|
+
return response.object;
|
|
483
|
+
}
|
|
484
|
+
// ==================== Credential Operations ====================
|
|
485
|
+
/** List storage credentials */
|
|
486
|
+
async listCredentials() {
|
|
487
|
+
const response = await this.request('GET', this.getCredentialsPath());
|
|
488
|
+
return response.credentials;
|
|
489
|
+
}
|
|
490
|
+
/** Create a new storage credential */
|
|
491
|
+
async createCredential(request) {
|
|
492
|
+
return this.request('POST', this.getCredentialsPath(), { body: JSON.stringify(request) });
|
|
493
|
+
}
|
|
494
|
+
/** Rotate a credential (generates new secret) */
|
|
495
|
+
async rotateCredential(credentialId) {
|
|
496
|
+
return this.request('POST', `${this.getCredentialsPath()}/${credentialId}/rotate`);
|
|
497
|
+
}
|
|
498
|
+
/** Revoke a credential */
|
|
499
|
+
async revokeCredential(credentialId) {
|
|
500
|
+
await this.request('DELETE', `${this.getCredentialsPath()}/${credentialId}`);
|
|
501
|
+
}
|
|
502
|
+
// ==================== Utility Methods ====================
|
|
503
|
+
/** Get the public URL for an object (if published) */
|
|
504
|
+
getPublicUrl(key, publicUrlBase) {
|
|
505
|
+
if (publicUrlBase) {
|
|
506
|
+
const normalizedBase = publicUrlBase.replace(/\/+$/, '');
|
|
507
|
+
return `${normalizedBase}/pub/${this.projectId}/${encodeURIComponent(key)}`;
|
|
508
|
+
}
|
|
509
|
+
return `${this.baseUrl}/pub/${this.projectId}/${encodeURIComponent(key)}`;
|
|
510
|
+
}
|
|
511
|
+
/** Get the authenticated download URL for an object */
|
|
512
|
+
getAuthenticatedUrl(key) {
|
|
513
|
+
return `${this.baseUrl}/p/${this.projectId}/${encodeURIComponent(key)}`;
|
|
514
|
+
}
|
|
515
|
+
/** Check if an object exists (efficient HEAD) */
|
|
516
|
+
async objectExists(key, options) {
|
|
517
|
+
const result = await this.headObject(key, options);
|
|
518
|
+
return result.exists;
|
|
519
|
+
}
|
|
520
|
+
/** Get object size without downloading */
|
|
521
|
+
async getObjectSize(key, options) {
|
|
522
|
+
const result = await this.headObject(key, options);
|
|
523
|
+
if (!result.exists)
|
|
524
|
+
return null;
|
|
525
|
+
const contentLength = result.headers.get('Content-Length');
|
|
526
|
+
return contentLength ? parseInt(contentLength, 10) : null;
|
|
527
|
+
}
|
|
528
|
+
/** Get object ETag */
|
|
529
|
+
async getObjectETag(key, options) {
|
|
530
|
+
const result = await this.headObject(key, options);
|
|
531
|
+
if (!result.exists)
|
|
532
|
+
return null;
|
|
533
|
+
return result.headers.get('ETag');
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
exports.MoogoClient = MoogoClient;
|
|
537
|
+
/** Convenience function to create a client from environment variables */
|
|
538
|
+
function createClientFromEnv(env) {
|
|
539
|
+
const getEnv = (key) => env?.[key] ?? process.env[key];
|
|
540
|
+
return new MoogoClient({
|
|
541
|
+
baseUrl: getEnv('MOOGO_API_URL') || 'https://api.moogo.dev',
|
|
542
|
+
projectId: getEnv('MOOGO_PROJECT_ID'),
|
|
543
|
+
accessKeyId: getEnv('MOOGO_BUCKET_ACCESS_KEY_ID'),
|
|
544
|
+
secretKey: getEnv('MOOGO_BUCKET_SECRET_KEY'),
|
|
545
|
+
});
|
|
546
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error classes for Moogo client.
|
|
3
|
+
* All errors follow the API error codes.
|
|
4
|
+
*/
|
|
5
|
+
export declare class MoogoError extends Error {
|
|
6
|
+
readonly code: string;
|
|
7
|
+
readonly status: number;
|
|
8
|
+
readonly detail?: string;
|
|
9
|
+
constructor(code: string, message: string, status: number, detail?: string);
|
|
10
|
+
static fromResponse(response: Response, data: any): MoogoError;
|
|
11
|
+
}
|
|
12
|
+
/** 400 - Missing or invalid object key */
|
|
13
|
+
export declare class MissingKeyError extends MoogoError {
|
|
14
|
+
constructor(detail?: string);
|
|
15
|
+
}
|
|
16
|
+
export declare class InvalidKeyError extends MoogoError {
|
|
17
|
+
constructor(detail?: string);
|
|
18
|
+
}
|
|
19
|
+
export declare class KeyTooLongError extends MoogoError {
|
|
20
|
+
constructor(detail?: string);
|
|
21
|
+
}
|
|
22
|
+
export declare class KeyTakenError extends MoogoError {
|
|
23
|
+
constructor(detail?: string);
|
|
24
|
+
}
|
|
25
|
+
export declare class InvalidBucketError extends MoogoError {
|
|
26
|
+
constructor(detail?: string);
|
|
27
|
+
}
|
|
28
|
+
export declare class InvalidCursorError extends MoogoError {
|
|
29
|
+
constructor(detail?: string);
|
|
30
|
+
}
|
|
31
|
+
export declare class InvalidOrderError extends MoogoError {
|
|
32
|
+
constructor(detail?: string);
|
|
33
|
+
}
|
|
34
|
+
export declare class BodyTooLargeError extends MoogoError {
|
|
35
|
+
constructor(detail?: string);
|
|
36
|
+
}
|
|
37
|
+
export declare class ObjectTooLargeError extends MoogoError {
|
|
38
|
+
constructor(detail?: string);
|
|
39
|
+
}
|
|
40
|
+
export declare class UnsupportedMediaTypeError extends MoogoError {
|
|
41
|
+
constructor(detail?: string);
|
|
42
|
+
}
|
|
43
|
+
export declare class QuotaExceededError extends MoogoError {
|
|
44
|
+
constructor(detail?: string);
|
|
45
|
+
}
|
|
46
|
+
export declare class ProjectPausedError extends MoogoError {
|
|
47
|
+
constructor(detail?: string);
|
|
48
|
+
}
|
|
49
|
+
export declare class LinkExpiredError extends MoogoError {
|
|
50
|
+
constructor(detail?: string);
|
|
51
|
+
}
|
|
52
|
+
export declare class NotFoundError extends MoogoError {
|
|
53
|
+
constructor(detail?: string);
|
|
54
|
+
}
|
|
55
|
+
export declare class BucketNotFoundError extends MoogoError {
|
|
56
|
+
constructor(detail?: string);
|
|
57
|
+
}
|
|
58
|
+
export declare class CredentialLimitError extends MoogoError {
|
|
59
|
+
constructor(detail?: string);
|
|
60
|
+
}
|
|
61
|
+
export declare class RateLimitedError extends MoogoError {
|
|
62
|
+
readonly retryAfter: number;
|
|
63
|
+
readonly resetAt: number | null;
|
|
64
|
+
constructor(retryAfter: number, resetAt: number | null, detail?: string);
|
|
65
|
+
static fromResponse(response: Response, data: any): RateLimitedError;
|
|
66
|
+
}
|
|
67
|
+
export declare class InvalidBodyError extends MoogoError {
|
|
68
|
+
constructor(detail?: string);
|
|
69
|
+
}
|
|
70
|
+
export declare class InvalidExpiresInError extends MoogoError {
|
|
71
|
+
constructor(detail?: string);
|
|
72
|
+
}
|
|
73
|
+
export declare class InvalidMethodError extends MoogoError {
|
|
74
|
+
constructor(detail?: string);
|
|
75
|
+
}
|
|
76
|
+
export declare class UnauthorizedError extends MoogoError {
|
|
77
|
+
constructor(detail?: string);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Create a typed error from an API response.
|
|
81
|
+
* Falls back to generic MoogoError for unknown codes.
|
|
82
|
+
*/
|
|
83
|
+
export declare function createErrorFromResponse(response: Response, data: any): MoogoError;
|
|
84
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,qBAAa,UAAW,SAAQ,KAAK;IACnC,SAAgB,IAAI,EAAE,MAAM,CAAC;IAC7B,SAAgB,MAAM,EAAE,MAAM,CAAC;IAC/B,SAAgB,MAAM,CAAC,EAAE,MAAM,CAAC;gBAEpB,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;IAS1E,MAAM,CAAC,YAAY,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,GAAG,UAAU;CAgB/D;AAED,0CAA0C;AAC1C,qBAAa,eAAgB,SAAQ,UAAU;gBACjC,MAAM,CAAC,EAAE,MAAM;CAK5B;AAED,qBAAa,eAAgB,SAAQ,UAAU;gBACjC,MAAM,CAAC,EAAE,MAAM;CAK5B;AAED,qBAAa,eAAgB,SAAQ,UAAU;gBACjC,MAAM,CAAC,EAAE,MAAM;CAK5B;AAED,qBAAa,aAAc,SAAQ,UAAU;gBAC/B,MAAM,CAAC,EAAE,MAAM;CAK5B;AAED,qBAAa,kBAAmB,SAAQ,UAAU;gBACpC,MAAM,CAAC,EAAE,MAAM;CAK5B;AAED,qBAAa,kBAAmB,SAAQ,UAAU;gBACpC,MAAM,CAAC,EAAE,MAAM;CAK5B;AAED,qBAAa,iBAAkB,SAAQ,UAAU;gBACnC,MAAM,CAAC,EAAE,MAAM;CAK5B;AAED,qBAAa,iBAAkB,SAAQ,UAAU;gBACnC,MAAM,CAAC,EAAE,MAAM;CAK5B;AAED,qBAAa,mBAAoB,SAAQ,UAAU;gBACrC,MAAM,CAAC,EAAE,MAAM;CAK5B;AAED,qBAAa,yBAA0B,SAAQ,UAAU;gBAC3C,MAAM,CAAC,EAAE,MAAM;CAK5B;AAED,qBAAa,kBAAmB,SAAQ,UAAU;gBACpC,MAAM,CAAC,EAAE,MAAM;CAK5B;AAED,qBAAa,kBAAmB,SAAQ,UAAU;gBACpC,MAAM,CAAC,EAAE,MAAM;CAK5B;AAED,qBAAa,gBAAiB,SAAQ,UAAU;gBAClC,MAAM,CAAC,EAAE,MAAM;CAK5B;AAED,qBAAa,aAAc,SAAQ,UAAU;gBAC/B,MAAM,CAAC,EAAE,MAAM;CAK5B;AAED,qBAAa,mBAAoB,SAAQ,UAAU;gBACrC,MAAM,CAAC,EAAE,MAAM;CAK5B;AAED,qBAAa,oBAAqB,SAAQ,UAAU;gBACtC,MAAM,CAAC,EAAE,MAAM;CAK5B;AAED,qBAAa,gBAAiB,SAAQ,UAAU;IAC9C,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,SAAgB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;gBAE3B,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,CAAC,EAAE,MAAM;IAQvE,MAAM,CAAC,YAAY,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,GAAG,gBAAgB;CAOrE;AAED,qBAAa,gBAAiB,SAAQ,UAAU;gBAClC,MAAM,CAAC,EAAE,MAAM;CAK5B;AAED,qBAAa,qBAAsB,SAAQ,UAAU;gBACvC,MAAM,CAAC,EAAE,MAAM;CAK5B;AAED,qBAAa,kBAAmB,SAAQ,UAAU;gBACpC,MAAM,CAAC,EAAE,MAAM;CAK5B;AAED,qBAAa,iBAAkB,SAAQ,UAAU;gBACnC,MAAM,CAAC,EAAE,MAAM;CAK5B;AA0BD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,GAAG,UAAU,CAWjF"}
|