@velajs/storage 0.2.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +56 -0
  4. package/dist/base64.d.ts +8 -0
  5. package/dist/base64.js +25 -0
  6. package/dist/client/index.d.ts +50 -0
  7. package/dist/client/index.js +243 -0
  8. package/dist/decorators/inject-storage.decorator.d.ts +17 -0
  9. package/dist/decorators/inject-storage.decorator.js +21 -0
  10. package/dist/drivers/memory/index.d.ts +16 -0
  11. package/dist/drivers/memory/index.js +202 -0
  12. package/dist/drivers/r2/index.d.ts +11 -0
  13. package/dist/drivers/r2/index.js +166 -0
  14. package/dist/drivers/r2/r2.types.d.ts +67 -0
  15. package/dist/drivers/r2/r2.types.js +5 -0
  16. package/dist/drivers/r2-http/index.d.ts +33 -0
  17. package/dist/drivers/r2-http/index.js +52 -0
  18. package/dist/drivers/s3/index.d.ts +5 -0
  19. package/dist/drivers/s3/index.js +3 -0
  20. package/dist/drivers/s3/s3-client.d.ts +33 -0
  21. package/dist/drivers/s3/s3-client.js +154 -0
  22. package/dist/drivers/s3/s3.driver.d.ts +4 -0
  23. package/dist/drivers/s3/s3.driver.js +404 -0
  24. package/dist/drivers/s3/s3.types.d.ts +28 -0
  25. package/dist/drivers/s3/s3.types.js +1 -0
  26. package/dist/drivers/s3/xml.d.ts +6 -0
  27. package/dist/drivers/s3/xml.js +52 -0
  28. package/dist/http/authorizer.types.d.ts +59 -0
  29. package/dist/http/authorizer.types.js +1 -0
  30. package/dist/http/http-helpers.d.ts +22 -0
  31. package/dist/http/http-helpers.js +80 -0
  32. package/dist/http/protocol.types.d.ts +76 -0
  33. package/dist/http/protocol.types.js +1 -0
  34. package/dist/index.d.ts +17 -0
  35. package/dist/index.js +16 -0
  36. package/dist/internal/body.d.ts +18 -0
  37. package/dist/internal/body.js +90 -0
  38. package/dist/internal/retry.d.ts +41 -0
  39. package/dist/internal/retry.js +127 -0
  40. package/dist/internal/stored-file.d.ts +27 -0
  41. package/dist/internal/stored-file.js +114 -0
  42. package/dist/middleware/cache.d.ts +50 -0
  43. package/dist/middleware/cache.js +101 -0
  44. package/dist/middleware/compose.d.ts +12 -0
  45. package/dist/middleware/compose.js +11 -0
  46. package/dist/middleware/compression.d.ts +16 -0
  47. package/dist/middleware/compression.js +101 -0
  48. package/dist/middleware/encryption.d.ts +14 -0
  49. package/dist/middleware/encryption.js +134 -0
  50. package/dist/middleware/failover.d.ts +18 -0
  51. package/dist/middleware/failover.js +52 -0
  52. package/dist/middleware/index.d.ts +15 -0
  53. package/dist/middleware/index.js +8 -0
  54. package/dist/middleware/retry.d.ts +14 -0
  55. package/dist/middleware/retry.js +47 -0
  56. package/dist/middleware/versioning.d.ts +36 -0
  57. package/dist/middleware/versioning.js +89 -0
  58. package/dist/middleware/wrap.d.ts +11 -0
  59. package/dist/middleware/wrap.js +46 -0
  60. package/dist/object-key.d.ts +17 -0
  61. package/dist/object-key.js +42 -0
  62. package/dist/storage.controller.d.ts +21 -0
  63. package/dist/storage.controller.js +380 -0
  64. package/dist/storage.error.d.ts +28 -0
  65. package/dist/storage.error.js +46 -0
  66. package/dist/storage.facade.d.ts +47 -0
  67. package/dist/storage.facade.js +443 -0
  68. package/dist/storage.module.d.ts +46 -0
  69. package/dist/storage.module.js +113 -0
  70. package/dist/storage.service.d.ts +42 -0
  71. package/dist/storage.service.js +86 -0
  72. package/dist/storage.tokens.d.ts +13 -0
  73. package/dist/storage.tokens.js +26 -0
  74. package/dist/storage.types.d.ts +245 -0
  75. package/dist/storage.types.js +1 -0
  76. package/dist/storagesdk/index.d.ts +85 -0
  77. package/dist/storagesdk/index.js +136 -0
  78. package/package.json +116 -0
@@ -0,0 +1,89 @@
1
+ import { StorageError } from "../storage.error.js";
2
+ import { passthrough } from "./wrap.js";
3
+ export const VERSIONED = Symbol.for('vela.storage.versioned');
4
+ /**
5
+ * Keeps prior versions on overwrite via key-suffixing (no external manifest →
6
+ * no read-modify-write race). Snapshots are cheap when the base driver
7
+ * `supportsServerSideCopy`. Direct presigned uploads bypass snapshotting, so
8
+ * `signedUploadUrl` throws. Reach `listVersions`/`restore`/`deleteVersion` via
9
+ * {@link unwrapVersioning}.
10
+ */ export function versioning(opts = {}) {
11
+ const prefix = opts.prefix ?? '.vela-versions/';
12
+ let seq = 0;
13
+ const vid = opts.versionId ?? (()=>`${Date.now().toString(36)}-${(seq++).toString(36)}`);
14
+ const vkey = (key, id)=>`${prefix}${key}/${id}`;
15
+ const isVersion = (key)=>key.startsWith(prefix);
16
+ const listVersions = async (inner, key)=>{
17
+ const res = await inner.list({
18
+ prefix: `${prefix}${key}/`
19
+ });
20
+ return res.items.map((item)=>({
21
+ versionId: item.key.slice(item.key.lastIndexOf('/') + 1),
22
+ key: item.key,
23
+ size: item.size,
24
+ lastModified: item.lastModified
25
+ }));
26
+ };
27
+ const snapshot = async (inner, key)=>{
28
+ if (!await inner.exists(key)) return;
29
+ const id = vid();
30
+ if (inner.supportsServerSideCopy) {
31
+ await inner.copy(key, vkey(key, id));
32
+ } else {
33
+ const current = await inner.download(key);
34
+ await inner.upload(vkey(key, id), current.stream(), {
35
+ contentType: current.type
36
+ });
37
+ }
38
+ if (opts.maxVersions) {
39
+ const versions = (await listVersions(inner, key)).sort((a, b)=>a.versionId < b.versionId ? -1 : 1);
40
+ for (const v of versions.slice(0, Math.max(0, versions.length - opts.maxVersions))){
41
+ await inner.delete(v.key);
42
+ }
43
+ }
44
+ };
45
+ return (inner)=>{
46
+ const driver = passthrough(inner, {
47
+ async upload (k, b, o) {
48
+ await snapshot(inner, k);
49
+ return inner.upload(k, b, o);
50
+ },
51
+ async copy (f, t, o) {
52
+ await snapshot(inner, t);
53
+ return inner.copy(f, t, o);
54
+ },
55
+ async delete (k, o) {
56
+ if (opts.onDelete !== 'purge') await snapshot(inner, k);
57
+ await inner.delete(k, o);
58
+ if (opts.onDelete === 'purge') {
59
+ for (const v of (await listVersions(inner, k)))await inner.delete(v.key);
60
+ }
61
+ },
62
+ list: opts.hideFromList === false ? undefined : async (o)=>{
63
+ const res = await inner.list(o);
64
+ return {
65
+ items: res.items.filter((i)=>!isVersion(i.key)),
66
+ prefixes: res.prefixes?.filter((p)=>!isVersion(p)),
67
+ cursor: res.cursor
68
+ };
69
+ },
70
+ signedUploadUrl () {
71
+ throw new StorageError('Unsupported', 'versioning: direct uploads bypass snapshots');
72
+ }
73
+ });
74
+ const versioned = {
75
+ listVersions: (key)=>listVersions(inner, key),
76
+ async restore (key, versionId) {
77
+ await snapshot(inner, key);
78
+ await inner.copy(vkey(key, versionId), key);
79
+ },
80
+ deleteVersion: (key, versionId)=>inner.delete(vkey(key, versionId))
81
+ };
82
+ return Object.assign(driver, {
83
+ [VERSIONED]: versioned
84
+ });
85
+ };
86
+ }
87
+ /** Retrieve the {@link Versioned} surface from a versioning-wrapped driver (or `undefined`). */ export function unwrapVersioning(driver) {
88
+ return driver[VERSIONED];
89
+ }
@@ -0,0 +1,11 @@
1
+ import type { StorageDriver } from '../storage.types';
2
+ /** A driver middleware: wraps a driver, returns a driver. */
3
+ export type Middleware = (inner: StorageDriver) => StorageDriver;
4
+ /**
5
+ * Build a driver that delegates to `inner`, applying `over` overrides and
6
+ * omitting the methods in `omit`. Explicit delegation (not spread) keeps
7
+ * class-based drivers' `this`. Optional methods are forwarded only when the
8
+ * inner driver has them (so `presence == capability` is preserved), unless
9
+ * omitted (e.g. a body-transforming middleware suppresses multipart/presign).
10
+ */
11
+ export declare function passthrough(inner: StorageDriver, over?: Partial<StorageDriver>, omit?: ReadonlyArray<keyof StorageDriver>): StorageDriver;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Build a driver that delegates to `inner`, applying `over` overrides and
3
+ * omitting the methods in `omit`. Explicit delegation (not spread) keeps
4
+ * class-based drivers' `this`. Optional methods are forwarded only when the
5
+ * inner driver has them (so `presence == capability` is preserved), unless
6
+ * omitted (e.g. a body-transforming middleware suppresses multipart/presign).
7
+ */ export function passthrough(inner, over = {}, omit = []) {
8
+ const omitted = new Set(omit);
9
+ const has = (k)=>!omitted.has(k);
10
+ const d = {
11
+ name: over.name ?? inner.name,
12
+ raw: inner.raw,
13
+ reportsUploadProgress: over.reportsUploadProgress ?? inner.reportsUploadProgress,
14
+ supportsRange: over.supportsRange ?? inner.supportsRange,
15
+ supportsDelimiter: over.supportsDelimiter ?? inner.supportsDelimiter,
16
+ supportsMetadata: over.supportsMetadata ?? inner.supportsMetadata,
17
+ supportsCacheControl: over.supportsCacheControl ?? inner.supportsCacheControl,
18
+ supportsServerSideCopy: over.supportsServerSideCopy ?? inner.supportsServerSideCopy,
19
+ signedUrl: over.signedUrl ?? inner.signedUrl,
20
+ upload: over.upload ?? ((k, b, o)=>inner.upload(k, b, o)),
21
+ download: over.download ?? ((k, o)=>inner.download(k, o)),
22
+ head: over.head ?? ((k, o)=>inner.head(k, o)),
23
+ exists: over.exists ?? ((k, o)=>inner.exists(k, o)),
24
+ delete: over.delete ?? ((k, o)=>inner.delete(k, o)),
25
+ copy: over.copy ?? ((f, t, o)=>inner.copy(f, t, o)),
26
+ list: over.list ?? ((o)=>inner.list(o)),
27
+ url: over.url ?? ((k, o)=>inner.url(k, o)),
28
+ signedUploadUrl: over.signedUploadUrl ?? ((k, o)=>inner.signedUploadUrl(k, o))
29
+ };
30
+ if (has('deleteMany') && (over.deleteMany || inner.deleteMany)) {
31
+ d.deleteMany = over.deleteMany ?? ((ks, o)=>inner.deleteMany(ks, o));
32
+ }
33
+ if (has('move') && (over.move || inner.move)) {
34
+ d.move = over.move ?? ((f, t, o)=>inner.move(f, t, o));
35
+ }
36
+ if (has('createMultipartUpload') && (over.createMultipartUpload || inner.createMultipartUpload)) {
37
+ d.createMultipartUpload = over.createMultipartUpload ?? ((k, o)=>inner.createMultipartUpload(k, o));
38
+ }
39
+ if (has('resumeMultipartUpload') && (over.resumeMultipartUpload || inner.resumeMultipartUpload)) {
40
+ d.resumeMultipartUpload = over.resumeMultipartUpload ?? ((k, u)=>inner.resumeMultipartUpload(k, u));
41
+ }
42
+ if (has('signedMultipart') && (over.signedMultipart || inner.signedMultipart)) {
43
+ d.signedMultipart = over.signedMultipart ?? inner.signedMultipart;
44
+ }
45
+ return d;
46
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * True when `key` is a safe object key:
3
+ * - non-empty, not longer than 1024 bytes (S3's limit)
4
+ * - no leading `/`
5
+ * - no `.` / `..` path segments (traversal)
6
+ * - no control characters
7
+ * - no backslashes (Windows-style separators)
8
+ */
9
+ export declare function isSafeKey(key: string): boolean;
10
+ /** Return `key` if safe, else throw `StorageError('InvalidKey')`. */
11
+ export declare function sanitizeKey(key: string): string;
12
+ /** Normalize a prefix: drop leading/trailing slashes, collapse to '' when empty. */
13
+ export declare function normalizePrefix(prefix: string | undefined): string;
14
+ /** Join a normalized prefix and a key with a single '/'. */
15
+ export declare function joinKey(prefix: string, key: string): string;
16
+ /** Strip a normalized prefix from a stored key (for surfacing to callers). */
17
+ export declare function stripPrefix(prefix: string, key: string): string;
@@ -0,0 +1,42 @@
1
+ import { StorageError } from "./storage.error.js";
2
+ // Object-key hygiene shared by the facade (prefix scoping) and the HTTP
3
+ // controller (post-authorize re-sanitization). The rules block the classic
4
+ // traversal / injection footguns without being so strict that legitimate
5
+ // keys (nested "folders", dots, spaces) are rejected.
6
+ // Control characters: C0 range (0x00-0x1F) plus DEL (0x7F).
7
+ const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
8
+ /**
9
+ * True when `key` is a safe object key:
10
+ * - non-empty, not longer than 1024 bytes (S3's limit)
11
+ * - no leading `/`
12
+ * - no `.` / `..` path segments (traversal)
13
+ * - no control characters
14
+ * - no backslashes (Windows-style separators)
15
+ */ export function isSafeKey(key) {
16
+ if (typeof key !== 'string' || key.length === 0 || key.length > 1024) return false;
17
+ if (key.startsWith('/')) return false;
18
+ if (key.includes('\\')) return false;
19
+ if (CONTROL_CHARS.test(key)) return false;
20
+ for (const segment of key.split('/')){
21
+ if (segment === '.' || segment === '..') return false;
22
+ }
23
+ return true;
24
+ }
25
+ /** Return `key` if safe, else throw `StorageError('InvalidKey')`. */ export function sanitizeKey(key) {
26
+ if (!isSafeKey(key)) {
27
+ throw new StorageError('InvalidKey', `unsafe object key: ${JSON.stringify(key)}`);
28
+ }
29
+ return key;
30
+ }
31
+ /** Normalize a prefix: drop leading/trailing slashes, collapse to '' when empty. */ export function normalizePrefix(prefix) {
32
+ if (!prefix) return '';
33
+ return prefix.replace(/^\/+/, '').replace(/\/+$/, '');
34
+ }
35
+ /** Join a normalized prefix and a key with a single '/'. */ export function joinKey(prefix, key) {
36
+ return prefix ? `${prefix}/${key}` : key;
37
+ }
38
+ /** Strip a normalized prefix from a stored key (for surfacing to callers). */ export function stripPrefix(prefix, key) {
39
+ if (!prefix) return key;
40
+ const withSlash = `${prefix}/`;
41
+ return key.startsWith(withSlash) ? key.slice(withSlash.length) : key;
42
+ }
@@ -0,0 +1,21 @@
1
+ import { type Token, type Type } from '@velajs/vela';
2
+ import type { StorageService } from './storage.service';
3
+ import type { StorageAuthorizer } from './http/authorizer.types';
4
+ export interface ResolvedHttpOptions {
5
+ driverName: string;
6
+ authorize?: StorageAuthorizer;
7
+ defaultPolicy: 'deny' | 'allow';
8
+ download: 'redirect' | 'proxy';
9
+ defaultExpiresIn: number;
10
+ maxExpiresIn: number;
11
+ maxUploadSize?: number;
12
+ maxListLimit: number;
13
+ deleteConcurrency: number;
14
+ }
15
+ /**
16
+ * Build a vela controller exposing presigned/multipart upload + download
17
+ * endpoints for one bucket. Factory-decorated (like better-auth's catch-all)
18
+ * so `basePath` bakes in at decoration time. Consumes the {@link StorageService}
19
+ * facade so gating/retry/prefix apply uniformly on the HTTP path.
20
+ */
21
+ export declare function createStorageController(basePath: string, serviceToken: Token<StorageService>, http: ResolvedHttpOptions): Type;
@@ -0,0 +1,380 @@
1
+ function _ts_decorate(decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ }
7
+ function _ts_metadata(k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ }
10
+ function _ts_param(paramIndex, decorator) {
11
+ return function(target, key) {
12
+ decorator(target, key, paramIndex);
13
+ };
14
+ }
15
+ import { Controller, Get, Inject, Injectable, Post, Req } from "@velajs/vela";
16
+ import { sanitizeKey } from "./object-key.js";
17
+ import { StorageError } from "./storage.error.js";
18
+ import { clampExpiry, dispositionHeader, mapError, parseRange, serializeStored } from "./http/http-helpers.js";
19
+ /**
20
+ * Build a vela controller exposing presigned/multipart upload + download
21
+ * endpoints for one bucket. Factory-decorated (like better-auth's catch-all)
22
+ * so `basePath` bakes in at decoration time. Consumes the {@link StorageService}
23
+ * facade so gating/retry/prefix apply uniformly on the HTTP path.
24
+ */ export function createStorageController(basePath, serviceToken, http) {
25
+ let StorageController = class StorageController {
26
+ svc;
27
+ constructor(svc){
28
+ this.svc = svc;
29
+ }
30
+ get storage() {
31
+ return this.svc.storage;
32
+ }
33
+ async authorize(c, action) {
34
+ if (!http.authorize) {
35
+ if (http.defaultPolicy === 'allow') return {};
36
+ throw new StorageError('AccessDenied', 'storage: no authorizer configured (default deny)');
37
+ }
38
+ const res = await http.authorize(action, {
39
+ req: c.req.raw,
40
+ ctx: c,
41
+ driver: http.driverName
42
+ });
43
+ if (res === false) throw new StorageError('AccessDenied', 'not allowed');
44
+ if (res === true) return {};
45
+ return res;
46
+ }
47
+ fail(c, e) {
48
+ const err = e instanceof StorageError ? e : StorageError.wrap(e);
49
+ const { wire, status } = mapError(err.code);
50
+ return c.json({
51
+ error: {
52
+ code: wire,
53
+ message: err.message
54
+ }
55
+ }, status);
56
+ }
57
+ async signUpload(c) {
58
+ try {
59
+ const b = await c.req.json();
60
+ const ov = await this.authorize(c, {
61
+ type: 'sign-upload',
62
+ key: b.key,
63
+ contentType: b.contentType,
64
+ size: b.size
65
+ });
66
+ const key = sanitizeKey(ov.key ?? b.key);
67
+ const upload = await this.storage.signedUploadUrl(key, {
68
+ expiresIn: clampExpiry(ov.expiresIn ?? b.expiresIn, http.defaultExpiresIn, http.maxExpiresIn),
69
+ contentType: b.contentType,
70
+ maxSize: ov.maxSize ?? http.maxUploadSize
71
+ });
72
+ return c.json({
73
+ key,
74
+ upload
75
+ });
76
+ } catch (e) {
77
+ return this.fail(c, e);
78
+ }
79
+ }
80
+ async multipartCreate(c) {
81
+ try {
82
+ const b = await c.req.json();
83
+ const ov = await this.authorize(c, {
84
+ type: 'multipart-create',
85
+ key: b.key,
86
+ contentType: b.contentType
87
+ });
88
+ const key = sanitizeKey(ov.key ?? b.key);
89
+ const sm = this.storage.signedMultipart;
90
+ if (!sm) throw new StorageError('Unsupported', 'presigned multipart not supported');
91
+ const created = await sm.create(key, {
92
+ contentType: b.contentType,
93
+ metadata: ov.metadata ?? b.metadata,
94
+ partSize: b.partSize
95
+ });
96
+ return c.json({
97
+ key,
98
+ uploadId: created.uploadId,
99
+ partSize: created.partSize
100
+ });
101
+ } catch (e) {
102
+ return this.fail(c, e);
103
+ }
104
+ }
105
+ async multipartSignPart(c) {
106
+ try {
107
+ const b = await c.req.json();
108
+ const ov = await this.authorize(c, {
109
+ type: 'multipart-sign-part',
110
+ key: b.key,
111
+ uploadId: b.uploadId,
112
+ partNumber: b.partNumber
113
+ });
114
+ const key = sanitizeKey(ov.key ?? b.key);
115
+ const sm = this.storage.signedMultipart;
116
+ if (!sm) throw new StorageError('Unsupported', 'presigned multipart not supported');
117
+ const part = await sm.signPart(key, b.uploadId, b.partNumber, {
118
+ expiresIn: clampExpiry(ov.expiresIn, http.defaultExpiresIn, http.maxExpiresIn)
119
+ });
120
+ return c.json({
121
+ partNumber: b.partNumber,
122
+ method: 'PUT',
123
+ url: part.url,
124
+ headers: part.headers
125
+ });
126
+ } catch (e) {
127
+ return this.fail(c, e);
128
+ }
129
+ }
130
+ async multipartComplete(c) {
131
+ try {
132
+ const b = await c.req.json();
133
+ const ov = await this.authorize(c, {
134
+ type: 'multipart-complete',
135
+ key: b.key,
136
+ uploadId: b.uploadId
137
+ });
138
+ const key = sanitizeKey(ov.key ?? b.key);
139
+ const sm = this.storage.signedMultipart;
140
+ if (!sm) throw new StorageError('Unsupported', 'presigned multipart not supported');
141
+ return c.json(await sm.complete(key, b.uploadId, b.parts));
142
+ } catch (e) {
143
+ return this.fail(c, e);
144
+ }
145
+ }
146
+ async multipartAbort(c) {
147
+ try {
148
+ const b = await c.req.json();
149
+ const ov = await this.authorize(c, {
150
+ type: 'multipart-abort',
151
+ key: b.key,
152
+ uploadId: b.uploadId
153
+ });
154
+ const key = sanitizeKey(ov.key ?? b.key);
155
+ const sm = this.storage.signedMultipart;
156
+ if (!sm) throw new StorageError('Unsupported', 'presigned multipart not supported');
157
+ await sm.abort(key, b.uploadId);
158
+ return c.json({
159
+ ok: true
160
+ });
161
+ } catch (e) {
162
+ return this.fail(c, e);
163
+ }
164
+ }
165
+ async list(c) {
166
+ try {
167
+ const prefix = c.req.query('prefix');
168
+ const ov = await this.authorize(c, {
169
+ type: 'list',
170
+ prefix
171
+ });
172
+ const limit = c.req.query('limit');
173
+ const res = await this.storage.list({
174
+ prefix: ov.prefix ?? prefix,
175
+ cursor: c.req.query('cursor'),
176
+ delimiter: c.req.query('delimiter'),
177
+ limit: limit ? Math.min(Number(limit), http.maxListLimit) : undefined
178
+ });
179
+ return c.json({
180
+ items: res.items.map(serializeStored),
181
+ prefixes: res.prefixes,
182
+ cursor: res.cursor
183
+ });
184
+ } catch (e) {
185
+ return this.fail(c, e);
186
+ }
187
+ }
188
+ async head(c) {
189
+ try {
190
+ const key = sanitizeKey(c.req.query('key') ?? '');
191
+ const ov = await this.authorize(c, {
192
+ type: 'head',
193
+ key
194
+ });
195
+ const file = await this.storage.head(sanitizeKey(ov.key ?? key));
196
+ return c.json(serializeStored(file));
197
+ } catch (e) {
198
+ return this.fail(c, e);
199
+ }
200
+ }
201
+ async delete(c) {
202
+ try {
203
+ const b = await c.req.json();
204
+ const keys = b.keys.map((k)=>sanitizeKey(k));
205
+ const ov = await this.authorize(c, {
206
+ type: 'delete',
207
+ keys
208
+ });
209
+ const effective = (ov.keys ?? keys).map((k)=>sanitizeKey(k));
210
+ return c.json(await this.storage.delete(effective, {
211
+ concurrency: http.deleteConcurrency
212
+ }));
213
+ } catch (e) {
214
+ return this.fail(c, e);
215
+ }
216
+ }
217
+ async download(c) {
218
+ try {
219
+ const key = sanitizeKey(c.req.query('key') ?? '');
220
+ const disp = c.req.query('disposition');
221
+ const ov = await this.authorize(c, {
222
+ type: 'download',
223
+ key
224
+ });
225
+ const ekey = sanitizeKey(ov.key ?? key);
226
+ const name = ekey.slice(ekey.lastIndexOf('/') + 1);
227
+ if (http.download === 'redirect' && this.storage.capabilities.signedUrl.supported) {
228
+ const url = await this.storage.url(ekey, {
229
+ expiresIn: clampExpiry(ov.expiresIn, http.defaultExpiresIn, http.maxExpiresIn),
230
+ responseContentDisposition: dispositionHeader(disp, name)
231
+ });
232
+ return c.redirect(url, 302);
233
+ }
234
+ const range = parseRange(c.req.header('range'));
235
+ const file = await this.storage.download(ekey, range ? {
236
+ range
237
+ } : undefined);
238
+ const headers = new Headers({
239
+ 'content-type': file.type,
240
+ 'content-length': String(file.size),
241
+ 'content-disposition': dispositionHeader(disp, file.name)
242
+ });
243
+ if (file.etag) headers.set('etag', file.etag);
244
+ if (range) {
245
+ headers.set('content-range', `bytes ${range.start}-${range.start + file.size - 1}/*`);
246
+ return new Response(file.stream(), {
247
+ status: 206,
248
+ headers
249
+ });
250
+ }
251
+ return new Response(file.stream(), {
252
+ status: 200,
253
+ headers
254
+ });
255
+ } catch (e) {
256
+ return this.fail(c, e);
257
+ }
258
+ }
259
+ async signDownload(c) {
260
+ try {
261
+ const b = await c.req.json();
262
+ const ov = await this.authorize(c, {
263
+ type: 'download',
264
+ key: b.key
265
+ });
266
+ const key = sanitizeKey(ov.key ?? b.key);
267
+ const expiresIn = clampExpiry(ov.expiresIn ?? b.expiresIn, http.defaultExpiresIn, http.maxExpiresIn);
268
+ const url = await this.storage.url(key, {
269
+ expiresIn
270
+ });
271
+ return c.json({
272
+ url,
273
+ expiresAt: Date.now() + expiresIn * 1000
274
+ });
275
+ } catch (e) {
276
+ return this.fail(c, e);
277
+ }
278
+ }
279
+ };
280
+ _ts_decorate([
281
+ Post('/sign-upload'),
282
+ _ts_param(0, Req()),
283
+ _ts_metadata("design:type", Function),
284
+ _ts_metadata("design:paramtypes", [
285
+ typeof Context === "undefined" ? Object : Context
286
+ ]),
287
+ _ts_metadata("design:returntype", Promise)
288
+ ], StorageController.prototype, "signUpload", null);
289
+ _ts_decorate([
290
+ Post('/multipart/create'),
291
+ _ts_param(0, Req()),
292
+ _ts_metadata("design:type", Function),
293
+ _ts_metadata("design:paramtypes", [
294
+ typeof Context === "undefined" ? Object : Context
295
+ ]),
296
+ _ts_metadata("design:returntype", Promise)
297
+ ], StorageController.prototype, "multipartCreate", null);
298
+ _ts_decorate([
299
+ Post('/multipart/sign-part'),
300
+ _ts_param(0, Req()),
301
+ _ts_metadata("design:type", Function),
302
+ _ts_metadata("design:paramtypes", [
303
+ typeof Context === "undefined" ? Object : Context
304
+ ]),
305
+ _ts_metadata("design:returntype", Promise)
306
+ ], StorageController.prototype, "multipartSignPart", null);
307
+ _ts_decorate([
308
+ Post('/multipart/complete'),
309
+ _ts_param(0, Req()),
310
+ _ts_metadata("design:type", Function),
311
+ _ts_metadata("design:paramtypes", [
312
+ typeof Context === "undefined" ? Object : Context
313
+ ]),
314
+ _ts_metadata("design:returntype", Promise)
315
+ ], StorageController.prototype, "multipartComplete", null);
316
+ _ts_decorate([
317
+ Post('/multipart/abort'),
318
+ _ts_param(0, Req()),
319
+ _ts_metadata("design:type", Function),
320
+ _ts_metadata("design:paramtypes", [
321
+ typeof Context === "undefined" ? Object : Context
322
+ ]),
323
+ _ts_metadata("design:returntype", Promise)
324
+ ], StorageController.prototype, "multipartAbort", null);
325
+ _ts_decorate([
326
+ Get('/list'),
327
+ _ts_param(0, Req()),
328
+ _ts_metadata("design:type", Function),
329
+ _ts_metadata("design:paramtypes", [
330
+ typeof Context === "undefined" ? Object : Context
331
+ ]),
332
+ _ts_metadata("design:returntype", Promise)
333
+ ], StorageController.prototype, "list", null);
334
+ _ts_decorate([
335
+ Get('/head'),
336
+ _ts_param(0, Req()),
337
+ _ts_metadata("design:type", Function),
338
+ _ts_metadata("design:paramtypes", [
339
+ typeof Context === "undefined" ? Object : Context
340
+ ]),
341
+ _ts_metadata("design:returntype", Promise)
342
+ ], StorageController.prototype, "head", null);
343
+ _ts_decorate([
344
+ Post('/delete'),
345
+ _ts_param(0, Req()),
346
+ _ts_metadata("design:type", Function),
347
+ _ts_metadata("design:paramtypes", [
348
+ typeof Context === "undefined" ? Object : Context
349
+ ]),
350
+ _ts_metadata("design:returntype", Promise)
351
+ ], StorageController.prototype, "delete", null);
352
+ _ts_decorate([
353
+ Get('/download'),
354
+ _ts_param(0, Req()),
355
+ _ts_metadata("design:type", Function),
356
+ _ts_metadata("design:paramtypes", [
357
+ typeof Context === "undefined" ? Object : Context
358
+ ]),
359
+ _ts_metadata("design:returntype", Promise)
360
+ ], StorageController.prototype, "download", null);
361
+ _ts_decorate([
362
+ Post('/sign-download'),
363
+ _ts_param(0, Req()),
364
+ _ts_metadata("design:type", Function),
365
+ _ts_metadata("design:paramtypes", [
366
+ typeof Context === "undefined" ? Object : Context
367
+ ]),
368
+ _ts_metadata("design:returntype", Promise)
369
+ ], StorageController.prototype, "signDownload", null);
370
+ StorageController = _ts_decorate([
371
+ Controller(basePath),
372
+ Injectable(),
373
+ _ts_param(0, Inject(serviceToken)),
374
+ _ts_metadata("design:type", Function),
375
+ _ts_metadata("design:paramtypes", [
376
+ typeof StorageService === "undefined" ? Object : StorageService
377
+ ])
378
+ ], StorageController);
379
+ return StorageController;
380
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The single error taxonomy for `@velajs/storage`. Every driver maps its
3
+ * provider/transport failures onto a {@link StorageErrorCode}; the facade's
4
+ * capability gates throw `Unsupported`; the retry loop keys off
5
+ * {@link StorageError.retryable}; and the HTTP controller maps codes to
6
+ * status via its own `codeToHttp()` table.
7
+ */
8
+ export type StorageErrorCode = 'NotFound' | 'AccessDenied' | 'Unsupported' | 'ReadOnly' | 'InvalidKey' | 'InvalidRequest' | 'Conflict' | 'RateLimited' | 'Network' | 'Timeout' | 'Aborted' | 'Provider' | 'Parse';
9
+ export interface StorageErrorOptions {
10
+ cause?: unknown;
11
+ /** Underlying HTTP status, when the failure came from a transport response. */
12
+ status?: number;
13
+ /** Override the default retryability implied by {@link StorageErrorCode}. */
14
+ retryable?: boolean;
15
+ }
16
+ export declare class StorageError extends Error {
17
+ name: string;
18
+ readonly code: StorageErrorCode;
19
+ readonly retryable: boolean;
20
+ readonly status: number | undefined;
21
+ constructor(code: StorageErrorCode, message: string, options?: StorageErrorOptions);
22
+ /** Wrap an arbitrary thrown value as a `StorageError` (idempotent). */
23
+ static wrap(e: unknown): StorageError;
24
+ /** Map a transport status code to a `StorageError` (5xx/429 retryable). */
25
+ static fromStatus(status: number, message: string, cause?: unknown): StorageError;
26
+ }
27
+ /** True for `AbortError`-shaped values (DOMException or `{ name: 'AbortError' }`). */
28
+ export declare function isAbort(e: unknown): boolean;