@fileway/supabase 0.0.0-stage → 0.5.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Azubuike Golden
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,79 @@
1
- # Temporary Holding Version
1
+ # @fileway/supabase
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/@fileway/supabase?style=flat-square)](https://www.npmjs.com/package/@fileway/supabase)
4
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/@fileway/supabase?style=flat-square)](https://bundlephobia.com/package/@fileway/supabase)
5
+
6
+ **Supabase Storage driver for [Fileway](https://github.com/Goldenhub/fileway).**
7
+
8
+ Implemented with `fetch` against the Storage REST and TUS APIs: no `supabase-js` required, so it runs on Node.js, Bun, Deno, and edge runtimes.
9
+
10
+ ```bash
11
+ npm install @fileway/supabase
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```ts
17
+ import { FilewayClient } from "@fileway/core";
18
+ import { SupabaseDriver } from "@fileway/supabase";
19
+
20
+ // Server side, with a secret (service role) key: full access, bypasses RLS.
21
+ const driver = new SupabaseDriver({
22
+ url: "https://<project-ref>.supabase.co",
23
+ key: process.env.SUPABASE_SECRET_KEY,
24
+ bucket: "avatars",
25
+ });
26
+
27
+ const client = new FilewayClient({ driver });
28
+
29
+ const result = await client.upload(stream, {
30
+ filename: "photo.jpg",
31
+ metadata: { userId: "42" }, // stored as the object's user metadata
32
+ });
33
+ ```
34
+
35
+ To act as a signed-in user so the bucket's RLS policies apply, pass the anon or publishable key plus the user's access token:
36
+
37
+ ```ts
38
+ new SupabaseDriver({
39
+ url: "https://<project-ref>.supabase.co",
40
+ key: process.env.SUPABASE_PUBLISHABLE_KEY,
41
+ accessToken: session.access_token,
42
+ bucket: "avatars",
43
+ });
44
+ ```
45
+
46
+ ## Config
47
+
48
+ | Option | Type | Description |
49
+ | -------------- | -------- | ----------- |
50
+ | `url` | `string` | Project URL (required). `https://<ref>.storage.supabase.co` is faster for large uploads |
51
+ | `key` | `string` | API key (required): secret / service role, or anon / publishable with `accessToken` |
52
+ | `accessToken` | `string` | A user's JWT, so RLS policies apply |
53
+ | `bucket` | `string` | Bucket name (required) |
54
+ | `baseUrl` | `string` | Public base URL for `getUrl` (default: the bucket's public object URL) |
55
+ | `cacheControl` | `number` | `Cache-Control` max-age in seconds (default `3600`) |
56
+ | `maxSizeBytes` | `number` | Reject larger uploads with a `size-exceeded` error |
57
+
58
+ ## Upload strategy
59
+
60
+ Uploads always overwrite (`x-upsert`) and memory stays bounded whatever the file size:
61
+
62
+ - **Up to 6 MiB**: one standard upload request.
63
+ - **Known size above 6 MiB**: a resumable TUS upload in 6 MiB chunks (the size Supabase requires). A failed or cancelled upload is terminated.
64
+ - **Unknown size above 6 MiB**: the stream is sent as the body of one standard upload, without buffering it. Supabase limits standard uploads to 5 GB, so pass `size` for larger files.
65
+
66
+ If the stream ends before or after a declared `size`, the upload throws a validation error and nothing is stored.
67
+
68
+ ## URLs
69
+
70
+ `getUrl()` returns the public object URL, which only works for public buckets. For private buckets use `getPresignedUrl()` (a Supabase signed URL) or `get()`.
71
+
72
+ ## Upload status
73
+
74
+ `pathFor` and `head` are implemented, so `client.getUploadStatus()` reports `complete` or `missing`, and lost upload responses are recovered without uploading twice.
75
+
76
+ ## Resources
77
+
78
+ - [Documentation](https://www.usefileway.dev/docs/drivers/supabase)
79
+ - [License: MIT](https://github.com/Goldenhub/fileway/blob/main/LICENSE)
package/dist/index.cjs ADDED
@@ -0,0 +1,394 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ SupabaseDriver: () => SupabaseDriver
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/errors.ts
28
+ function classifySupabaseError(status, body) {
29
+ let parsed = {};
30
+ try {
31
+ parsed = JSON.parse(body);
32
+ } catch {
33
+ }
34
+ const text = `${parsed.error ?? ""} ${parsed.message ?? ""} ${parsed.error === void 0 ? body : ""}`.toLowerCase();
35
+ const code = Number(parsed.statusCode ?? status);
36
+ if (text.includes("bucket not found")) return "bucket-not-found";
37
+ if (text.includes("not found") || text.includes("not_found") || text.includes("no such")) return "not-found";
38
+ if (text.includes("exceeded the maximum allowed size") || text.includes("payload too large") || code === 413) {
39
+ return "size-exceeded";
40
+ }
41
+ if (code === 401 || code === 403 || text.includes("jwt") || text.includes("unauthorized") || text.includes("row-level security")) {
42
+ return "auth-failed";
43
+ }
44
+ if (code === 429 || text.includes("too many requests")) return "rate-limited";
45
+ if (code === 404) return "not-found";
46
+ return "provider-error";
47
+ }
48
+
49
+ // src/index.ts
50
+ var import_core = require("@fileway/core");
51
+ var randomUUID = () => globalThis.crypto.randomUUID();
52
+ var PROVIDER = "supabase";
53
+ var TUS_CHUNK_SIZE = 6 * 1024 * 1024;
54
+ var TUS_VERSION = "1.0.0";
55
+ var PRESIGN_DEFAULT_EXPIRY = 3600;
56
+ var TOKEN_KEY = "fileway-upload-token";
57
+ var MAX_CHUNK_RESENDS = 3;
58
+ var SupabaseDriver = class {
59
+ name = "supabase";
60
+ api;
61
+ bucket;
62
+ baseUrl;
63
+ headers;
64
+ maxSizeBytes;
65
+ cacheControl;
66
+ constructor(config) {
67
+ if (!config.url || !config.key || !config.bucket) {
68
+ throw new import_core.StorageError("config", "SupabaseDriver requires url, key and bucket", { provider: PROVIDER });
69
+ }
70
+ this.api = `${config.url.replace(/\/+$/, "")}/storage/v1`;
71
+ this.bucket = config.bucket;
72
+ this.baseUrl = (config.baseUrl ?? `${this.api}/object/public/${config.bucket}`).replace(/\/+$/, "");
73
+ const bearer = config.accessToken ?? (config.key.startsWith("eyJ") ? config.key : void 0);
74
+ this.headers = { apikey: config.key, ...bearer ? { authorization: `Bearer ${bearer}` } : {} };
75
+ this.maxSizeBytes = config.maxSizeBytes;
76
+ this.cacheControl = `max-age=${config.cacheControl ?? 3600}`;
77
+ }
78
+ async upload(stream, options, context) {
79
+ (0, import_core.validateUploadOptions)(options);
80
+ if (this.maxSizeBytes !== void 0 && options.size !== void 0 && options.size > this.maxSizeBytes) {
81
+ throw new import_core.StorageError("size-exceeded", `file size ${options.size} exceeds maxSizeBytes ${this.maxSizeBytes}`, { provider: PROVIDER });
82
+ }
83
+ const id = options.id ?? randomUUID();
84
+ const path = this.pathFor({ ...options, id });
85
+ const contentType = options.mimeType ?? "application/octet-stream";
86
+ const metadata = { ...options.metadata, ...context?.uploadToken ? { [TOKEN_KEY]: context.uploadToken } : {} };
87
+ const signal = options.signal;
88
+ const reader = stream.getReader();
89
+ let aborted = signal?.aborted ?? false;
90
+ const onAbort = () => {
91
+ aborted = true;
92
+ reader.cancel().catch(() => {
93
+ });
94
+ };
95
+ if (signal && !aborted) signal.addEventListener("abort", onAbort, { once: true });
96
+ let totalBytes = 0;
97
+ const read = async () => {
98
+ const { done, value } = await reader.read();
99
+ if (aborted) throw (0, import_core.abortError)("upload aborted");
100
+ if (done) return void 0;
101
+ totalBytes += value.byteLength;
102
+ if (this.maxSizeBytes !== void 0 && totalBytes > this.maxSizeBytes) {
103
+ throw new import_core.StorageError("size-exceeded", `upload exceeds maxSizeBytes ${this.maxSizeBytes}`, { provider: PROVIDER });
104
+ }
105
+ return value;
106
+ };
107
+ const checkSize = () => {
108
+ if (options.size !== void 0 && totalBytes !== options.size) {
109
+ throw new import_core.ValidationError(`stream delivered ${totalBytes} bytes, expected ${options.size}`);
110
+ }
111
+ };
112
+ let tusUrl;
113
+ try {
114
+ let etag;
115
+ if (options.size !== void 0 && options.size > TUS_CHUNK_SIZE) {
116
+ tusUrl = await this.createTusUpload(path, options.size, contentType, metadata, signal);
117
+ let buffer = [];
118
+ let bufferedLen = 0;
119
+ let offset = 0;
120
+ while (true) {
121
+ const value = await read();
122
+ if (totalBytes > options.size) checkSize();
123
+ if (value) {
124
+ buffer.push(value);
125
+ bufferedLen += value.byteLength;
126
+ }
127
+ while (bufferedLen >= TUS_CHUNK_SIZE || !value && bufferedLen > 0) {
128
+ if (!value) checkSize();
129
+ const merged = concatBytes(buffer, bufferedLen);
130
+ const take = Math.min(TUS_CHUNK_SIZE, bufferedLen);
131
+ offset = await this.patchTus(tusUrl, merged.subarray(0, take), offset, signal);
132
+ const rest = merged.subarray(take);
133
+ buffer = [rest];
134
+ bufferedLen = rest.byteLength;
135
+ }
136
+ if (!value) break;
137
+ }
138
+ checkSize();
139
+ } else {
140
+ const first = [];
141
+ let firstLen = 0;
142
+ let ended = false;
143
+ while (firstLen <= TUS_CHUNK_SIZE) {
144
+ const value = await read();
145
+ if (!value) {
146
+ ended = true;
147
+ break;
148
+ }
149
+ first.push(value);
150
+ firstLen += value.byteLength;
151
+ }
152
+ const head = concatBytes(first, firstLen);
153
+ if (ended) {
154
+ checkSize();
155
+ etag = await this.standardUpload(path, head, contentType, metadata, signal);
156
+ } else {
157
+ checkSize();
158
+ const bodyError = {};
159
+ const body = new ReadableStream({
160
+ start(controller) {
161
+ controller.enqueue(head);
162
+ },
163
+ async pull(controller) {
164
+ try {
165
+ const value = await read();
166
+ if (value) controller.enqueue(value);
167
+ else controller.close();
168
+ } catch (err) {
169
+ bodyError.current = err;
170
+ controller.error(err);
171
+ }
172
+ },
173
+ cancel(reason) {
174
+ return reader.cancel(reason);
175
+ }
176
+ });
177
+ try {
178
+ etag = await this.standardUpload(path, body, contentType, metadata, signal);
179
+ } catch (err) {
180
+ if (bodyError.current) throw bodyError.current;
181
+ throw err;
182
+ }
183
+ checkSize();
184
+ }
185
+ }
186
+ return {
187
+ id,
188
+ url: `${this.baseUrl}/${(0, import_core.urlEncodePath)(path)}`,
189
+ path,
190
+ size: totalBytes,
191
+ meta: { bucket: this.bucket, ...etag ? { etag } : {} }
192
+ };
193
+ } catch (err) {
194
+ reader.cancel().catch(() => {
195
+ });
196
+ if (tusUrl) {
197
+ await fetch(tusUrl, { method: "DELETE", headers: { ...this.headers, "tus-resumable": TUS_VERSION } }).catch(() => {
198
+ });
199
+ }
200
+ throw err;
201
+ } finally {
202
+ signal?.removeEventListener("abort", onAbort);
203
+ }
204
+ }
205
+ /** `POST /object/{bucket}/{path}`, overwriting any existing object. */
206
+ async standardUpload(path, body, contentType, metadata, signal) {
207
+ const res = await fetchWithAbort(this.objectUrl("object", path), {
208
+ method: "POST",
209
+ headers: {
210
+ ...this.headers,
211
+ "content-type": contentType,
212
+ "cache-control": this.cacheControl,
213
+ "x-upsert": "true",
214
+ "x-metadata": base64(JSON.stringify(metadata))
215
+ },
216
+ body,
217
+ // Node's fetch (undici) requires `duplex: "half"` for stream bodies;
218
+ // other runtimes ignore it.
219
+ duplex: "half",
220
+ signal: signal ?? null
221
+ });
222
+ if (!res.ok) throw await supabaseError("Supabase upload failed", res);
223
+ return res.headers.get("etag") ?? void 0;
224
+ }
225
+ /** Creates a TUS upload and returns its URL. */
226
+ async createTusUpload(path, size, contentType, metadata, signal) {
227
+ const endpoint = `${this.api}/upload/resumable`;
228
+ const uploadMetadata = {
229
+ bucketName: this.bucket,
230
+ objectName: path,
231
+ contentType,
232
+ cacheControl: this.cacheControl.replace("max-age=", ""),
233
+ metadata: JSON.stringify(metadata)
234
+ };
235
+ const res = await fetchWithAbort(endpoint, {
236
+ method: "POST",
237
+ headers: {
238
+ ...this.headers,
239
+ "tus-resumable": TUS_VERSION,
240
+ "upload-length": String(size),
241
+ "upload-metadata": Object.entries(uploadMetadata).map(([k, v]) => `${k} ${base64(v)}`).join(","),
242
+ "x-upsert": "true"
243
+ },
244
+ signal: signal ?? null
245
+ });
246
+ if (!res.ok) throw await supabaseError("Supabase resumable upload create failed", res);
247
+ const location = res.headers.get("location");
248
+ if (!location) {
249
+ throw new import_core.StorageError("provider-error", "Supabase resumable upload returned no Location header", { provider: PROVIDER });
250
+ }
251
+ return new URL(location, endpoint).href;
252
+ }
253
+ /** PATCHes `bytes` at `offset`, resending whatever the server did not store.
254
+ * Returns the new offset. */
255
+ async patchTus(url, bytes, offset, signal) {
256
+ const end = offset + bytes.byteLength;
257
+ let current = offset;
258
+ for (let attempt = 0; attempt <= MAX_CHUNK_RESENDS && current < end; attempt++) {
259
+ const res = await fetchWithAbort(url, {
260
+ method: "PATCH",
261
+ headers: {
262
+ ...this.headers,
263
+ "tus-resumable": TUS_VERSION,
264
+ "upload-offset": String(current),
265
+ "content-type": "application/offset+octet-stream"
266
+ },
267
+ body: bytes.subarray(current - offset),
268
+ signal: signal ?? null
269
+ });
270
+ if (!res.ok) throw await supabaseError("Supabase resumable upload chunk failed", res);
271
+ const next = Number(res.headers.get("upload-offset") ?? end);
272
+ if (next < current) break;
273
+ current = next;
274
+ }
275
+ if (current < end) {
276
+ throw new import_core.StorageError("provider-error", "Supabase did not store the uploaded chunk", { provider: PROVIDER });
277
+ }
278
+ return current;
279
+ }
280
+ /** Deletes the object; a missing object is not an error. */
281
+ async delete(path, options) {
282
+ const res = await fetchWithAbort(`${this.api}/object/${encodeURIComponent(this.bucket)}`, {
283
+ method: "DELETE",
284
+ headers: { ...this.headers, "content-type": "application/json" },
285
+ body: JSON.stringify({ prefixes: [path] }),
286
+ signal: options?.signal ?? null
287
+ });
288
+ if (!res.ok) throw await supabaseError("Supabase delete failed", res);
289
+ }
290
+ async getUrl(path, _options) {
291
+ return `${this.baseUrl}/${(0, import_core.urlEncodePath)(path)}`;
292
+ }
293
+ /** Streams the object. `options.signal` also cancels reading the body. */
294
+ async get(path, options) {
295
+ const res = await fetchWithAbort(this.objectUrl("object/authenticated", path), {
296
+ headers: this.headers,
297
+ signal: options?.signal ?? null
298
+ });
299
+ if (!res.ok) throw await supabaseError("Supabase get failed", res);
300
+ if (!res.body) {
301
+ throw new import_core.StorageError("provider-error", "Supabase get returned no response body", { provider: PROVIDER });
302
+ }
303
+ return res.body;
304
+ }
305
+ /** A signed download URL for `path`, valid for `expiresIn` seconds (positive integer, default 3600). */
306
+ async getPresignedUrl(path, options) {
307
+ const expiresIn = options?.expiresIn ?? PRESIGN_DEFAULT_EXPIRY;
308
+ if (!Number.isInteger(expiresIn) || expiresIn < 1) {
309
+ throw new import_core.ValidationError("expiresIn must be a positive integer number of seconds");
310
+ }
311
+ const res = await fetchWithAbort(this.objectUrl("object/sign", path), {
312
+ method: "POST",
313
+ headers: { ...this.headers, "content-type": "application/json" },
314
+ body: JSON.stringify({ expiresIn }),
315
+ signal: options?.signal ?? null
316
+ });
317
+ if (!res.ok) throw await supabaseError("Supabase sign failed", res);
318
+ const { signedURL } = await res.json();
319
+ if (!signedURL) {
320
+ throw new import_core.StorageError("provider-error", "Supabase sign returned no signedURL", { provider: PROVIDER });
321
+ }
322
+ return `${this.api}${signedURL.startsWith("/") ? "" : "/"}${signedURL}`;
323
+ }
324
+ /** The object name an upload with `options.id` is stored under: `<path>/<id>.<ext>`. */
325
+ pathFor(options) {
326
+ const ext = options.filename.includes(".") ? options.filename.split(".").pop() : "";
327
+ const filename = ext ? `${options.id}.${ext}` : options.id;
328
+ return options.path ? `${options.path}/${filename}` : filename;
329
+ }
330
+ /** The object at `path` (`GET /object/info`), or `null` if it does not exist. */
331
+ async head(path, options) {
332
+ const res = await fetchWithAbort(this.objectUrl("object/info", path), {
333
+ headers: this.headers,
334
+ signal: options?.signal ?? null
335
+ });
336
+ if (!res.ok) {
337
+ const error = await supabaseError("Supabase info failed", res);
338
+ if (error.code === "not-found") return null;
339
+ throw error;
340
+ }
341
+ const info = await res.json();
342
+ const name = path.split("/").pop() ?? path;
343
+ const token = info.metadata?.[TOKEN_KEY];
344
+ return {
345
+ id: name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : name,
346
+ url: `${this.baseUrl}/${(0, import_core.urlEncodePath)(path)}`,
347
+ path,
348
+ size: Number(info.size ?? 0),
349
+ meta: { bucket: this.bucket, ...info.etag ? { etag: info.etag } : {} },
350
+ ...token ? { uploadToken: token } : {},
351
+ ...info.last_modified ? { lastModified: new Date(info.last_modified) } : {},
352
+ ...info.content_type ? { contentType: info.content_type } : {}
353
+ };
354
+ }
355
+ objectUrl(route, path) {
356
+ return `${this.api}/${route}/${encodeURIComponent(this.bucket)}/${(0, import_core.urlEncodePath)(path)}`;
357
+ }
358
+ };
359
+ async function fetchWithAbort(url, init) {
360
+ try {
361
+ return await fetch(url, init);
362
+ } catch (err) {
363
+ if ((0, import_core.isAbortError)(err)) throw err;
364
+ const cause = err instanceof Error ? err : new Error(String(err));
365
+ throw new import_core.StorageError("network", `${PROVIDER} network error: ${cause.message}`, { provider: PROVIDER, cause });
366
+ }
367
+ }
368
+ async function supabaseError(prefix, res) {
369
+ const body = await res.text().catch(() => "");
370
+ return new import_core.StorageError(
371
+ classifySupabaseError(res.status, body),
372
+ `${prefix}: ${res.status} ${res.statusText}${body ? `: ${body}` : ""}`,
373
+ { statusCode: res.status, provider: PROVIDER }
374
+ );
375
+ }
376
+ function base64(value) {
377
+ let binary = "";
378
+ for (const byte of new TextEncoder().encode(value)) binary += String.fromCharCode(byte);
379
+ return btoa(binary);
380
+ }
381
+ function concatBytes(chunks, total) {
382
+ const out = new Uint8Array(total);
383
+ let offset = 0;
384
+ for (const chunk of chunks) {
385
+ out.set(chunk, offset);
386
+ offset += chunk.byteLength;
387
+ }
388
+ return out;
389
+ }
390
+ // Annotate the CommonJS export names for ESM import in node:
391
+ 0 && (module.exports = {
392
+ SupabaseDriver
393
+ });
394
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/errors.ts"],"sourcesContent":["import type { BaseDriver, OperationOptions, PresignedUrlOptions, StoredObject, UploadContext, UploadOptions, UploadResult } from \"@fileway/core\";\nimport { classifySupabaseError } from \"./errors.js\";\nimport { validateUploadOptions, ValidationError, urlEncodePath, abortError, isAbortError, StorageError } from \"@fileway/core\";\n\nconst randomUUID = () => globalThis.crypto.randomUUID();\nconst PROVIDER = \"supabase\";\n\n// Supabase's resumable (TUS) endpoint requires 6 MiB chunks.\nconst TUS_CHUNK_SIZE = 6 * 1024 * 1024;\nconst TUS_VERSION = \"1.0.0\";\nconst PRESIGN_DEFAULT_EXPIRY = 3600;\n// User metadata key carrying FilewayClient's per-upload token.\nconst TOKEN_KEY = \"fileway-upload-token\";\n// How many times a TUS chunk is resent when the server stored only part of it.\nconst MAX_CHUNK_RESENDS = 3;\n\nexport interface SupabaseDriverConfig {\n /** Project URL, e.g. `https://<ref>.supabase.co` (or the direct storage host\n * `https://<ref>.storage.supabase.co`, recommended for large uploads). */\n url: string;\n /** API key: a secret / service role key on the server, or the anon or\n * publishable key together with `accessToken`. */\n key: string;\n /** A signed-in user's JWT, so the bucket's RLS policies apply. */\n accessToken?: string;\n bucket: string;\n /** Public base URL for `getUrl`. Defaults to the bucket's public object URL. */\n baseUrl?: string;\n /** Reject uploads larger than this many bytes with a `size-exceeded` error. */\n maxSizeBytes?: number;\n /** `Cache-Control` max-age in seconds for uploaded objects. Default 3600. */\n cacheControl?: number;\n}\n\nexport type SupabaseMeta = {\n bucket: string;\n etag?: string;\n};\n\n/** The `GET /object/info` response (fields this driver reads). */\ninterface ObjectInfo {\n name?: string;\n size?: number | null;\n content_type?: string | null;\n etag?: string | null;\n last_modified?: string | null;\n metadata?: Record<string, string> | null;\n}\n\nexport class SupabaseDriver implements BaseDriver<SupabaseMeta> {\n readonly name = \"supabase\";\n private api: string;\n private bucket: string;\n private baseUrl: string;\n private headers: Record<string, string>;\n private maxSizeBytes: number | undefined;\n private cacheControl: string;\n\n constructor(config: SupabaseDriverConfig) {\n if (!config.url || !config.key || !config.bucket) {\n throw new StorageError(\"config\", \"SupabaseDriver requires url, key and bucket\", { provider: PROVIDER });\n }\n this.api = `${config.url.replace(/\\/+$/, \"\")}/storage/v1`;\n this.bucket = config.bucket;\n this.baseUrl = (config.baseUrl ?? `${this.api}/object/public/${config.bucket}`).replace(/\\/+$/, \"\");\n // New-style keys (sb_secret_..., sb_publishable_...) are not JWTs and go\n // only in `apikey`; legacy JWT keys and user tokens also go in Authorization.\n const bearer = config.accessToken ?? (config.key.startsWith(\"eyJ\") ? config.key : undefined);\n this.headers = { apikey: config.key, ...(bearer ? { authorization: `Bearer ${bearer}` } : {}) };\n this.maxSizeBytes = config.maxSizeBytes;\n this.cacheControl = `max-age=${config.cacheControl ?? 3600}`;\n }\n\n async upload(\n stream: ReadableStream<Uint8Array>,\n options: UploadOptions,\n context?: UploadContext,\n ): Promise<UploadResult<SupabaseMeta>> {\n validateUploadOptions(options);\n if (this.maxSizeBytes !== undefined && options.size !== undefined && options.size > this.maxSizeBytes) {\n throw new StorageError(\"size-exceeded\", `file size ${options.size} exceeds maxSizeBytes ${this.maxSizeBytes}`, { provider: PROVIDER });\n }\n\n const id = options.id ?? randomUUID();\n const path = this.pathFor({ ...options, id });\n const contentType = options.mimeType ?? \"application/octet-stream\";\n const metadata = { ...options.metadata, ...(context?.uploadToken ? { [TOKEN_KEY]: context.uploadToken } : {}) };\n const signal = options.signal;\n\n const reader = stream.getReader();\n let aborted = signal?.aborted ?? false;\n const onAbort = () => {\n aborted = true;\n // Cancelling the reader wakes any pending `read()` with `done: true`.\n reader.cancel().catch(() => {});\n };\n if (signal && !aborted) signal.addEventListener(\"abort\", onAbort, { once: true });\n\n let totalBytes = 0;\n const read = async (): Promise<Uint8Array | undefined> => {\n const { done, value } = await reader.read();\n if (aborted) throw abortError(\"upload aborted\");\n if (done) return undefined;\n totalBytes += value.byteLength;\n if (this.maxSizeBytes !== undefined && totalBytes > this.maxSizeBytes) {\n throw new StorageError(\"size-exceeded\", `upload exceeds maxSizeBytes ${this.maxSizeBytes}`, { provider: PROVIDER });\n }\n return value;\n };\n const checkSize = () => {\n if (options.size !== undefined && totalBytes !== options.size) {\n throw new ValidationError(`stream delivered ${totalBytes} bytes, expected ${options.size}`);\n }\n };\n\n let tusUrl: string | undefined;\n try {\n let etag: string | undefined;\n\n if (options.size !== undefined && options.size > TUS_CHUNK_SIZE) {\n // Known large size: resumable TUS upload, one 6 MiB chunk in memory at a time.\n tusUrl = await this.createTusUpload(path, options.size, contentType, metadata, signal);\n let buffer: Uint8Array[] = [];\n let bufferedLen = 0;\n let offset = 0;\n while (true) {\n const value = await read();\n if (totalBytes > options.size) checkSize();\n if (value) {\n buffer.push(value);\n bufferedLen += value.byteLength;\n }\n while (bufferedLen >= TUS_CHUNK_SIZE || (!value && bufferedLen > 0)) {\n if (!value) checkSize();\n const merged = concatBytes(buffer, bufferedLen);\n const take = Math.min(TUS_CHUNK_SIZE, bufferedLen);\n offset = await this.patchTus(tusUrl, merged.subarray(0, take), offset, signal);\n const rest = merged.subarray(take);\n buffer = [rest];\n bufferedLen = rest.byteLength;\n }\n if (!value) break;\n }\n checkSize();\n } else {\n // Read the first chunk; if the stream ends inside it, send one buffered request.\n const first: Uint8Array[] = [];\n let firstLen = 0;\n let ended = false;\n while (firstLen <= TUS_CHUNK_SIZE) {\n const value = await read();\n if (!value) {\n ended = true;\n break;\n }\n first.push(value);\n firstLen += value.byteLength;\n }\n const head = concatBytes(first, firstLen);\n\n if (ended) {\n checkSize();\n etag = await this.standardUpload(path, head, contentType, metadata, signal);\n } else {\n // A declared size this small cannot be followed by more data.\n checkSize();\n // Unknown size beyond one chunk: stream the rest in a single request,\n // so memory stays bounded without knowing the total in advance.\n const bodyError: { current?: unknown } = {};\n const body = new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(head);\n },\n async pull(controller) {\n try {\n const value = await read();\n if (value) controller.enqueue(value);\n else controller.close();\n } catch (err) {\n bodyError.current = err;\n controller.error(err);\n }\n },\n cancel(reason) {\n return reader.cancel(reason);\n },\n });\n try {\n etag = await this.standardUpload(path, body, contentType, metadata, signal);\n } catch (err) {\n // A body error (abort, size limit) surfaces as a generic fetch failure.\n if (bodyError.current) throw bodyError.current;\n throw err;\n }\n checkSize();\n }\n }\n\n return {\n id,\n url: `${this.baseUrl}/${urlEncodePath(path)}`,\n path,\n size: totalBytes,\n meta: { bucket: this.bucket, ...(etag ? { etag } : {}) },\n };\n } catch (err) {\n reader.cancel().catch(() => {});\n if (tusUrl) {\n // Best-effort: terminate the unfinished TUS upload.\n await fetch(tusUrl, { method: \"DELETE\", headers: { ...this.headers, \"tus-resumable\": TUS_VERSION } }).catch(() => {});\n }\n throw err;\n } finally {\n signal?.removeEventListener(\"abort\", onAbort);\n }\n }\n\n /** `POST /object/{bucket}/{path}`, overwriting any existing object. */\n private async standardUpload(\n path: string,\n body: Uint8Array | ReadableStream<Uint8Array>,\n contentType: string,\n metadata: Record<string, string>,\n signal: AbortSignal | undefined,\n ): Promise<string | undefined> {\n const res = await fetchWithAbort(this.objectUrl(\"object\", path), {\n method: \"POST\",\n headers: {\n ...this.headers,\n \"content-type\": contentType,\n \"cache-control\": this.cacheControl,\n \"x-upsert\": \"true\",\n \"x-metadata\": base64(JSON.stringify(metadata)),\n },\n body: body as BodyInit,\n // Node's fetch (undici) requires `duplex: \"half\"` for stream bodies;\n // other runtimes ignore it.\n duplex: \"half\",\n signal: signal ?? null,\n } as RequestInit);\n if (!res.ok) throw await supabaseError(\"Supabase upload failed\", res);\n return res.headers.get(\"etag\") ?? undefined;\n }\n\n /** Creates a TUS upload and returns its URL. */\n private async createTusUpload(\n path: string,\n size: number,\n contentType: string,\n metadata: Record<string, string>,\n signal: AbortSignal | undefined,\n ): Promise<string> {\n const endpoint = `${this.api}/upload/resumable`;\n const uploadMetadata = {\n bucketName: this.bucket,\n objectName: path,\n contentType,\n cacheControl: this.cacheControl.replace(\"max-age=\", \"\"),\n metadata: JSON.stringify(metadata),\n };\n const res = await fetchWithAbort(endpoint, {\n method: \"POST\",\n headers: {\n ...this.headers,\n \"tus-resumable\": TUS_VERSION,\n \"upload-length\": String(size),\n \"upload-metadata\": Object.entries(uploadMetadata)\n .map(([k, v]) => `${k} ${base64(v)}`)\n .join(\",\"),\n \"x-upsert\": \"true\",\n },\n signal: signal ?? null,\n });\n if (!res.ok) throw await supabaseError(\"Supabase resumable upload create failed\", res);\n const location = res.headers.get(\"location\");\n if (!location) {\n throw new StorageError(\"provider-error\", \"Supabase resumable upload returned no Location header\", { provider: PROVIDER });\n }\n return new URL(location, endpoint).href;\n }\n\n /** PATCHes `bytes` at `offset`, resending whatever the server did not store.\n * Returns the new offset. */\n private async patchTus(url: string, bytes: Uint8Array, offset: number, signal: AbortSignal | undefined): Promise<number> {\n const end = offset + bytes.byteLength;\n let current = offset;\n for (let attempt = 0; attempt <= MAX_CHUNK_RESENDS && current < end; attempt++) {\n const res = await fetchWithAbort(url, {\n method: \"PATCH\",\n headers: {\n ...this.headers,\n \"tus-resumable\": TUS_VERSION,\n \"upload-offset\": String(current),\n \"content-type\": \"application/offset+octet-stream\",\n },\n body: bytes.subarray(current - offset) as BodyInit,\n signal: signal ?? null,\n });\n if (!res.ok) throw await supabaseError(\"Supabase resumable upload chunk failed\", res);\n const next = Number(res.headers.get(\"upload-offset\") ?? end);\n if (next < current) break;\n current = next;\n }\n if (current < end) {\n throw new StorageError(\"provider-error\", \"Supabase did not store the uploaded chunk\", { provider: PROVIDER });\n }\n return current;\n }\n\n /** Deletes the object; a missing object is not an error. */\n async delete(path: string, options?: OperationOptions): Promise<void> {\n const res = await fetchWithAbort(`${this.api}/object/${encodeURIComponent(this.bucket)}`, {\n method: \"DELETE\",\n headers: { ...this.headers, \"content-type\": \"application/json\" },\n body: JSON.stringify({ prefixes: [path] }),\n signal: options?.signal ?? null,\n });\n if (!res.ok) throw await supabaseError(\"Supabase delete failed\", res);\n }\n\n async getUrl(path: string, _options?: OperationOptions): Promise<string> {\n return `${this.baseUrl}/${urlEncodePath(path)}`;\n }\n\n /** Streams the object. `options.signal` also cancels reading the body. */\n async get(path: string, options?: OperationOptions): Promise<ReadableStream<Uint8Array>> {\n const res = await fetchWithAbort(this.objectUrl(\"object/authenticated\", path), {\n headers: this.headers,\n signal: options?.signal ?? null,\n });\n if (!res.ok) throw await supabaseError(\"Supabase get failed\", res);\n if (!res.body) {\n throw new StorageError(\"provider-error\", \"Supabase get returned no response body\", { provider: PROVIDER });\n }\n return res.body;\n }\n\n /** A signed download URL for `path`, valid for `expiresIn` seconds (positive integer, default 3600). */\n async getPresignedUrl(path: string, options?: PresignedUrlOptions): Promise<string> {\n const expiresIn = options?.expiresIn ?? PRESIGN_DEFAULT_EXPIRY;\n if (!Number.isInteger(expiresIn) || expiresIn < 1) {\n throw new ValidationError(\"expiresIn must be a positive integer number of seconds\");\n }\n const res = await fetchWithAbort(this.objectUrl(\"object/sign\", path), {\n method: \"POST\",\n headers: { ...this.headers, \"content-type\": \"application/json\" },\n body: JSON.stringify({ expiresIn }),\n signal: options?.signal ?? null,\n });\n if (!res.ok) throw await supabaseError(\"Supabase sign failed\", res);\n const { signedURL } = (await res.json()) as { signedURL?: string };\n if (!signedURL) {\n throw new StorageError(\"provider-error\", \"Supabase sign returned no signedURL\", { provider: PROVIDER });\n }\n return `${this.api}${signedURL.startsWith(\"/\") ? \"\" : \"/\"}${signedURL}`;\n }\n\n /** The object name an upload with `options.id` is stored under: `<path>/<id>.<ext>`. */\n pathFor(options: UploadOptions & { id: string }): string {\n const ext = options.filename.includes(\".\") ? options.filename.split(\".\").pop()! : \"\";\n const filename = ext ? `${options.id}.${ext}` : options.id;\n return options.path ? `${options.path}/${filename}` : filename;\n }\n\n /** The object at `path` (`GET /object/info`), or `null` if it does not exist. */\n async head(path: string, options?: OperationOptions): Promise<StoredObject<SupabaseMeta> | null> {\n const res = await fetchWithAbort(this.objectUrl(\"object/info\", path), {\n headers: this.headers,\n signal: options?.signal ?? null,\n });\n if (!res.ok) {\n const error = await supabaseError(\"Supabase info failed\", res);\n if (error.code === \"not-found\") return null;\n throw error;\n }\n const info = (await res.json()) as ObjectInfo;\n const name = path.split(\"/\").pop() ?? path;\n const token = info.metadata?.[TOKEN_KEY];\n return {\n id: name.includes(\".\") ? name.slice(0, name.lastIndexOf(\".\")) : name,\n url: `${this.baseUrl}/${urlEncodePath(path)}`,\n path,\n size: Number(info.size ?? 0),\n meta: { bucket: this.bucket, ...(info.etag ? { etag: info.etag } : {}) },\n ...(token ? { uploadToken: token } : {}),\n ...(info.last_modified ? { lastModified: new Date(info.last_modified) } : {}),\n ...(info.content_type ? { contentType: info.content_type } : {}),\n };\n }\n\n private objectUrl(route: string, path: string): string {\n return `${this.api}/${route}/${encodeURIComponent(this.bucket)}/${urlEncodePath(path)}`;\n }\n}\n\n/** `fetch` that converts non-abort failures into a typed `network` error,\n * while letting aborts (`AbortError`) pass through untouched. */\nasync function fetchWithAbort(url: string | URL, init: RequestInit): Promise<Response> {\n try {\n return await fetch(url, init);\n } catch (err) {\n if (isAbortError(err)) throw err;\n const cause = err instanceof Error ? err : new Error(String(err));\n throw new StorageError(\"network\", `${PROVIDER} network error: ${cause.message}`, { provider: PROVIDER, cause });\n }\n}\n\nasync function supabaseError(prefix: string, res: Response): Promise<StorageError> {\n const body = await res.text().catch(() => \"\");\n return new StorageError(\n classifySupabaseError(res.status, body),\n `${prefix}: ${res.status} ${res.statusText}${body ? `: ${body}` : \"\"}`,\n { statusCode: res.status, provider: PROVIDER },\n );\n}\n\nfunction base64(value: string): string {\n let binary = \"\";\n for (const byte of new TextEncoder().encode(value)) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\nfunction concatBytes(chunks: Uint8Array[], total: number): Uint8Array {\n const out = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return out;\n}\n","import type { StorageErrorCode } from \"@fileway/core\";\n\n// Internal: error classification for SupabaseDriver. Not exported from the package.\n\n/**\n * Classifies a Supabase Storage failure. The API often answers 400 with the\n * real status in the JSON body (`{ statusCode: \"404\", error, message }`), so\n * the body is checked before the HTTP status.\n */\nexport function classifySupabaseError(status: number, body: string): StorageErrorCode {\n let parsed: { statusCode?: string | number; error?: string; message?: string } = {};\n try {\n parsed = JSON.parse(body) as typeof parsed;\n } catch {\n // Not JSON (e.g. a TUS error); fall through to text and status checks.\n }\n const text = `${parsed.error ?? \"\"} ${parsed.message ?? \"\"} ${parsed.error === undefined ? body : \"\"}`.toLowerCase();\n const code = Number(parsed.statusCode ?? status);\n\n if (text.includes(\"bucket not found\")) return \"bucket-not-found\";\n if (text.includes(\"not found\") || text.includes(\"not_found\") || text.includes(\"no such\")) return \"not-found\";\n if (text.includes(\"exceeded the maximum allowed size\") || text.includes(\"payload too large\") || code === 413) {\n return \"size-exceeded\";\n }\n if (code === 401 || code === 403 || text.includes(\"jwt\") || text.includes(\"unauthorized\") || text.includes(\"row-level security\")) {\n return \"auth-failed\";\n }\n if (code === 429 || text.includes(\"too many requests\")) return \"rate-limited\";\n if (code === 404) return \"not-found\";\n return \"provider-error\";\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,57 @@
1
+ import { BaseDriver, UploadOptions, UploadContext, UploadResult, OperationOptions, PresignedUrlOptions, StoredObject } from '@fileway/core';
2
+
3
+ interface SupabaseDriverConfig {
4
+ /** Project URL, e.g. `https://<ref>.supabase.co` (or the direct storage host
5
+ * `https://<ref>.storage.supabase.co`, recommended for large uploads). */
6
+ url: string;
7
+ /** API key: a secret / service role key on the server, or the anon or
8
+ * publishable key together with `accessToken`. */
9
+ key: string;
10
+ /** A signed-in user's JWT, so the bucket's RLS policies apply. */
11
+ accessToken?: string;
12
+ bucket: string;
13
+ /** Public base URL for `getUrl`. Defaults to the bucket's public object URL. */
14
+ baseUrl?: string;
15
+ /** Reject uploads larger than this many bytes with a `size-exceeded` error. */
16
+ maxSizeBytes?: number;
17
+ /** `Cache-Control` max-age in seconds for uploaded objects. Default 3600. */
18
+ cacheControl?: number;
19
+ }
20
+ type SupabaseMeta = {
21
+ bucket: string;
22
+ etag?: string;
23
+ };
24
+ declare class SupabaseDriver implements BaseDriver<SupabaseMeta> {
25
+ readonly name = "supabase";
26
+ private api;
27
+ private bucket;
28
+ private baseUrl;
29
+ private headers;
30
+ private maxSizeBytes;
31
+ private cacheControl;
32
+ constructor(config: SupabaseDriverConfig);
33
+ upload(stream: ReadableStream<Uint8Array>, options: UploadOptions, context?: UploadContext): Promise<UploadResult<SupabaseMeta>>;
34
+ /** `POST /object/{bucket}/{path}`, overwriting any existing object. */
35
+ private standardUpload;
36
+ /** Creates a TUS upload and returns its URL. */
37
+ private createTusUpload;
38
+ /** PATCHes `bytes` at `offset`, resending whatever the server did not store.
39
+ * Returns the new offset. */
40
+ private patchTus;
41
+ /** Deletes the object; a missing object is not an error. */
42
+ delete(path: string, options?: OperationOptions): Promise<void>;
43
+ getUrl(path: string, _options?: OperationOptions): Promise<string>;
44
+ /** Streams the object. `options.signal` also cancels reading the body. */
45
+ get(path: string, options?: OperationOptions): Promise<ReadableStream<Uint8Array>>;
46
+ /** A signed download URL for `path`, valid for `expiresIn` seconds (positive integer, default 3600). */
47
+ getPresignedUrl(path: string, options?: PresignedUrlOptions): Promise<string>;
48
+ /** The object name an upload with `options.id` is stored under: `<path>/<id>.<ext>`. */
49
+ pathFor(options: UploadOptions & {
50
+ id: string;
51
+ }): string;
52
+ /** The object at `path` (`GET /object/info`), or `null` if it does not exist. */
53
+ head(path: string, options?: OperationOptions): Promise<StoredObject<SupabaseMeta> | null>;
54
+ private objectUrl;
55
+ }
56
+
57
+ export { SupabaseDriver, type SupabaseDriverConfig, type SupabaseMeta };
@@ -0,0 +1,57 @@
1
+ import { BaseDriver, UploadOptions, UploadContext, UploadResult, OperationOptions, PresignedUrlOptions, StoredObject } from '@fileway/core';
2
+
3
+ interface SupabaseDriverConfig {
4
+ /** Project URL, e.g. `https://<ref>.supabase.co` (or the direct storage host
5
+ * `https://<ref>.storage.supabase.co`, recommended for large uploads). */
6
+ url: string;
7
+ /** API key: a secret / service role key on the server, or the anon or
8
+ * publishable key together with `accessToken`. */
9
+ key: string;
10
+ /** A signed-in user's JWT, so the bucket's RLS policies apply. */
11
+ accessToken?: string;
12
+ bucket: string;
13
+ /** Public base URL for `getUrl`. Defaults to the bucket's public object URL. */
14
+ baseUrl?: string;
15
+ /** Reject uploads larger than this many bytes with a `size-exceeded` error. */
16
+ maxSizeBytes?: number;
17
+ /** `Cache-Control` max-age in seconds for uploaded objects. Default 3600. */
18
+ cacheControl?: number;
19
+ }
20
+ type SupabaseMeta = {
21
+ bucket: string;
22
+ etag?: string;
23
+ };
24
+ declare class SupabaseDriver implements BaseDriver<SupabaseMeta> {
25
+ readonly name = "supabase";
26
+ private api;
27
+ private bucket;
28
+ private baseUrl;
29
+ private headers;
30
+ private maxSizeBytes;
31
+ private cacheControl;
32
+ constructor(config: SupabaseDriverConfig);
33
+ upload(stream: ReadableStream<Uint8Array>, options: UploadOptions, context?: UploadContext): Promise<UploadResult<SupabaseMeta>>;
34
+ /** `POST /object/{bucket}/{path}`, overwriting any existing object. */
35
+ private standardUpload;
36
+ /** Creates a TUS upload and returns its URL. */
37
+ private createTusUpload;
38
+ /** PATCHes `bytes` at `offset`, resending whatever the server did not store.
39
+ * Returns the new offset. */
40
+ private patchTus;
41
+ /** Deletes the object; a missing object is not an error. */
42
+ delete(path: string, options?: OperationOptions): Promise<void>;
43
+ getUrl(path: string, _options?: OperationOptions): Promise<string>;
44
+ /** Streams the object. `options.signal` also cancels reading the body. */
45
+ get(path: string, options?: OperationOptions): Promise<ReadableStream<Uint8Array>>;
46
+ /** A signed download URL for `path`, valid for `expiresIn` seconds (positive integer, default 3600). */
47
+ getPresignedUrl(path: string, options?: PresignedUrlOptions): Promise<string>;
48
+ /** The object name an upload with `options.id` is stored under: `<path>/<id>.<ext>`. */
49
+ pathFor(options: UploadOptions & {
50
+ id: string;
51
+ }): string;
52
+ /** The object at `path` (`GET /object/info`), or `null` if it does not exist. */
53
+ head(path: string, options?: OperationOptions): Promise<StoredObject<SupabaseMeta> | null>;
54
+ private objectUrl;
55
+ }
56
+
57
+ export { SupabaseDriver, type SupabaseDriverConfig, type SupabaseMeta };
package/dist/index.js ADDED
@@ -0,0 +1,367 @@
1
+ // src/errors.ts
2
+ function classifySupabaseError(status, body) {
3
+ let parsed = {};
4
+ try {
5
+ parsed = JSON.parse(body);
6
+ } catch {
7
+ }
8
+ const text = `${parsed.error ?? ""} ${parsed.message ?? ""} ${parsed.error === void 0 ? body : ""}`.toLowerCase();
9
+ const code = Number(parsed.statusCode ?? status);
10
+ if (text.includes("bucket not found")) return "bucket-not-found";
11
+ if (text.includes("not found") || text.includes("not_found") || text.includes("no such")) return "not-found";
12
+ if (text.includes("exceeded the maximum allowed size") || text.includes("payload too large") || code === 413) {
13
+ return "size-exceeded";
14
+ }
15
+ if (code === 401 || code === 403 || text.includes("jwt") || text.includes("unauthorized") || text.includes("row-level security")) {
16
+ return "auth-failed";
17
+ }
18
+ if (code === 429 || text.includes("too many requests")) return "rate-limited";
19
+ if (code === 404) return "not-found";
20
+ return "provider-error";
21
+ }
22
+
23
+ // src/index.ts
24
+ import { validateUploadOptions, ValidationError, urlEncodePath, abortError, isAbortError, StorageError } from "@fileway/core";
25
+ var randomUUID = () => globalThis.crypto.randomUUID();
26
+ var PROVIDER = "supabase";
27
+ var TUS_CHUNK_SIZE = 6 * 1024 * 1024;
28
+ var TUS_VERSION = "1.0.0";
29
+ var PRESIGN_DEFAULT_EXPIRY = 3600;
30
+ var TOKEN_KEY = "fileway-upload-token";
31
+ var MAX_CHUNK_RESENDS = 3;
32
+ var SupabaseDriver = class {
33
+ name = "supabase";
34
+ api;
35
+ bucket;
36
+ baseUrl;
37
+ headers;
38
+ maxSizeBytes;
39
+ cacheControl;
40
+ constructor(config) {
41
+ if (!config.url || !config.key || !config.bucket) {
42
+ throw new StorageError("config", "SupabaseDriver requires url, key and bucket", { provider: PROVIDER });
43
+ }
44
+ this.api = `${config.url.replace(/\/+$/, "")}/storage/v1`;
45
+ this.bucket = config.bucket;
46
+ this.baseUrl = (config.baseUrl ?? `${this.api}/object/public/${config.bucket}`).replace(/\/+$/, "");
47
+ const bearer = config.accessToken ?? (config.key.startsWith("eyJ") ? config.key : void 0);
48
+ this.headers = { apikey: config.key, ...bearer ? { authorization: `Bearer ${bearer}` } : {} };
49
+ this.maxSizeBytes = config.maxSizeBytes;
50
+ this.cacheControl = `max-age=${config.cacheControl ?? 3600}`;
51
+ }
52
+ async upload(stream, options, context) {
53
+ validateUploadOptions(options);
54
+ if (this.maxSizeBytes !== void 0 && options.size !== void 0 && options.size > this.maxSizeBytes) {
55
+ throw new StorageError("size-exceeded", `file size ${options.size} exceeds maxSizeBytes ${this.maxSizeBytes}`, { provider: PROVIDER });
56
+ }
57
+ const id = options.id ?? randomUUID();
58
+ const path = this.pathFor({ ...options, id });
59
+ const contentType = options.mimeType ?? "application/octet-stream";
60
+ const metadata = { ...options.metadata, ...context?.uploadToken ? { [TOKEN_KEY]: context.uploadToken } : {} };
61
+ const signal = options.signal;
62
+ const reader = stream.getReader();
63
+ let aborted = signal?.aborted ?? false;
64
+ const onAbort = () => {
65
+ aborted = true;
66
+ reader.cancel().catch(() => {
67
+ });
68
+ };
69
+ if (signal && !aborted) signal.addEventListener("abort", onAbort, { once: true });
70
+ let totalBytes = 0;
71
+ const read = async () => {
72
+ const { done, value } = await reader.read();
73
+ if (aborted) throw abortError("upload aborted");
74
+ if (done) return void 0;
75
+ totalBytes += value.byteLength;
76
+ if (this.maxSizeBytes !== void 0 && totalBytes > this.maxSizeBytes) {
77
+ throw new StorageError("size-exceeded", `upload exceeds maxSizeBytes ${this.maxSizeBytes}`, { provider: PROVIDER });
78
+ }
79
+ return value;
80
+ };
81
+ const checkSize = () => {
82
+ if (options.size !== void 0 && totalBytes !== options.size) {
83
+ throw new ValidationError(`stream delivered ${totalBytes} bytes, expected ${options.size}`);
84
+ }
85
+ };
86
+ let tusUrl;
87
+ try {
88
+ let etag;
89
+ if (options.size !== void 0 && options.size > TUS_CHUNK_SIZE) {
90
+ tusUrl = await this.createTusUpload(path, options.size, contentType, metadata, signal);
91
+ let buffer = [];
92
+ let bufferedLen = 0;
93
+ let offset = 0;
94
+ while (true) {
95
+ const value = await read();
96
+ if (totalBytes > options.size) checkSize();
97
+ if (value) {
98
+ buffer.push(value);
99
+ bufferedLen += value.byteLength;
100
+ }
101
+ while (bufferedLen >= TUS_CHUNK_SIZE || !value && bufferedLen > 0) {
102
+ if (!value) checkSize();
103
+ const merged = concatBytes(buffer, bufferedLen);
104
+ const take = Math.min(TUS_CHUNK_SIZE, bufferedLen);
105
+ offset = await this.patchTus(tusUrl, merged.subarray(0, take), offset, signal);
106
+ const rest = merged.subarray(take);
107
+ buffer = [rest];
108
+ bufferedLen = rest.byteLength;
109
+ }
110
+ if (!value) break;
111
+ }
112
+ checkSize();
113
+ } else {
114
+ const first = [];
115
+ let firstLen = 0;
116
+ let ended = false;
117
+ while (firstLen <= TUS_CHUNK_SIZE) {
118
+ const value = await read();
119
+ if (!value) {
120
+ ended = true;
121
+ break;
122
+ }
123
+ first.push(value);
124
+ firstLen += value.byteLength;
125
+ }
126
+ const head = concatBytes(first, firstLen);
127
+ if (ended) {
128
+ checkSize();
129
+ etag = await this.standardUpload(path, head, contentType, metadata, signal);
130
+ } else {
131
+ checkSize();
132
+ const bodyError = {};
133
+ const body = new ReadableStream({
134
+ start(controller) {
135
+ controller.enqueue(head);
136
+ },
137
+ async pull(controller) {
138
+ try {
139
+ const value = await read();
140
+ if (value) controller.enqueue(value);
141
+ else controller.close();
142
+ } catch (err) {
143
+ bodyError.current = err;
144
+ controller.error(err);
145
+ }
146
+ },
147
+ cancel(reason) {
148
+ return reader.cancel(reason);
149
+ }
150
+ });
151
+ try {
152
+ etag = await this.standardUpload(path, body, contentType, metadata, signal);
153
+ } catch (err) {
154
+ if (bodyError.current) throw bodyError.current;
155
+ throw err;
156
+ }
157
+ checkSize();
158
+ }
159
+ }
160
+ return {
161
+ id,
162
+ url: `${this.baseUrl}/${urlEncodePath(path)}`,
163
+ path,
164
+ size: totalBytes,
165
+ meta: { bucket: this.bucket, ...etag ? { etag } : {} }
166
+ };
167
+ } catch (err) {
168
+ reader.cancel().catch(() => {
169
+ });
170
+ if (tusUrl) {
171
+ await fetch(tusUrl, { method: "DELETE", headers: { ...this.headers, "tus-resumable": TUS_VERSION } }).catch(() => {
172
+ });
173
+ }
174
+ throw err;
175
+ } finally {
176
+ signal?.removeEventListener("abort", onAbort);
177
+ }
178
+ }
179
+ /** `POST /object/{bucket}/{path}`, overwriting any existing object. */
180
+ async standardUpload(path, body, contentType, metadata, signal) {
181
+ const res = await fetchWithAbort(this.objectUrl("object", path), {
182
+ method: "POST",
183
+ headers: {
184
+ ...this.headers,
185
+ "content-type": contentType,
186
+ "cache-control": this.cacheControl,
187
+ "x-upsert": "true",
188
+ "x-metadata": base64(JSON.stringify(metadata))
189
+ },
190
+ body,
191
+ // Node's fetch (undici) requires `duplex: "half"` for stream bodies;
192
+ // other runtimes ignore it.
193
+ duplex: "half",
194
+ signal: signal ?? null
195
+ });
196
+ if (!res.ok) throw await supabaseError("Supabase upload failed", res);
197
+ return res.headers.get("etag") ?? void 0;
198
+ }
199
+ /** Creates a TUS upload and returns its URL. */
200
+ async createTusUpload(path, size, contentType, metadata, signal) {
201
+ const endpoint = `${this.api}/upload/resumable`;
202
+ const uploadMetadata = {
203
+ bucketName: this.bucket,
204
+ objectName: path,
205
+ contentType,
206
+ cacheControl: this.cacheControl.replace("max-age=", ""),
207
+ metadata: JSON.stringify(metadata)
208
+ };
209
+ const res = await fetchWithAbort(endpoint, {
210
+ method: "POST",
211
+ headers: {
212
+ ...this.headers,
213
+ "tus-resumable": TUS_VERSION,
214
+ "upload-length": String(size),
215
+ "upload-metadata": Object.entries(uploadMetadata).map(([k, v]) => `${k} ${base64(v)}`).join(","),
216
+ "x-upsert": "true"
217
+ },
218
+ signal: signal ?? null
219
+ });
220
+ if (!res.ok) throw await supabaseError("Supabase resumable upload create failed", res);
221
+ const location = res.headers.get("location");
222
+ if (!location) {
223
+ throw new StorageError("provider-error", "Supabase resumable upload returned no Location header", { provider: PROVIDER });
224
+ }
225
+ return new URL(location, endpoint).href;
226
+ }
227
+ /** PATCHes `bytes` at `offset`, resending whatever the server did not store.
228
+ * Returns the new offset. */
229
+ async patchTus(url, bytes, offset, signal) {
230
+ const end = offset + bytes.byteLength;
231
+ let current = offset;
232
+ for (let attempt = 0; attempt <= MAX_CHUNK_RESENDS && current < end; attempt++) {
233
+ const res = await fetchWithAbort(url, {
234
+ method: "PATCH",
235
+ headers: {
236
+ ...this.headers,
237
+ "tus-resumable": TUS_VERSION,
238
+ "upload-offset": String(current),
239
+ "content-type": "application/offset+octet-stream"
240
+ },
241
+ body: bytes.subarray(current - offset),
242
+ signal: signal ?? null
243
+ });
244
+ if (!res.ok) throw await supabaseError("Supabase resumable upload chunk failed", res);
245
+ const next = Number(res.headers.get("upload-offset") ?? end);
246
+ if (next < current) break;
247
+ current = next;
248
+ }
249
+ if (current < end) {
250
+ throw new StorageError("provider-error", "Supabase did not store the uploaded chunk", { provider: PROVIDER });
251
+ }
252
+ return current;
253
+ }
254
+ /** Deletes the object; a missing object is not an error. */
255
+ async delete(path, options) {
256
+ const res = await fetchWithAbort(`${this.api}/object/${encodeURIComponent(this.bucket)}`, {
257
+ method: "DELETE",
258
+ headers: { ...this.headers, "content-type": "application/json" },
259
+ body: JSON.stringify({ prefixes: [path] }),
260
+ signal: options?.signal ?? null
261
+ });
262
+ if (!res.ok) throw await supabaseError("Supabase delete failed", res);
263
+ }
264
+ async getUrl(path, _options) {
265
+ return `${this.baseUrl}/${urlEncodePath(path)}`;
266
+ }
267
+ /** Streams the object. `options.signal` also cancels reading the body. */
268
+ async get(path, options) {
269
+ const res = await fetchWithAbort(this.objectUrl("object/authenticated", path), {
270
+ headers: this.headers,
271
+ signal: options?.signal ?? null
272
+ });
273
+ if (!res.ok) throw await supabaseError("Supabase get failed", res);
274
+ if (!res.body) {
275
+ throw new StorageError("provider-error", "Supabase get returned no response body", { provider: PROVIDER });
276
+ }
277
+ return res.body;
278
+ }
279
+ /** A signed download URL for `path`, valid for `expiresIn` seconds (positive integer, default 3600). */
280
+ async getPresignedUrl(path, options) {
281
+ const expiresIn = options?.expiresIn ?? PRESIGN_DEFAULT_EXPIRY;
282
+ if (!Number.isInteger(expiresIn) || expiresIn < 1) {
283
+ throw new ValidationError("expiresIn must be a positive integer number of seconds");
284
+ }
285
+ const res = await fetchWithAbort(this.objectUrl("object/sign", path), {
286
+ method: "POST",
287
+ headers: { ...this.headers, "content-type": "application/json" },
288
+ body: JSON.stringify({ expiresIn }),
289
+ signal: options?.signal ?? null
290
+ });
291
+ if (!res.ok) throw await supabaseError("Supabase sign failed", res);
292
+ const { signedURL } = await res.json();
293
+ if (!signedURL) {
294
+ throw new StorageError("provider-error", "Supabase sign returned no signedURL", { provider: PROVIDER });
295
+ }
296
+ return `${this.api}${signedURL.startsWith("/") ? "" : "/"}${signedURL}`;
297
+ }
298
+ /** The object name an upload with `options.id` is stored under: `<path>/<id>.<ext>`. */
299
+ pathFor(options) {
300
+ const ext = options.filename.includes(".") ? options.filename.split(".").pop() : "";
301
+ const filename = ext ? `${options.id}.${ext}` : options.id;
302
+ return options.path ? `${options.path}/${filename}` : filename;
303
+ }
304
+ /** The object at `path` (`GET /object/info`), or `null` if it does not exist. */
305
+ async head(path, options) {
306
+ const res = await fetchWithAbort(this.objectUrl("object/info", path), {
307
+ headers: this.headers,
308
+ signal: options?.signal ?? null
309
+ });
310
+ if (!res.ok) {
311
+ const error = await supabaseError("Supabase info failed", res);
312
+ if (error.code === "not-found") return null;
313
+ throw error;
314
+ }
315
+ const info = await res.json();
316
+ const name = path.split("/").pop() ?? path;
317
+ const token = info.metadata?.[TOKEN_KEY];
318
+ return {
319
+ id: name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : name,
320
+ url: `${this.baseUrl}/${urlEncodePath(path)}`,
321
+ path,
322
+ size: Number(info.size ?? 0),
323
+ meta: { bucket: this.bucket, ...info.etag ? { etag: info.etag } : {} },
324
+ ...token ? { uploadToken: token } : {},
325
+ ...info.last_modified ? { lastModified: new Date(info.last_modified) } : {},
326
+ ...info.content_type ? { contentType: info.content_type } : {}
327
+ };
328
+ }
329
+ objectUrl(route, path) {
330
+ return `${this.api}/${route}/${encodeURIComponent(this.bucket)}/${urlEncodePath(path)}`;
331
+ }
332
+ };
333
+ async function fetchWithAbort(url, init) {
334
+ try {
335
+ return await fetch(url, init);
336
+ } catch (err) {
337
+ if (isAbortError(err)) throw err;
338
+ const cause = err instanceof Error ? err : new Error(String(err));
339
+ throw new StorageError("network", `${PROVIDER} network error: ${cause.message}`, { provider: PROVIDER, cause });
340
+ }
341
+ }
342
+ async function supabaseError(prefix, res) {
343
+ const body = await res.text().catch(() => "");
344
+ return new StorageError(
345
+ classifySupabaseError(res.status, body),
346
+ `${prefix}: ${res.status} ${res.statusText}${body ? `: ${body}` : ""}`,
347
+ { statusCode: res.status, provider: PROVIDER }
348
+ );
349
+ }
350
+ function base64(value) {
351
+ let binary = "";
352
+ for (const byte of new TextEncoder().encode(value)) binary += String.fromCharCode(byte);
353
+ return btoa(binary);
354
+ }
355
+ function concatBytes(chunks, total) {
356
+ const out = new Uint8Array(total);
357
+ let offset = 0;
358
+ for (const chunk of chunks) {
359
+ out.set(chunk, offset);
360
+ offset += chunk.byteLength;
361
+ }
362
+ return out;
363
+ }
364
+ export {
365
+ SupabaseDriver
366
+ };
367
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/index.ts"],"sourcesContent":["import type { StorageErrorCode } from \"@fileway/core\";\n\n// Internal: error classification for SupabaseDriver. Not exported from the package.\n\n/**\n * Classifies a Supabase Storage failure. The API often answers 400 with the\n * real status in the JSON body (`{ statusCode: \"404\", error, message }`), so\n * the body is checked before the HTTP status.\n */\nexport function classifySupabaseError(status: number, body: string): StorageErrorCode {\n let parsed: { statusCode?: string | number; error?: string; message?: string } = {};\n try {\n parsed = JSON.parse(body) as typeof parsed;\n } catch {\n // Not JSON (e.g. a TUS error); fall through to text and status checks.\n }\n const text = `${parsed.error ?? \"\"} ${parsed.message ?? \"\"} ${parsed.error === undefined ? body : \"\"}`.toLowerCase();\n const code = Number(parsed.statusCode ?? status);\n\n if (text.includes(\"bucket not found\")) return \"bucket-not-found\";\n if (text.includes(\"not found\") || text.includes(\"not_found\") || text.includes(\"no such\")) return \"not-found\";\n if (text.includes(\"exceeded the maximum allowed size\") || text.includes(\"payload too large\") || code === 413) {\n return \"size-exceeded\";\n }\n if (code === 401 || code === 403 || text.includes(\"jwt\") || text.includes(\"unauthorized\") || text.includes(\"row-level security\")) {\n return \"auth-failed\";\n }\n if (code === 429 || text.includes(\"too many requests\")) return \"rate-limited\";\n if (code === 404) return \"not-found\";\n return \"provider-error\";\n}\n","import type { BaseDriver, OperationOptions, PresignedUrlOptions, StoredObject, UploadContext, UploadOptions, UploadResult } from \"@fileway/core\";\nimport { classifySupabaseError } from \"./errors.js\";\nimport { validateUploadOptions, ValidationError, urlEncodePath, abortError, isAbortError, StorageError } from \"@fileway/core\";\n\nconst randomUUID = () => globalThis.crypto.randomUUID();\nconst PROVIDER = \"supabase\";\n\n// Supabase's resumable (TUS) endpoint requires 6 MiB chunks.\nconst TUS_CHUNK_SIZE = 6 * 1024 * 1024;\nconst TUS_VERSION = \"1.0.0\";\nconst PRESIGN_DEFAULT_EXPIRY = 3600;\n// User metadata key carrying FilewayClient's per-upload token.\nconst TOKEN_KEY = \"fileway-upload-token\";\n// How many times a TUS chunk is resent when the server stored only part of it.\nconst MAX_CHUNK_RESENDS = 3;\n\nexport interface SupabaseDriverConfig {\n /** Project URL, e.g. `https://<ref>.supabase.co` (or the direct storage host\n * `https://<ref>.storage.supabase.co`, recommended for large uploads). */\n url: string;\n /** API key: a secret / service role key on the server, or the anon or\n * publishable key together with `accessToken`. */\n key: string;\n /** A signed-in user's JWT, so the bucket's RLS policies apply. */\n accessToken?: string;\n bucket: string;\n /** Public base URL for `getUrl`. Defaults to the bucket's public object URL. */\n baseUrl?: string;\n /** Reject uploads larger than this many bytes with a `size-exceeded` error. */\n maxSizeBytes?: number;\n /** `Cache-Control` max-age in seconds for uploaded objects. Default 3600. */\n cacheControl?: number;\n}\n\nexport type SupabaseMeta = {\n bucket: string;\n etag?: string;\n};\n\n/** The `GET /object/info` response (fields this driver reads). */\ninterface ObjectInfo {\n name?: string;\n size?: number | null;\n content_type?: string | null;\n etag?: string | null;\n last_modified?: string | null;\n metadata?: Record<string, string> | null;\n}\n\nexport class SupabaseDriver implements BaseDriver<SupabaseMeta> {\n readonly name = \"supabase\";\n private api: string;\n private bucket: string;\n private baseUrl: string;\n private headers: Record<string, string>;\n private maxSizeBytes: number | undefined;\n private cacheControl: string;\n\n constructor(config: SupabaseDriverConfig) {\n if (!config.url || !config.key || !config.bucket) {\n throw new StorageError(\"config\", \"SupabaseDriver requires url, key and bucket\", { provider: PROVIDER });\n }\n this.api = `${config.url.replace(/\\/+$/, \"\")}/storage/v1`;\n this.bucket = config.bucket;\n this.baseUrl = (config.baseUrl ?? `${this.api}/object/public/${config.bucket}`).replace(/\\/+$/, \"\");\n // New-style keys (sb_secret_..., sb_publishable_...) are not JWTs and go\n // only in `apikey`; legacy JWT keys and user tokens also go in Authorization.\n const bearer = config.accessToken ?? (config.key.startsWith(\"eyJ\") ? config.key : undefined);\n this.headers = { apikey: config.key, ...(bearer ? { authorization: `Bearer ${bearer}` } : {}) };\n this.maxSizeBytes = config.maxSizeBytes;\n this.cacheControl = `max-age=${config.cacheControl ?? 3600}`;\n }\n\n async upload(\n stream: ReadableStream<Uint8Array>,\n options: UploadOptions,\n context?: UploadContext,\n ): Promise<UploadResult<SupabaseMeta>> {\n validateUploadOptions(options);\n if (this.maxSizeBytes !== undefined && options.size !== undefined && options.size > this.maxSizeBytes) {\n throw new StorageError(\"size-exceeded\", `file size ${options.size} exceeds maxSizeBytes ${this.maxSizeBytes}`, { provider: PROVIDER });\n }\n\n const id = options.id ?? randomUUID();\n const path = this.pathFor({ ...options, id });\n const contentType = options.mimeType ?? \"application/octet-stream\";\n const metadata = { ...options.metadata, ...(context?.uploadToken ? { [TOKEN_KEY]: context.uploadToken } : {}) };\n const signal = options.signal;\n\n const reader = stream.getReader();\n let aborted = signal?.aborted ?? false;\n const onAbort = () => {\n aborted = true;\n // Cancelling the reader wakes any pending `read()` with `done: true`.\n reader.cancel().catch(() => {});\n };\n if (signal && !aborted) signal.addEventListener(\"abort\", onAbort, { once: true });\n\n let totalBytes = 0;\n const read = async (): Promise<Uint8Array | undefined> => {\n const { done, value } = await reader.read();\n if (aborted) throw abortError(\"upload aborted\");\n if (done) return undefined;\n totalBytes += value.byteLength;\n if (this.maxSizeBytes !== undefined && totalBytes > this.maxSizeBytes) {\n throw new StorageError(\"size-exceeded\", `upload exceeds maxSizeBytes ${this.maxSizeBytes}`, { provider: PROVIDER });\n }\n return value;\n };\n const checkSize = () => {\n if (options.size !== undefined && totalBytes !== options.size) {\n throw new ValidationError(`stream delivered ${totalBytes} bytes, expected ${options.size}`);\n }\n };\n\n let tusUrl: string | undefined;\n try {\n let etag: string | undefined;\n\n if (options.size !== undefined && options.size > TUS_CHUNK_SIZE) {\n // Known large size: resumable TUS upload, one 6 MiB chunk in memory at a time.\n tusUrl = await this.createTusUpload(path, options.size, contentType, metadata, signal);\n let buffer: Uint8Array[] = [];\n let bufferedLen = 0;\n let offset = 0;\n while (true) {\n const value = await read();\n if (totalBytes > options.size) checkSize();\n if (value) {\n buffer.push(value);\n bufferedLen += value.byteLength;\n }\n while (bufferedLen >= TUS_CHUNK_SIZE || (!value && bufferedLen > 0)) {\n if (!value) checkSize();\n const merged = concatBytes(buffer, bufferedLen);\n const take = Math.min(TUS_CHUNK_SIZE, bufferedLen);\n offset = await this.patchTus(tusUrl, merged.subarray(0, take), offset, signal);\n const rest = merged.subarray(take);\n buffer = [rest];\n bufferedLen = rest.byteLength;\n }\n if (!value) break;\n }\n checkSize();\n } else {\n // Read the first chunk; if the stream ends inside it, send one buffered request.\n const first: Uint8Array[] = [];\n let firstLen = 0;\n let ended = false;\n while (firstLen <= TUS_CHUNK_SIZE) {\n const value = await read();\n if (!value) {\n ended = true;\n break;\n }\n first.push(value);\n firstLen += value.byteLength;\n }\n const head = concatBytes(first, firstLen);\n\n if (ended) {\n checkSize();\n etag = await this.standardUpload(path, head, contentType, metadata, signal);\n } else {\n // A declared size this small cannot be followed by more data.\n checkSize();\n // Unknown size beyond one chunk: stream the rest in a single request,\n // so memory stays bounded without knowing the total in advance.\n const bodyError: { current?: unknown } = {};\n const body = new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(head);\n },\n async pull(controller) {\n try {\n const value = await read();\n if (value) controller.enqueue(value);\n else controller.close();\n } catch (err) {\n bodyError.current = err;\n controller.error(err);\n }\n },\n cancel(reason) {\n return reader.cancel(reason);\n },\n });\n try {\n etag = await this.standardUpload(path, body, contentType, metadata, signal);\n } catch (err) {\n // A body error (abort, size limit) surfaces as a generic fetch failure.\n if (bodyError.current) throw bodyError.current;\n throw err;\n }\n checkSize();\n }\n }\n\n return {\n id,\n url: `${this.baseUrl}/${urlEncodePath(path)}`,\n path,\n size: totalBytes,\n meta: { bucket: this.bucket, ...(etag ? { etag } : {}) },\n };\n } catch (err) {\n reader.cancel().catch(() => {});\n if (tusUrl) {\n // Best-effort: terminate the unfinished TUS upload.\n await fetch(tusUrl, { method: \"DELETE\", headers: { ...this.headers, \"tus-resumable\": TUS_VERSION } }).catch(() => {});\n }\n throw err;\n } finally {\n signal?.removeEventListener(\"abort\", onAbort);\n }\n }\n\n /** `POST /object/{bucket}/{path}`, overwriting any existing object. */\n private async standardUpload(\n path: string,\n body: Uint8Array | ReadableStream<Uint8Array>,\n contentType: string,\n metadata: Record<string, string>,\n signal: AbortSignal | undefined,\n ): Promise<string | undefined> {\n const res = await fetchWithAbort(this.objectUrl(\"object\", path), {\n method: \"POST\",\n headers: {\n ...this.headers,\n \"content-type\": contentType,\n \"cache-control\": this.cacheControl,\n \"x-upsert\": \"true\",\n \"x-metadata\": base64(JSON.stringify(metadata)),\n },\n body: body as BodyInit,\n // Node's fetch (undici) requires `duplex: \"half\"` for stream bodies;\n // other runtimes ignore it.\n duplex: \"half\",\n signal: signal ?? null,\n } as RequestInit);\n if (!res.ok) throw await supabaseError(\"Supabase upload failed\", res);\n return res.headers.get(\"etag\") ?? undefined;\n }\n\n /** Creates a TUS upload and returns its URL. */\n private async createTusUpload(\n path: string,\n size: number,\n contentType: string,\n metadata: Record<string, string>,\n signal: AbortSignal | undefined,\n ): Promise<string> {\n const endpoint = `${this.api}/upload/resumable`;\n const uploadMetadata = {\n bucketName: this.bucket,\n objectName: path,\n contentType,\n cacheControl: this.cacheControl.replace(\"max-age=\", \"\"),\n metadata: JSON.stringify(metadata),\n };\n const res = await fetchWithAbort(endpoint, {\n method: \"POST\",\n headers: {\n ...this.headers,\n \"tus-resumable\": TUS_VERSION,\n \"upload-length\": String(size),\n \"upload-metadata\": Object.entries(uploadMetadata)\n .map(([k, v]) => `${k} ${base64(v)}`)\n .join(\",\"),\n \"x-upsert\": \"true\",\n },\n signal: signal ?? null,\n });\n if (!res.ok) throw await supabaseError(\"Supabase resumable upload create failed\", res);\n const location = res.headers.get(\"location\");\n if (!location) {\n throw new StorageError(\"provider-error\", \"Supabase resumable upload returned no Location header\", { provider: PROVIDER });\n }\n return new URL(location, endpoint).href;\n }\n\n /** PATCHes `bytes` at `offset`, resending whatever the server did not store.\n * Returns the new offset. */\n private async patchTus(url: string, bytes: Uint8Array, offset: number, signal: AbortSignal | undefined): Promise<number> {\n const end = offset + bytes.byteLength;\n let current = offset;\n for (let attempt = 0; attempt <= MAX_CHUNK_RESENDS && current < end; attempt++) {\n const res = await fetchWithAbort(url, {\n method: \"PATCH\",\n headers: {\n ...this.headers,\n \"tus-resumable\": TUS_VERSION,\n \"upload-offset\": String(current),\n \"content-type\": \"application/offset+octet-stream\",\n },\n body: bytes.subarray(current - offset) as BodyInit,\n signal: signal ?? null,\n });\n if (!res.ok) throw await supabaseError(\"Supabase resumable upload chunk failed\", res);\n const next = Number(res.headers.get(\"upload-offset\") ?? end);\n if (next < current) break;\n current = next;\n }\n if (current < end) {\n throw new StorageError(\"provider-error\", \"Supabase did not store the uploaded chunk\", { provider: PROVIDER });\n }\n return current;\n }\n\n /** Deletes the object; a missing object is not an error. */\n async delete(path: string, options?: OperationOptions): Promise<void> {\n const res = await fetchWithAbort(`${this.api}/object/${encodeURIComponent(this.bucket)}`, {\n method: \"DELETE\",\n headers: { ...this.headers, \"content-type\": \"application/json\" },\n body: JSON.stringify({ prefixes: [path] }),\n signal: options?.signal ?? null,\n });\n if (!res.ok) throw await supabaseError(\"Supabase delete failed\", res);\n }\n\n async getUrl(path: string, _options?: OperationOptions): Promise<string> {\n return `${this.baseUrl}/${urlEncodePath(path)}`;\n }\n\n /** Streams the object. `options.signal` also cancels reading the body. */\n async get(path: string, options?: OperationOptions): Promise<ReadableStream<Uint8Array>> {\n const res = await fetchWithAbort(this.objectUrl(\"object/authenticated\", path), {\n headers: this.headers,\n signal: options?.signal ?? null,\n });\n if (!res.ok) throw await supabaseError(\"Supabase get failed\", res);\n if (!res.body) {\n throw new StorageError(\"provider-error\", \"Supabase get returned no response body\", { provider: PROVIDER });\n }\n return res.body;\n }\n\n /** A signed download URL for `path`, valid for `expiresIn` seconds (positive integer, default 3600). */\n async getPresignedUrl(path: string, options?: PresignedUrlOptions): Promise<string> {\n const expiresIn = options?.expiresIn ?? PRESIGN_DEFAULT_EXPIRY;\n if (!Number.isInteger(expiresIn) || expiresIn < 1) {\n throw new ValidationError(\"expiresIn must be a positive integer number of seconds\");\n }\n const res = await fetchWithAbort(this.objectUrl(\"object/sign\", path), {\n method: \"POST\",\n headers: { ...this.headers, \"content-type\": \"application/json\" },\n body: JSON.stringify({ expiresIn }),\n signal: options?.signal ?? null,\n });\n if (!res.ok) throw await supabaseError(\"Supabase sign failed\", res);\n const { signedURL } = (await res.json()) as { signedURL?: string };\n if (!signedURL) {\n throw new StorageError(\"provider-error\", \"Supabase sign returned no signedURL\", { provider: PROVIDER });\n }\n return `${this.api}${signedURL.startsWith(\"/\") ? \"\" : \"/\"}${signedURL}`;\n }\n\n /** The object name an upload with `options.id` is stored under: `<path>/<id>.<ext>`. */\n pathFor(options: UploadOptions & { id: string }): string {\n const ext = options.filename.includes(\".\") ? options.filename.split(\".\").pop()! : \"\";\n const filename = ext ? `${options.id}.${ext}` : options.id;\n return options.path ? `${options.path}/${filename}` : filename;\n }\n\n /** The object at `path` (`GET /object/info`), or `null` if it does not exist. */\n async head(path: string, options?: OperationOptions): Promise<StoredObject<SupabaseMeta> | null> {\n const res = await fetchWithAbort(this.objectUrl(\"object/info\", path), {\n headers: this.headers,\n signal: options?.signal ?? null,\n });\n if (!res.ok) {\n const error = await supabaseError(\"Supabase info failed\", res);\n if (error.code === \"not-found\") return null;\n throw error;\n }\n const info = (await res.json()) as ObjectInfo;\n const name = path.split(\"/\").pop() ?? path;\n const token = info.metadata?.[TOKEN_KEY];\n return {\n id: name.includes(\".\") ? name.slice(0, name.lastIndexOf(\".\")) : name,\n url: `${this.baseUrl}/${urlEncodePath(path)}`,\n path,\n size: Number(info.size ?? 0),\n meta: { bucket: this.bucket, ...(info.etag ? { etag: info.etag } : {}) },\n ...(token ? { uploadToken: token } : {}),\n ...(info.last_modified ? { lastModified: new Date(info.last_modified) } : {}),\n ...(info.content_type ? { contentType: info.content_type } : {}),\n };\n }\n\n private objectUrl(route: string, path: string): string {\n return `${this.api}/${route}/${encodeURIComponent(this.bucket)}/${urlEncodePath(path)}`;\n }\n}\n\n/** `fetch` that converts non-abort failures into a typed `network` error,\n * while letting aborts (`AbortError`) pass through untouched. */\nasync function fetchWithAbort(url: string | URL, init: RequestInit): Promise<Response> {\n try {\n return await fetch(url, init);\n } catch (err) {\n if (isAbortError(err)) throw err;\n const cause = err instanceof Error ? err : new Error(String(err));\n throw new StorageError(\"network\", `${PROVIDER} network error: ${cause.message}`, { provider: PROVIDER, cause });\n }\n}\n\nasync function supabaseError(prefix: string, res: Response): Promise<StorageError> {\n const body = await res.text().catch(() => \"\");\n return new StorageError(\n classifySupabaseError(res.status, body),\n `${prefix}: ${res.status} ${res.statusText}${body ? `: ${body}` : \"\"}`,\n { statusCode: res.status, provider: PROVIDER },\n );\n}\n\nfunction base64(value: string): string {\n let binary = \"\";\n for (const byte of new TextEncoder().encode(value)) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\nfunction concatBytes(chunks: Uint8Array[], total: number): Uint8Array {\n const out = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return out;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,48 @@
1
1
  {
2
2
  "name": "@fileway/supabase",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.5.0",
4
+ "license": "MIT",
5
+ "keywords": [
6
+ "fileway",
7
+ "file-upload",
8
+ "storage",
9
+ "supabase",
10
+ "supabase-storage",
11
+ "tus",
12
+ "stream"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/Goldenhub/fileway.git"
17
+ },
18
+ "type": "module",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "worker": "./dist/index.js",
24
+ "deno": "./dist/index.js",
25
+ "bun": "./dist/index.js",
26
+ "import": "./dist/index.js",
27
+ "require": "./dist/index.cjs"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "dependencies": {
37
+ "@fileway/core": "0.5.0"
38
+ },
39
+ "devDependencies": {
40
+ "tsup": "^8.0.0",
41
+ "typescript": "^5.5.0",
42
+ "vitest": "^1.6.0"
43
+ },
44
+ "scripts": {
45
+ "build": "tsup",
46
+ "test": "vitest run"
47
+ }
6
48
  }