@velajs/storage 0.3.1 → 0.3.2

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 (99) hide show
  1. package/CHANGELOG.md +6 -1
  2. package/dist/body-BMUcBeQ0.js +93 -0
  3. package/dist/body-BMUcBeQ0.js.map +1 -0
  4. package/dist/client/index.d.ts +49 -46
  5. package/dist/client/index.js +207 -228
  6. package/dist/client/index.js.map +1 -0
  7. package/dist/drivers/memory/index.d.ts +17 -13
  8. package/dist/drivers/memory/index.js +190 -197
  9. package/dist/drivers/memory/index.js.map +1 -0
  10. package/dist/drivers/r2/index.d.ts +7 -4
  11. package/dist/drivers/r2/index.js +162 -160
  12. package/dist/drivers/r2/index.js.map +1 -0
  13. package/dist/drivers/r2-http/index.d.ts +23 -19
  14. package/dist/drivers/r2-http/index.js +52 -45
  15. package/dist/drivers/r2-http/index.js.map +1 -0
  16. package/dist/drivers/s3/index.d.ts +79 -5
  17. package/dist/drivers/s3/index.js +2 -3
  18. package/dist/index.d.ts +271 -17
  19. package/dist/index.js +1078 -16
  20. package/dist/index.js.map +1 -0
  21. package/dist/middleware/index.d.ts +177 -15
  22. package/dist/middleware/index.js +553 -8
  23. package/dist/middleware/index.js.map +1 -0
  24. package/dist/protocol.types-C5PPBD12.d.ts +80 -0
  25. package/dist/r2.types-Bw4Ft83t.d.ts +71 -0
  26. package/dist/retry-DXtO6R9D.js +119 -0
  27. package/dist/retry-DXtO6R9D.js.map +1 -0
  28. package/dist/s3-DrE9NrnD.js +557 -0
  29. package/dist/s3-DrE9NrnD.js.map +1 -0
  30. package/dist/storage.error-DVCOkJhc.js +49 -0
  31. package/dist/storage.error-DVCOkJhc.js.map +1 -0
  32. package/dist/storage.types-BJN7Yp1J.d.ts +288 -0
  33. package/dist/storagesdk/index.d.ts +73 -69
  34. package/dist/storagesdk/index.js +117 -130
  35. package/dist/storagesdk/index.js.map +1 -0
  36. package/dist/stored-file-2Yl07eRc.js +105 -0
  37. package/dist/stored-file-2Yl07eRc.js.map +1 -0
  38. package/dist/testing/index.d.ts +12 -8
  39. package/dist/testing/index.js +36 -53
  40. package/dist/testing/index.js.map +1 -0
  41. package/package.json +62 -48
  42. package/dist/base64.d.ts +0 -8
  43. package/dist/base64.js +0 -25
  44. package/dist/decorators/inject-storage.decorator.d.ts +0 -17
  45. package/dist/decorators/inject-storage.decorator.js +0 -21
  46. package/dist/drivers/r2/r2.types.d.ts +0 -67
  47. package/dist/drivers/r2/r2.types.js +0 -5
  48. package/dist/drivers/s3/s3-client.d.ts +0 -33
  49. package/dist/drivers/s3/s3-client.js +0 -154
  50. package/dist/drivers/s3/s3.driver.d.ts +0 -4
  51. package/dist/drivers/s3/s3.driver.js +0 -407
  52. package/dist/drivers/s3/s3.types.d.ts +0 -28
  53. package/dist/drivers/s3/s3.types.js +0 -1
  54. package/dist/drivers/s3/xml.d.ts +0 -6
  55. package/dist/drivers/s3/xml.js +0 -52
  56. package/dist/http/authorizer.types.d.ts +0 -59
  57. package/dist/http/authorizer.types.js +0 -1
  58. package/dist/http/http-helpers.d.ts +0 -22
  59. package/dist/http/http-helpers.js +0 -80
  60. package/dist/http/protocol.types.d.ts +0 -76
  61. package/dist/http/protocol.types.js +0 -1
  62. package/dist/internal/body.d.ts +0 -18
  63. package/dist/internal/body.js +0 -90
  64. package/dist/internal/retry.d.ts +0 -41
  65. package/dist/internal/retry.js +0 -127
  66. package/dist/internal/stored-file.d.ts +0 -27
  67. package/dist/internal/stored-file.js +0 -114
  68. package/dist/middleware/cache.d.ts +0 -50
  69. package/dist/middleware/cache.js +0 -101
  70. package/dist/middleware/compose.d.ts +0 -12
  71. package/dist/middleware/compose.js +0 -11
  72. package/dist/middleware/compression.d.ts +0 -16
  73. package/dist/middleware/compression.js +0 -101
  74. package/dist/middleware/encryption.d.ts +0 -14
  75. package/dist/middleware/encryption.js +0 -134
  76. package/dist/middleware/failover.d.ts +0 -18
  77. package/dist/middleware/failover.js +0 -52
  78. package/dist/middleware/retry.d.ts +0 -14
  79. package/dist/middleware/retry.js +0 -47
  80. package/dist/middleware/versioning.d.ts +0 -36
  81. package/dist/middleware/versioning.js +0 -89
  82. package/dist/middleware/wrap.d.ts +0 -11
  83. package/dist/middleware/wrap.js +0 -46
  84. package/dist/object-key.d.ts +0 -17
  85. package/dist/object-key.js +0 -42
  86. package/dist/storage.controller.d.ts +0 -21
  87. package/dist/storage.controller.js +0 -386
  88. package/dist/storage.error.d.ts +0 -38
  89. package/dist/storage.error.js +0 -52
  90. package/dist/storage.facade.d.ts +0 -47
  91. package/dist/storage.facade.js +0 -443
  92. package/dist/storage.module.d.ts +0 -46
  93. package/dist/storage.module.js +0 -113
  94. package/dist/storage.service.d.ts +0 -42
  95. package/dist/storage.service.js +0 -86
  96. package/dist/storage.tokens.d.ts +0 -13
  97. package/dist/storage.tokens.js +0 -26
  98. package/dist/storage.types.d.ts +0 -245
  99. package/dist/storage.types.js +0 -1
@@ -1,16 +0,0 @@
1
- import { type Middleware } from './wrap';
2
- export type CompressionFormat = 'gzip' | 'deflate' | 'deflate-raw';
3
- export interface CompressionOptions {
4
- /** Stream format. Default `gzip`. */
5
- format?: CompressionFormat;
6
- /** What to do when `CompressionStream` is unavailable. Default `throw`. */
7
- onUnavailable?: 'throw' | 'passthrough';
8
- }
9
- /**
10
- * Transparent gzip/deflate via Web Streams `CompressionStream` — fully
11
- * streaming, opaque mode (v1). The stored object is compressed and tagged
12
- * `vela-zip`; reads detect the tag and decompress, so pre-existing (untagged)
13
- * objects pass through unharmed. Range reads / presigned URLs are disabled
14
- * (the byte stream is not seekable and a direct URL would serve gzip).
15
- */
16
- export declare function compression(opts?: CompressionOptions): Middleware;
@@ -1,101 +0,0 @@
1
- import { byteLengthOf, toStream } from "../internal/body.js";
2
- import { createStoredFile } from "../internal/stored-file.js";
3
- import { StorageError } from "../storage.error.js";
4
- import { passthrough } from "./wrap.js";
5
- function stripVela(meta) {
6
- if (!meta) return undefined;
7
- const out = {};
8
- for (const [k, v] of Object.entries(meta))if (!k.startsWith('vela-')) out[k] = v;
9
- return Object.keys(out).length ? out : undefined;
10
- }
11
- /**
12
- * Transparent gzip/deflate via Web Streams `CompressionStream` — fully
13
- * streaming, opaque mode (v1). The stored object is compressed and tagged
14
- * `vela-zip`; reads detect the tag and decompress, so pre-existing (untagged)
15
- * objects pass through unharmed. Range reads / presigned URLs are disabled
16
- * (the byte stream is not seekable and a direct URL would serve gzip).
17
- */ export function compression(opts = {}) {
18
- const format = opts.format ?? 'gzip';
19
- const available = typeof CompressionStream !== 'undefined' && typeof DecompressionStream !== 'undefined';
20
- if (!available && (opts.onUnavailable ?? 'throw') === 'throw') {
21
- throw new StorageError('Unsupported', 'CompressionStream is not available in this runtime');
22
- }
23
- return (inner)=>{
24
- if (!available) return inner; // passthrough mode
25
- const decompressDownload = async (k, _o)=>{
26
- const file = await inner.download(k);
27
- const zip = file.metadata?.['vela-zip'];
28
- if (!zip) return file; // untagged / legacy object
29
- const plain = file.stream().pipeThrough(new DecompressionStream(zip));
30
- const size = file.metadata?.['vela-size'] ? Number(file.metadata['vela-size']) : file.size;
31
- return createStoredFile({
32
- key: k,
33
- name: file.name,
34
- size,
35
- type: file.metadata?.['vela-ct'] ?? file.type,
36
- lastModified: file.lastModified,
37
- etag: file.etag,
38
- metadata: stripVela(file.metadata)
39
- }, {
40
- kind: 'stream',
41
- stream: plain
42
- });
43
- };
44
- return passthrough(inner, {
45
- reportsUploadProgress: false,
46
- supportsRange: false,
47
- signedUrl: {
48
- supported: false,
49
- upload: false
50
- },
51
- async upload (k, body, o) {
52
- const zipped = toStream(body).pipeThrough(new CompressionStream(format));
53
- const metadata = {
54
- ...o?.metadata,
55
- 'vela-zip': format
56
- };
57
- if (o?.contentType) metadata['vela-ct'] = o.contentType;
58
- const known = byteLengthOf(body);
59
- if (known != null) metadata['vela-size'] = String(known);
60
- const r = await inner.upload(k, zipped, {
61
- ...o,
62
- metadata: inner.supportsMetadata ? metadata : undefined,
63
- contentType: 'application/octet-stream'
64
- });
65
- return {
66
- ...r,
67
- size: known ?? r.size,
68
- contentType: o?.contentType ?? 'application/octet-stream'
69
- };
70
- },
71
- download: decompressDownload,
72
- async head (k, o) {
73
- const file = await inner.head(k, o);
74
- if (!file.metadata?.['vela-zip']) return file;
75
- const size = file.metadata['vela-size'] ? Number(file.metadata['vela-size']) : file.size;
76
- return createStoredFile({
77
- key: k,
78
- name: file.name,
79
- size,
80
- type: file.metadata['vela-ct'] ?? file.type,
81
- lastModified: file.lastModified,
82
- etag: file.etag,
83
- metadata: stripVela(file.metadata)
84
- }, {
85
- kind: 'lazy',
86
- fetch: async ()=>new Response((await decompressDownload(k)).stream())
87
- });
88
- },
89
- url () {
90
- throw new StorageError('Unsupported', 'compression: url() would serve compressed bytes');
91
- },
92
- signedUploadUrl () {
93
- throw new StorageError('Unsupported', 'compression: signed uploads would store uncompressed');
94
- }
95
- }, [
96
- 'createMultipartUpload',
97
- 'resumeMultipartUpload',
98
- 'signedMultipart'
99
- ]);
100
- };
101
- }
@@ -1,14 +0,0 @@
1
- import { type Middleware } from './wrap';
2
- export interface EncryptionOptions {
3
- /** A 256-bit AES-GCM key: a `CryptoKey` or 32 raw bytes. */
4
- key: CryptoKey | Uint8Array;
5
- /** How to treat objects that aren't valid ciphertext (legacy data). Default `throw`. */
6
- onInvalid?: 'throw' | 'passthrough';
7
- }
8
- /**
9
- * Transparent client-side encryption via Web Crypto AES-GCM. v1 encrypts the
10
- * WHOLE object (buffered); the stored object is `IV(12) || ciphertext+tag`.
11
- * Range reads, presigned URLs, and multipart are disabled (a signed URL would
12
- * hand out ciphertext / accept plaintext, defeating the invariant).
13
- */
14
- export declare function encryption(opts: EncryptionOptions): Middleware;
@@ -1,134 +0,0 @@
1
- import { toBytes } from "../internal/body.js";
2
- import { createStoredFile } from "../internal/stored-file.js";
3
- import { StorageError } from "../storage.error.js";
4
- import { passthrough } from "./wrap.js";
5
- const IV_BYTES = 12;
6
- // The Web Crypto typings pin BufferSource to ArrayBuffer-backed views; our byte
7
- // views are ArrayBufferLike-typed. They are ArrayBuffer-backed at runtime, so a
8
- // narrowing cast is safe and confined to this helper.
9
- const asBuf = (u)=>u;
10
- function stripVela(meta) {
11
- if (!meta) return undefined;
12
- const out = {};
13
- for (const [k, v] of Object.entries(meta))if (!k.startsWith('vela-')) out[k] = v;
14
- return Object.keys(out).length ? out : undefined;
15
- }
16
- async function importKey(key) {
17
- if (key instanceof Uint8Array) {
18
- return crypto.subtle.importKey('raw', key, {
19
- name: 'AES-GCM'
20
- }, false, [
21
- 'encrypt',
22
- 'decrypt'
23
- ]);
24
- }
25
- return key;
26
- }
27
- /**
28
- * Transparent client-side encryption via Web Crypto AES-GCM. v1 encrypts the
29
- * WHOLE object (buffered); the stored object is `IV(12) || ciphertext+tag`.
30
- * Range reads, presigned URLs, and multipart are disabled (a signed URL would
31
- * hand out ciphertext / accept plaintext, defeating the invariant).
32
- */ export function encryption(opts) {
33
- const keyPromise = importKey(opts.key);
34
- return (inner)=>{
35
- const decryptDownload = async (k, _o)=>{
36
- const file = await inner.download(k);
37
- const framed = new Uint8Array(await file.arrayBuffer());
38
- if (framed.byteLength < IV_BYTES) {
39
- if (opts.onInvalid === 'passthrough') return file;
40
- throw new StorageError('Parse', `object too small to be encrypted: ${k}`);
41
- }
42
- const iv = framed.subarray(0, IV_BYTES);
43
- const ciphertext = framed.subarray(IV_BYTES);
44
- const key = await keyPromise;
45
- let plain;
46
- try {
47
- plain = new Uint8Array(await crypto.subtle.decrypt({
48
- name: 'AES-GCM',
49
- iv: asBuf(iv)
50
- }, key, asBuf(ciphertext)));
51
- } catch (e) {
52
- if (opts.onInvalid === 'passthrough') return file;
53
- throw new StorageError('Provider', `decryption failed: ${k}`, {
54
- cause: e
55
- });
56
- }
57
- return createStoredFile({
58
- key: k,
59
- name: file.name,
60
- size: plain.byteLength,
61
- type: file.metadata?.['vela-ct'] ?? file.type,
62
- lastModified: file.lastModified,
63
- etag: file.etag,
64
- metadata: stripVela(file.metadata)
65
- }, {
66
- kind: 'bytes',
67
- bytes: plain
68
- });
69
- };
70
- return passthrough(inner, {
71
- reportsUploadProgress: false,
72
- supportsRange: false,
73
- signedUrl: {
74
- supported: false,
75
- upload: false
76
- },
77
- async upload (k, body, o) {
78
- const plaintext = await toBytes(body);
79
- const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
80
- const key = await keyPromise;
81
- const ct = new Uint8Array(await crypto.subtle.encrypt({
82
- name: 'AES-GCM',
83
- iv
84
- }, key, asBuf(plaintext)));
85
- const framed = new Uint8Array(IV_BYTES + ct.byteLength);
86
- framed.set(iv, 0);
87
- framed.set(ct, IV_BYTES);
88
- const metadata = {
89
- ...o?.metadata,
90
- 'vela-enc': 'AES-GCM'
91
- };
92
- if (o?.contentType) metadata['vela-ct'] = o.contentType;
93
- metadata['vela-size'] = String(plaintext.byteLength);
94
- const r = await inner.upload(k, framed, {
95
- ...o,
96
- metadata: inner.supportsMetadata ? metadata : undefined,
97
- contentType: 'application/octet-stream'
98
- });
99
- return {
100
- ...r,
101
- size: plaintext.byteLength,
102
- contentType: o?.contentType ?? 'application/octet-stream'
103
- };
104
- },
105
- download: decryptDownload,
106
- async head (k, o) {
107
- const file = await inner.head(k, o);
108
- const size = file.metadata?.['vela-size'] ? Number(file.metadata['vela-size']) : file.size;
109
- return createStoredFile({
110
- key: k,
111
- name: file.name,
112
- size,
113
- type: file.metadata?.['vela-ct'] ?? file.type,
114
- lastModified: file.lastModified,
115
- etag: file.etag,
116
- metadata: stripVela(file.metadata)
117
- }, {
118
- kind: 'lazy',
119
- fetch: async ()=>new Response((await decryptDownload(k)).stream())
120
- });
121
- },
122
- url () {
123
- throw new StorageError('Unsupported', 'encryption: url() would expose ciphertext');
124
- },
125
- signedUploadUrl () {
126
- throw new StorageError('Unsupported', 'encryption: signed uploads would store plaintext');
127
- }
128
- }, [
129
- 'createMultipartUpload',
130
- 'resumeMultipartUpload',
131
- 'signedMultipart'
132
- ]);
133
- };
134
- }
@@ -1,18 +0,0 @@
1
- import type { StorageDriver } from '../storage.types';
2
- import { type Middleware } from './wrap';
3
- export interface FailoverOptions {
4
- /** Decide whether an error should trigger failover. Default: transient errors. */
5
- shouldFailover?: (error: unknown) => boolean;
6
- onFailover?: (info: {
7
- from: number;
8
- to: number;
9
- error: unknown;
10
- }) => void;
11
- }
12
- /**
13
- * Read-failover across a primary + fallbacks. Reads try each driver in order
14
- * until one succeeds; writes and presigning go to the primary only (avoids
15
- * partial/divergent writes). Capabilities are the intersection so the facade
16
- * never routes a feature to a driver that can't serve it.
17
- */
18
- export declare function failover(fallbacks: StorageDriver[], opts?: FailoverOptions): Middleware;
@@ -1,52 +0,0 @@
1
- import { StorageError } from "../storage.error.js";
2
- import { passthrough } from "./wrap.js";
3
- function isTransient(error) {
4
- return error instanceof StorageError ? error.retryable : true;
5
- }
6
- function intersectCapabilities(chain) {
7
- return {
8
- supportsRange: chain.every((d)=>!!d.supportsRange),
9
- supportsDelimiter: chain.every((d)=>!!d.supportsDelimiter),
10
- supportsMetadata: chain.every((d)=>!!d.supportsMetadata),
11
- supportsCacheControl: chain.every((d)=>!!d.supportsCacheControl),
12
- supportsServerSideCopy: chain.every((d)=>!!d.supportsServerSideCopy)
13
- };
14
- }
15
- /**
16
- * Read-failover across a primary + fallbacks. Reads try each driver in order
17
- * until one succeeds; writes and presigning go to the primary only (avoids
18
- * partial/divergent writes). Capabilities are the intersection so the facade
19
- * never routes a feature to a driver that can't serve it.
20
- */ export function failover(fallbacks, opts = {}) {
21
- const should = opts.shouldFailover ?? isTransient;
22
- return (primary)=>{
23
- const chain = [
24
- primary,
25
- ...fallbacks
26
- ];
27
- const read = async (fn)=>{
28
- let lastError;
29
- for(let i = 0; i < chain.length; i++){
30
- try {
31
- return await fn(chain[i]);
32
- } catch (e) {
33
- lastError = e;
34
- if (!should(e) || i === chain.length - 1) throw e;
35
- opts.onFailover?.({
36
- from: i,
37
- to: i + 1,
38
- error: e
39
- });
40
- }
41
- }
42
- throw lastError;
43
- };
44
- return passthrough(primary, {
45
- ...intersectCapabilities(chain),
46
- download: (k, o)=>read((d)=>d.download(k, o)),
47
- head: (k, o)=>read((d)=>d.head(k, o)),
48
- exists: (k, o)=>read((d)=>d.exists(k, o)),
49
- list: (o)=>read((d)=>d.list(o))
50
- });
51
- };
52
- }
@@ -1,14 +0,0 @@
1
- import type { RetryOptions } from '../storage.types';
2
- import { type Middleware } from './wrap';
3
- export interface RetryMiddlewareOptions {
4
- /** Default retry policy for driver operations. Per-call `opts.retries` wins. */
5
- retries?: RetryOptions;
6
- }
7
- /**
8
- * Retry transient failures at the driver level. Reads and idempotent ops
9
- * always retry; an upload retries only when its body is replayable (not a
10
- * one-shot stream). Note: the `Storage` facade also retries per
11
- * `OperationOptions.retries` — use this middleware when driving a driver
12
- * directly, or set the facade's retries to 0 to avoid compounding.
13
- */
14
- export declare function retry(opts?: RetryMiddlewareOptions): Middleware;
@@ -1,47 +0,0 @@
1
- import { isStream } from "../internal/body.js";
2
- import { runWithRetry } from "../internal/retry.js";
3
- import { passthrough } from "./wrap.js";
4
- /**
5
- * Retry transient failures at the driver level. Reads and idempotent ops
6
- * always retry; an upload retries only when its body is replayable (not a
7
- * one-shot stream). Note: the `Storage` facade also retries per
8
- * `OperationOptions.retries` — use this middleware when driving a driver
9
- * directly, or set the facade's retries to 0 to avoid compounding.
10
- */ export function retry(opts = {}) {
11
- const base = opts.retries ?? 3;
12
- const run = (o, fn)=>runWithRetry(fn, {
13
- retries: o?.retries ?? base,
14
- timeout: o?.timeout,
15
- signal: o?.signal
16
- });
17
- return (inner)=>passthrough(inner, {
18
- download: (k, o)=>run(o, (s)=>inner.download(k, {
19
- ...o,
20
- signal: s
21
- })),
22
- head: (k, o)=>run(o, (s)=>inner.head(k, {
23
- ...o,
24
- signal: s
25
- })),
26
- exists: (k, o)=>run(o, (s)=>inner.exists(k, {
27
- ...o,
28
- signal: s
29
- })),
30
- list: (o)=>run(o, (s)=>inner.list({
31
- ...o,
32
- signal: s
33
- })),
34
- delete: (k, o)=>run(o, (s)=>inner.delete(k, {
35
- ...o,
36
- signal: s
37
- })),
38
- copy: (f, t, o)=>run(o, (s)=>inner.copy(f, t, {
39
- ...o,
40
- signal: s
41
- })),
42
- upload: (k, b, o)=>isStream(b) ? inner.upload(k, b, o) : run(o, (s)=>inner.upload(k, b, {
43
- ...o,
44
- signal: s
45
- }))
46
- });
47
- }
@@ -1,36 +0,0 @@
1
- import type { StorageDriver } from '../storage.types';
2
- import { type Middleware } from './wrap';
3
- export interface VersionInfo {
4
- versionId: string;
5
- key: string;
6
- size: number;
7
- lastModified?: number;
8
- }
9
- /** Extra surface a versioning-wrapped driver exposes (reached via {@link unwrapVersioning}). */
10
- export interface Versioned {
11
- listVersions(key: string): Promise<VersionInfo[]>;
12
- restore(key: string, versionId: string): Promise<void>;
13
- deleteVersion(key: string, versionId: string): Promise<void>;
14
- }
15
- export declare const VERSIONED: unique symbol;
16
- export interface VersioningOptions {
17
- /** Prefix under which prior versions are stored. Default `.vela-versions/`. */
18
- prefix?: string;
19
- versionId?: () => string;
20
- /** Keep at most this many versions per key (prunes oldest). */
21
- maxVersions?: number;
22
- /** Hide the version prefix from `list()`. Default true. */
23
- hideFromList?: boolean;
24
- /** On delete: snapshot first (default) or purge all versions. */
25
- onDelete?: 'snapshot' | 'purge';
26
- }
27
- /**
28
- * Keeps prior versions on overwrite via key-suffixing (no external manifest →
29
- * no read-modify-write race). Snapshots are cheap when the base driver
30
- * `supportsServerSideCopy`. Direct presigned uploads bypass snapshotting, so
31
- * `signedUploadUrl` throws. Reach `listVersions`/`restore`/`deleteVersion` via
32
- * {@link unwrapVersioning}.
33
- */
34
- export declare function versioning(opts?: VersioningOptions): Middleware;
35
- /** Retrieve the {@link Versioned} surface from a versioning-wrapped driver (or `undefined`). */
36
- export declare function unwrapVersioning(driver: StorageDriver): Versioned | undefined;
@@ -1,89 +0,0 @@
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
- }
@@ -1,11 +0,0 @@
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;
@@ -1,46 +0,0 @@
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
- }
@@ -1,17 +0,0 @@
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;
@@ -1,42 +0,0 @@
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
- }