uploaderkit 1.0.0 → 2.0.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 (86) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +16 -0
  3. package/README.es.md +1140 -0
  4. package/README.md +1135 -0
  5. package/dist/FileViewer-ChTf7-uY.d.cts +49 -0
  6. package/dist/FileViewer-nuD-GEdJ.d.ts +49 -0
  7. package/dist/adapters/gcs.cjs +83 -0
  8. package/dist/adapters/gcs.cjs.map +1 -0
  9. package/dist/adapters/gcs.d.cts +31 -0
  10. package/dist/adapters/gcs.d.ts +31 -0
  11. package/dist/adapters/gcs.js +81 -0
  12. package/dist/adapters/gcs.js.map +1 -0
  13. package/dist/adapters/memory.cjs +27 -0
  14. package/dist/adapters/memory.cjs.map +1 -0
  15. package/dist/adapters/memory.d.cts +18 -0
  16. package/dist/adapters/memory.d.ts +18 -0
  17. package/dist/adapters/memory.js +25 -0
  18. package/dist/adapters/memory.js.map +1 -0
  19. package/dist/adapters/s3.cjs +75 -0
  20. package/dist/adapters/s3.cjs.map +1 -0
  21. package/dist/adapters/s3.d.cts +27 -0
  22. package/dist/adapters/s3.d.ts +27 -0
  23. package/dist/adapters/s3.js +73 -0
  24. package/dist/adapters/s3.js.map +1 -0
  25. package/dist/chunk-3FI44IOW.js +150 -0
  26. package/dist/chunk-3FI44IOW.js.map +1 -0
  27. package/dist/chunk-H7BRW5IY.js +308 -0
  28. package/dist/chunk-H7BRW5IY.js.map +1 -0
  29. package/dist/chunk-PDKAF4GX.js +707 -0
  30. package/dist/chunk-PDKAF4GX.js.map +1 -0
  31. package/dist/chunk-PTEX7F4R.js +309 -0
  32. package/dist/chunk-PTEX7F4R.js.map +1 -0
  33. package/dist/chunk-T5YZG6JW.js +511 -0
  34. package/dist/chunk-T5YZG6JW.js.map +1 -0
  35. package/dist/index.cjs +220 -27
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.d.cts +57 -170
  38. package/dist/index.d.ts +57 -170
  39. package/dist/index.js +1 -324
  40. package/dist/index.js.map +1 -1
  41. package/dist/presets.cjs +1665 -0
  42. package/dist/presets.cjs.map +1 -0
  43. package/dist/presets.d.cts +100 -0
  44. package/dist/presets.d.ts +100 -0
  45. package/dist/presets.js +405 -0
  46. package/dist/presets.js.map +1 -0
  47. package/dist/react.cjs +981 -0
  48. package/dist/react.cjs.map +1 -0
  49. package/dist/react.d.cts +106 -0
  50. package/dist/react.d.ts +106 -0
  51. package/dist/react.js +5 -0
  52. package/dist/react.js.map +1 -0
  53. package/dist/server/express.cjs +179 -0
  54. package/dist/server/express.cjs.map +1 -0
  55. package/dist/server/express.d.cts +64 -0
  56. package/dist/server/express.d.ts +64 -0
  57. package/dist/server/express.js +111 -0
  58. package/dist/server/express.js.map +1 -0
  59. package/dist/server/next.cjs +165 -0
  60. package/dist/server/next.cjs.map +1 -0
  61. package/dist/server/next.d.cts +38 -0
  62. package/dist/server/next.d.ts +38 -0
  63. package/dist/server/next.js +107 -0
  64. package/dist/server/next.js.map +1 -0
  65. package/dist/server.cjs +550 -0
  66. package/dist/server.cjs.map +1 -0
  67. package/dist/server.d.cts +19 -0
  68. package/dist/server.d.ts +19 -0
  69. package/dist/server.js +80 -0
  70. package/dist/server.js.map +1 -0
  71. package/dist/storage-CYkSHWZX.d.cts +133 -0
  72. package/dist/storage-Qc9epG0G.d.ts +133 -0
  73. package/dist/types-BSlJJwti.d.cts +341 -0
  74. package/dist/types-BSlJJwti.d.ts +341 -0
  75. package/dist/ui.cjs +2325 -0
  76. package/dist/ui.cjs.map +1 -0
  77. package/dist/ui.d.cts +331 -0
  78. package/dist/ui.d.ts +331 -0
  79. package/dist/ui.js +749 -0
  80. package/dist/ui.js.map +1 -0
  81. package/dist/useSlottedUploader-BzT5jV8c.d.cts +139 -0
  82. package/dist/useSlottedUploader-IkG5RhEO.d.ts +139 -0
  83. package/dist/useUploader-BiBdS-7y.d.cts +117 -0
  84. package/dist/useUploader-CQHpj_oI.d.ts +117 -0
  85. package/package.json +189 -3
  86. package/tailwind.css +80 -0
package/dist/index.d.cts CHANGED
@@ -1,166 +1,5 @@
1
- /**
2
- * Every type in this file must be usable from a browser and from a server.
3
- * That is what lets one scope definition drive the client's guards and the
4
- * server's guards without the two drifting apart.
5
- */
6
- type ImageExtension = 'png' | 'jpg' | 'jpeg' | 'svg' | 'webp' | 'gif' | 'ico' | 'heic';
7
- type CertificateExtension = 'cer' | 'crt' | 'pem';
8
- type KeyExtension = 'key';
9
- type DocumentExtension = 'pdf' | 'doc' | 'docx' | 'xls' | 'xlsx' | 'csv' | 'txt';
10
- type DataExtension = 'xml' | 'json';
11
- type VideoExtension = 'mp4' | 'mov' | 'avi' | 'webm';
12
- type AudioExtension = 'mp3' | 'wav' | 'ogg' | 'aac';
13
- type FileExtension = ImageExtension | CertificateExtension | KeyExtension | DocumentExtension | DataExtension | VideoExtension | AudioExtension;
14
- /** Validation preset. Picks extensions, size ceiling and header checking. */
15
- type FileCategory = 'image' | 'certificate' | 'key' | 'pdf' | 'document' | 'data' | 'video' | 'audio' | 'any';
16
- type FileCategoryConfig = {
17
- extensions: FileExtension[];
18
- maxBytes: number;
19
- /** Read the leading bytes to catch an .exe renamed to .pdf. */
20
- validateMagicNumbers: boolean;
21
- /** Lower-case singular noun used in error messages. */
22
- label: string;
23
- /** Value for an `<input accept="…">`. */
24
- accept: string;
25
- };
26
- /**
27
- * The subset of the DOM `File` this package needs. A browser `File`, a Node 18+
28
- * `File` and a multer memory file (via `fromMulterFile`) all satisfy it, so
29
- * `validateFile` is the same call on both sides of the wire.
30
- */
31
- type FileLike = {
32
- name: string;
33
- size: number;
34
- /** MIME type as reported by the source. Never trusted on its own. */
35
- type: string;
36
- arrayBuffer(): Promise<ArrayBuffer>;
37
- };
38
- type UploadStatus = 'idle' | 'validating' | 'uploading' | 'success' | 'error';
39
- /** Machine-readable reason a file was rejected. Pair with `message` for UI. */
40
- type ValidationCode = 'extension-not-allowed' | 'too-large' | 'empty-file' | 'magic-number-mismatch' | 'mime-not-allowed' | 'custom';
41
- type ValidationResult = {
42
- valid: true;
43
- } | {
44
- valid: false;
45
- code: ValidationCode;
46
- message: string;
47
- };
48
- type ValidationOptions = {
49
- maxBytes?: number;
50
- allowedExtensions?: FileExtension[];
51
- validateMagicNumbers?: boolean;
52
- /** Runs last, only when every built-in check passed. */
53
- customValidation?: (file: FileLike) => Promise<ValidationResult>;
54
- };
55
- /**
56
- * Public files are served straight from their URL; private files are only ever
57
- * reachable through a signed, expiring URL minted by the server.
58
- */
59
- type Visibility = 'public' | 'private';
60
- /** Client-side image pipeline applied before upload. */
61
- type CompressOptions = {
62
- maxWidth?: number;
63
- maxHeight?: number;
64
- /** 0–1. Ignored by formats without lossy encoding. */
65
- quality?: number;
66
- /**
67
- * Drop EXIF metadata. Defaults to true: camera photos carry GPS
68
- * coordinates, and a public bucket is the wrong place for them.
69
- */
70
- stripExif?: boolean;
71
- };
72
- /**
73
- * A named destination. The single source of truth for where a file lands, who
74
- * may read it, and what is accepted there.
75
- */
76
- type ScopeConfig = {
77
- /**
78
- * Storage key for an upload. Receives the owning entity and the incoming
79
- * file so the caller controls collisions and folder shape.
80
- */
81
- path: (entityId: string, file: FileLike) => string;
82
- visibility: Visibility;
83
- /** Extensions accepted here. Narrower than the category preset, never wider. */
84
- accept: FileExtension[];
85
- maxBytes: number;
86
- category?: FileCategory;
87
- /** Hand the bytes to the app's cipher before they leave the server. */
88
- encrypt?: boolean;
89
- compress?: CompressOptions;
90
- /** Replace the object at the same key instead of adding a new one. */
91
- overwrite?: boolean;
92
- /** Documentation-only today; drives lifecycle rules once adapters read it. */
93
- retention?: string;
94
- /** Free-form tags forwarded to the provider when it supports metadata. */
95
- metadata?: Record<string, string>;
96
- };
97
- type ScopeRegistry<T extends Record<string, ScopeConfig>> = {
98
- scopes: T;
99
- names: (keyof T & string)[];
100
- get(name: string): ScopeConfig;
101
- has(name: string): boolean;
102
- /** `<input accept>` string for a scope, derived from its extensions. */
103
- accept(name: keyof T & string): string;
104
- };
105
- /** What the app persists after a successful upload. */
106
- type StoredFile = {
107
- /** Provider key. The handle for delete, re-sign and download. */
108
- key: string;
109
- /** Public URL, or a signed expiring URL when the scope is private. */
110
- url: string;
111
- scope: string;
112
- entityId: string;
113
- fileName: string;
114
- mimeType: string;
115
- size: number;
116
- /** Enables dedupe and integrity checks when the provider reports it. */
117
- checksum?: string;
118
- uploadedAt: number;
119
- uploadedBy?: string;
120
- };
121
- type PutInput = {
122
- key: string;
123
- body: Uint8Array;
124
- contentType: string;
125
- visibility: Visibility;
126
- metadata?: Record<string, string>;
127
- };
128
- /**
129
- * What a provider can actually do. Declared rather than discovered so a scope
130
- * that needs signed URLs fails at boot, not the first time a user opens a file.
131
- */
132
- type ProviderCapabilities = {
133
- signedUrl: boolean;
134
- resumable: boolean;
135
- rangeRead: boolean;
136
- };
137
- type SignedUrlOptions = {
138
- /** Seconds until the URL stops working. */
139
- expiresIn: number;
140
- /** Force a download instead of inline rendering. */
141
- download?: boolean;
142
- };
143
- type StorageProvider = {
144
- name: string;
145
- capabilities: ProviderCapabilities;
146
- put(input: PutInput): Promise<{
147
- key: string;
148
- url: string;
149
- checksum?: string;
150
- }>;
151
- get(key: string): Promise<Uint8Array>;
152
- delete(key: string): Promise<boolean>;
153
- signedUrl?(key: string, options: SignedUrlOptions): Promise<string>;
154
- list(prefix: string): Promise<{
155
- key: string;
156
- size: number;
157
- }[]>;
158
- };
159
- /** Injected cipher for `encrypt: true` scopes. The package never ships one. */
160
- type CryptoHooks = {
161
- encrypt: (data: Uint8Array) => Uint8Array | Promise<Uint8Array>;
162
- decrypt: (data: Uint8Array) => Uint8Array | Promise<Uint8Array>;
163
- };
1
+ import { F as FileCategory, a as FileCategoryConfig, b as FileExtension, c as FileLike, d as ScopeConfig, e as ScopeRegistry, S as StorageProvider, R as ReplaceMode, U as UploaderLabels, f as ValidationResult, g as ValidationOptions } from './types-BSlJJwti.cjs';
2
+ export { A as AudioExtension, C as CertificateExtension, h as CompressOptions, i as CryptoHooks, D as DEFAULT_LABELS, j as DataExtension, k as DocumentExtension, E as EN_LABELS, l as ES_LABELS, I as ImageExtension, K as KeyExtension, P as ProviderCapabilities, m as PutInput, n as SignedUrlOptions, o as StoredFile, p as UploadStatus, q as ValidationCode, r as VideoExtension, V as Visibility, s as resolveLabels } from './types-BSlJJwti.cjs';
164
3
 
165
4
  declare const KB = 1024;
166
5
  declare const MB: number;
@@ -183,6 +22,30 @@ declare const getFileExtension: (fileName: string) => string;
183
22
  declare const getMimeType: (fileName: string) => string;
184
23
  declare const isKnownExtension: (value: string) => value is FileExtension;
185
24
  declare const formatFileSize: (bytes: number) => string;
25
+ /**
26
+ * Turns a user's file name into a safe key segment: ASCII, lower case, one
27
+ * extension, no path syntax.
28
+ *
29
+ * Call it INSIDE your scope's `path()`, not around it:
30
+ *
31
+ * ```ts
32
+ * path: (id, file) => `Docs/${id}/${sanitizeFileName(file.name)}`
33
+ * ```
34
+ *
35
+ * `resolveKey` deliberately does not apply it for you. It would rewrite the key
36
+ * your `path()` returned, and a client computing the same key to decide replace
37
+ * mode would then disagree with the server. Inside `path()` both sides run the
38
+ * one function and cannot drift.
39
+ *
40
+ * This exists because a macOS screenshot took an API down. `Screenshot
41
+ * 2026-08-31 at 4.18.54 p.m..png` ends in `p.m.` + `.png`, so the raw name
42
+ * carries `..`; the traversal guard rejected the key and the consumer dropped
43
+ * the rejection, killing the process.
44
+ *
45
+ * Only the KEY is slugged — keep the original in `StoredFile.fileName`, which
46
+ * is what a UI should render.
47
+ */
48
+ declare const sanitizeFileName: (fileName: string) => string;
186
49
  /** Value for an `<input accept="…">` built from an extension list. */
187
50
  declare const toAcceptAttribute: (extensions: FileExtension[]) => string;
188
51
  /**
@@ -200,36 +63,60 @@ declare const fromMulterFile: (file: {
200
63
  declare class ScopeError extends Error {
201
64
  name: string;
202
65
  }
66
+ /** True when `path` returns the same key whatever the file is called. */
67
+ declare const hasStableKey: (scope: ScopeConfig) => boolean;
68
+ /**
69
+ * The mode a scope runs in when it does not name one.
70
+ *
71
+ * @see AGENTS.md §3 — why a stable key needs no sweep and a name-carrying one does
72
+ */
73
+ declare const resolveReplaceMode: (scope: ScopeConfig) => ReplaceMode;
74
+ /** Folder an `'entity'` replace is allowed to sweep. */
75
+ declare const resolveScopePrefix: (scope: ScopeConfig, entityId: string) => string;
203
76
  /**
204
77
  * Declares every destination an app can write to. The returned registry is
205
78
  * imported by the client (to configure inputs and pre-validate) and by the
206
79
  * server (to authorize and re-validate), which is what keeps the two in sync.
207
80
  *
208
81
  * Definitions are checked eagerly: a malformed scope throws at import time,
209
- * not on the first upload.
82
+ * not on the first upload. That includes the two replace mistakes no runtime
83
+ * check could recover from — an entity-wide replace on a collection, and two
84
+ * scopes whose folders overlap, where an avatar upload would delete the same
85
+ * user's documents.
210
86
  */
211
87
  declare const defineScopes: <T extends Record<string, ScopeConfig>>(scopes: T) => ScopeRegistry<T>;
212
88
  /**
213
89
  * The one validation call. Client runs it for feedback, server runs it for
214
90
  * safety, both against the same scope.
91
+ *
92
+ * `labels` is what keeps the two answers identical in wording as well as in
93
+ * verdict: the hook passes the copy it renders with, `createStorage` passes
94
+ * the copy it was configured with.
215
95
  */
216
- declare const validateForScope: <T extends Record<string, ScopeConfig>>(registry: ScopeRegistry<T>, name: string, file: FileLike) => Promise<ValidationResult>;
96
+ declare const validateForScope: <T extends Record<string, ScopeConfig>>(registry: ScopeRegistry<T>, name: string, file: FileLike, labels?: Partial<UploaderLabels>) => Promise<ValidationResult>;
217
97
  /**
218
98
  * Fails fast when a provider cannot honour what the scopes promise — a private
219
99
  * scope on a provider that cannot sign URLs is a runtime 403 waiting to happen.
220
100
  */
221
101
  declare const assertProviderSupports: <T extends Record<string, ScopeConfig>>(registry: ScopeRegistry<T>, provider: StorageProvider) => void;
222
- /** Resolves the storage key for an upload. */
102
+ /**
103
+ * Resolves the storage key for an upload.
104
+ *
105
+ * The guard is a backstop, not a sanitizer: it refuses a key rather than
106
+ * rewriting one, because a client computing the same key to decide replace
107
+ * mode would then disagree with the server. Run {@link sanitizeFileName}
108
+ * inside the scope's `path()` and both sides stay identical.
109
+ */
223
110
  declare const resolveKey: <T extends Record<string, ScopeConfig>>(registry: ScopeRegistry<T>, name: string, entityId: string, file: FileLike) => string;
224
111
 
225
- declare const validateExtension: (fileName: string, allowed: FileExtension[]) => ValidationResult;
226
- declare const validateSize: (size: number, maxBytes: number) => ValidationResult;
112
+ declare const validateExtension: (fileName: string, allowed: FileExtension[], labels?: Partial<UploaderLabels>) => ValidationResult;
113
+ declare const validateSize: (size: number, maxBytes: number, labels?: Partial<UploaderLabels>) => ValidationResult;
227
114
  /**
228
115
  * Compares the file's leading bytes against the signature its extension
229
116
  * claims. Extensions without a known signature pass — absence of a signature
230
117
  * is not evidence of tampering.
231
118
  */
232
- declare const validateMagicNumbers: (file: FileLike) => Promise<ValidationResult>;
119
+ declare const validateMagicNumbers: (file: FileLike, labels?: Partial<UploaderLabels>) => Promise<ValidationResult>;
233
120
  /**
234
121
  * Runs the checks cheapest-first so a 2 GB file is rejected on its size before
235
122
  * anything reads its bytes.
@@ -242,4 +129,4 @@ type FileValidation = {
242
129
  /** Validates every file and reports each result; never short-circuits. */
243
130
  declare const validateFiles: (files: FileLike[], options?: ValidationOptions) => Promise<FileValidation[]>;
244
131
 
245
- export { type AudioExtension, type CertificateExtension, type CompressOptions, type CryptoHooks, type DataExtension, type DocumentExtension, FILE_CATEGORY_CONFIG, type FileCategory, type FileCategoryConfig, type FileExtension, type FileLike, type FileValidation, GB, type ImageExtension, KB, type KeyExtension, MAGIC_NUMBERS, MB, MIME_TYPES, type ProviderCapabilities, type PutInput, type ScopeConfig, ScopeError, type ScopeRegistry, type SignedUrlOptions, type StorageProvider, type StoredFile, type UploadStatus, type ValidationCode, type ValidationOptions, type ValidationResult, type VideoExtension, type Visibility, assertProviderSupports, defineScopes, formatFileSize, fromMulterFile, getFileExtension, getMimeType, isKnownExtension, resolveKey, toAcceptAttribute, validateExtension, validateFile, validateFiles, validateForScope, validateMagicNumbers, validateSize };
132
+ export { FILE_CATEGORY_CONFIG, FileCategory, FileCategoryConfig, FileExtension, FileLike, type FileValidation, GB, KB, MAGIC_NUMBERS, MB, MIME_TYPES, ReplaceMode, ScopeConfig, ScopeError, ScopeRegistry, StorageProvider, UploaderLabels, ValidationOptions, ValidationResult, assertProviderSupports, defineScopes, formatFileSize, fromMulterFile, getFileExtension, getMimeType, hasStableKey, isKnownExtension, resolveKey, resolveReplaceMode, resolveScopePrefix, sanitizeFileName, toAcceptAttribute, validateExtension, validateFile, validateFiles, validateForScope, validateMagicNumbers, validateSize };
package/dist/index.d.ts CHANGED
@@ -1,166 +1,5 @@
1
- /**
2
- * Every type in this file must be usable from a browser and from a server.
3
- * That is what lets one scope definition drive the client's guards and the
4
- * server's guards without the two drifting apart.
5
- */
6
- type ImageExtension = 'png' | 'jpg' | 'jpeg' | 'svg' | 'webp' | 'gif' | 'ico' | 'heic';
7
- type CertificateExtension = 'cer' | 'crt' | 'pem';
8
- type KeyExtension = 'key';
9
- type DocumentExtension = 'pdf' | 'doc' | 'docx' | 'xls' | 'xlsx' | 'csv' | 'txt';
10
- type DataExtension = 'xml' | 'json';
11
- type VideoExtension = 'mp4' | 'mov' | 'avi' | 'webm';
12
- type AudioExtension = 'mp3' | 'wav' | 'ogg' | 'aac';
13
- type FileExtension = ImageExtension | CertificateExtension | KeyExtension | DocumentExtension | DataExtension | VideoExtension | AudioExtension;
14
- /** Validation preset. Picks extensions, size ceiling and header checking. */
15
- type FileCategory = 'image' | 'certificate' | 'key' | 'pdf' | 'document' | 'data' | 'video' | 'audio' | 'any';
16
- type FileCategoryConfig = {
17
- extensions: FileExtension[];
18
- maxBytes: number;
19
- /** Read the leading bytes to catch an .exe renamed to .pdf. */
20
- validateMagicNumbers: boolean;
21
- /** Lower-case singular noun used in error messages. */
22
- label: string;
23
- /** Value for an `<input accept="…">`. */
24
- accept: string;
25
- };
26
- /**
27
- * The subset of the DOM `File` this package needs. A browser `File`, a Node 18+
28
- * `File` and a multer memory file (via `fromMulterFile`) all satisfy it, so
29
- * `validateFile` is the same call on both sides of the wire.
30
- */
31
- type FileLike = {
32
- name: string;
33
- size: number;
34
- /** MIME type as reported by the source. Never trusted on its own. */
35
- type: string;
36
- arrayBuffer(): Promise<ArrayBuffer>;
37
- };
38
- type UploadStatus = 'idle' | 'validating' | 'uploading' | 'success' | 'error';
39
- /** Machine-readable reason a file was rejected. Pair with `message` for UI. */
40
- type ValidationCode = 'extension-not-allowed' | 'too-large' | 'empty-file' | 'magic-number-mismatch' | 'mime-not-allowed' | 'custom';
41
- type ValidationResult = {
42
- valid: true;
43
- } | {
44
- valid: false;
45
- code: ValidationCode;
46
- message: string;
47
- };
48
- type ValidationOptions = {
49
- maxBytes?: number;
50
- allowedExtensions?: FileExtension[];
51
- validateMagicNumbers?: boolean;
52
- /** Runs last, only when every built-in check passed. */
53
- customValidation?: (file: FileLike) => Promise<ValidationResult>;
54
- };
55
- /**
56
- * Public files are served straight from their URL; private files are only ever
57
- * reachable through a signed, expiring URL minted by the server.
58
- */
59
- type Visibility = 'public' | 'private';
60
- /** Client-side image pipeline applied before upload. */
61
- type CompressOptions = {
62
- maxWidth?: number;
63
- maxHeight?: number;
64
- /** 0–1. Ignored by formats without lossy encoding. */
65
- quality?: number;
66
- /**
67
- * Drop EXIF metadata. Defaults to true: camera photos carry GPS
68
- * coordinates, and a public bucket is the wrong place for them.
69
- */
70
- stripExif?: boolean;
71
- };
72
- /**
73
- * A named destination. The single source of truth for where a file lands, who
74
- * may read it, and what is accepted there.
75
- */
76
- type ScopeConfig = {
77
- /**
78
- * Storage key for an upload. Receives the owning entity and the incoming
79
- * file so the caller controls collisions and folder shape.
80
- */
81
- path: (entityId: string, file: FileLike) => string;
82
- visibility: Visibility;
83
- /** Extensions accepted here. Narrower than the category preset, never wider. */
84
- accept: FileExtension[];
85
- maxBytes: number;
86
- category?: FileCategory;
87
- /** Hand the bytes to the app's cipher before they leave the server. */
88
- encrypt?: boolean;
89
- compress?: CompressOptions;
90
- /** Replace the object at the same key instead of adding a new one. */
91
- overwrite?: boolean;
92
- /** Documentation-only today; drives lifecycle rules once adapters read it. */
93
- retention?: string;
94
- /** Free-form tags forwarded to the provider when it supports metadata. */
95
- metadata?: Record<string, string>;
96
- };
97
- type ScopeRegistry<T extends Record<string, ScopeConfig>> = {
98
- scopes: T;
99
- names: (keyof T & string)[];
100
- get(name: string): ScopeConfig;
101
- has(name: string): boolean;
102
- /** `<input accept>` string for a scope, derived from its extensions. */
103
- accept(name: keyof T & string): string;
104
- };
105
- /** What the app persists after a successful upload. */
106
- type StoredFile = {
107
- /** Provider key. The handle for delete, re-sign and download. */
108
- key: string;
109
- /** Public URL, or a signed expiring URL when the scope is private. */
110
- url: string;
111
- scope: string;
112
- entityId: string;
113
- fileName: string;
114
- mimeType: string;
115
- size: number;
116
- /** Enables dedupe and integrity checks when the provider reports it. */
117
- checksum?: string;
118
- uploadedAt: number;
119
- uploadedBy?: string;
120
- };
121
- type PutInput = {
122
- key: string;
123
- body: Uint8Array;
124
- contentType: string;
125
- visibility: Visibility;
126
- metadata?: Record<string, string>;
127
- };
128
- /**
129
- * What a provider can actually do. Declared rather than discovered so a scope
130
- * that needs signed URLs fails at boot, not the first time a user opens a file.
131
- */
132
- type ProviderCapabilities = {
133
- signedUrl: boolean;
134
- resumable: boolean;
135
- rangeRead: boolean;
136
- };
137
- type SignedUrlOptions = {
138
- /** Seconds until the URL stops working. */
139
- expiresIn: number;
140
- /** Force a download instead of inline rendering. */
141
- download?: boolean;
142
- };
143
- type StorageProvider = {
144
- name: string;
145
- capabilities: ProviderCapabilities;
146
- put(input: PutInput): Promise<{
147
- key: string;
148
- url: string;
149
- checksum?: string;
150
- }>;
151
- get(key: string): Promise<Uint8Array>;
152
- delete(key: string): Promise<boolean>;
153
- signedUrl?(key: string, options: SignedUrlOptions): Promise<string>;
154
- list(prefix: string): Promise<{
155
- key: string;
156
- size: number;
157
- }[]>;
158
- };
159
- /** Injected cipher for `encrypt: true` scopes. The package never ships one. */
160
- type CryptoHooks = {
161
- encrypt: (data: Uint8Array) => Uint8Array | Promise<Uint8Array>;
162
- decrypt: (data: Uint8Array) => Uint8Array | Promise<Uint8Array>;
163
- };
1
+ import { F as FileCategory, a as FileCategoryConfig, b as FileExtension, c as FileLike, d as ScopeConfig, e as ScopeRegistry, S as StorageProvider, R as ReplaceMode, U as UploaderLabels, f as ValidationResult, g as ValidationOptions } from './types-BSlJJwti.js';
2
+ export { A as AudioExtension, C as CertificateExtension, h as CompressOptions, i as CryptoHooks, D as DEFAULT_LABELS, j as DataExtension, k as DocumentExtension, E as EN_LABELS, l as ES_LABELS, I as ImageExtension, K as KeyExtension, P as ProviderCapabilities, m as PutInput, n as SignedUrlOptions, o as StoredFile, p as UploadStatus, q as ValidationCode, r as VideoExtension, V as Visibility, s as resolveLabels } from './types-BSlJJwti.js';
164
3
 
165
4
  declare const KB = 1024;
166
5
  declare const MB: number;
@@ -183,6 +22,30 @@ declare const getFileExtension: (fileName: string) => string;
183
22
  declare const getMimeType: (fileName: string) => string;
184
23
  declare const isKnownExtension: (value: string) => value is FileExtension;
185
24
  declare const formatFileSize: (bytes: number) => string;
25
+ /**
26
+ * Turns a user's file name into a safe key segment: ASCII, lower case, one
27
+ * extension, no path syntax.
28
+ *
29
+ * Call it INSIDE your scope's `path()`, not around it:
30
+ *
31
+ * ```ts
32
+ * path: (id, file) => `Docs/${id}/${sanitizeFileName(file.name)}`
33
+ * ```
34
+ *
35
+ * `resolveKey` deliberately does not apply it for you. It would rewrite the key
36
+ * your `path()` returned, and a client computing the same key to decide replace
37
+ * mode would then disagree with the server. Inside `path()` both sides run the
38
+ * one function and cannot drift.
39
+ *
40
+ * This exists because a macOS screenshot took an API down. `Screenshot
41
+ * 2026-08-31 at 4.18.54 p.m..png` ends in `p.m.` + `.png`, so the raw name
42
+ * carries `..`; the traversal guard rejected the key and the consumer dropped
43
+ * the rejection, killing the process.
44
+ *
45
+ * Only the KEY is slugged — keep the original in `StoredFile.fileName`, which
46
+ * is what a UI should render.
47
+ */
48
+ declare const sanitizeFileName: (fileName: string) => string;
186
49
  /** Value for an `<input accept="…">` built from an extension list. */
187
50
  declare const toAcceptAttribute: (extensions: FileExtension[]) => string;
188
51
  /**
@@ -200,36 +63,60 @@ declare const fromMulterFile: (file: {
200
63
  declare class ScopeError extends Error {
201
64
  name: string;
202
65
  }
66
+ /** True when `path` returns the same key whatever the file is called. */
67
+ declare const hasStableKey: (scope: ScopeConfig) => boolean;
68
+ /**
69
+ * The mode a scope runs in when it does not name one.
70
+ *
71
+ * @see AGENTS.md §3 — why a stable key needs no sweep and a name-carrying one does
72
+ */
73
+ declare const resolveReplaceMode: (scope: ScopeConfig) => ReplaceMode;
74
+ /** Folder an `'entity'` replace is allowed to sweep. */
75
+ declare const resolveScopePrefix: (scope: ScopeConfig, entityId: string) => string;
203
76
  /**
204
77
  * Declares every destination an app can write to. The returned registry is
205
78
  * imported by the client (to configure inputs and pre-validate) and by the
206
79
  * server (to authorize and re-validate), which is what keeps the two in sync.
207
80
  *
208
81
  * Definitions are checked eagerly: a malformed scope throws at import time,
209
- * not on the first upload.
82
+ * not on the first upload. That includes the two replace mistakes no runtime
83
+ * check could recover from — an entity-wide replace on a collection, and two
84
+ * scopes whose folders overlap, where an avatar upload would delete the same
85
+ * user's documents.
210
86
  */
211
87
  declare const defineScopes: <T extends Record<string, ScopeConfig>>(scopes: T) => ScopeRegistry<T>;
212
88
  /**
213
89
  * The one validation call. Client runs it for feedback, server runs it for
214
90
  * safety, both against the same scope.
91
+ *
92
+ * `labels` is what keeps the two answers identical in wording as well as in
93
+ * verdict: the hook passes the copy it renders with, `createStorage` passes
94
+ * the copy it was configured with.
215
95
  */
216
- declare const validateForScope: <T extends Record<string, ScopeConfig>>(registry: ScopeRegistry<T>, name: string, file: FileLike) => Promise<ValidationResult>;
96
+ declare const validateForScope: <T extends Record<string, ScopeConfig>>(registry: ScopeRegistry<T>, name: string, file: FileLike, labels?: Partial<UploaderLabels>) => Promise<ValidationResult>;
217
97
  /**
218
98
  * Fails fast when a provider cannot honour what the scopes promise — a private
219
99
  * scope on a provider that cannot sign URLs is a runtime 403 waiting to happen.
220
100
  */
221
101
  declare const assertProviderSupports: <T extends Record<string, ScopeConfig>>(registry: ScopeRegistry<T>, provider: StorageProvider) => void;
222
- /** Resolves the storage key for an upload. */
102
+ /**
103
+ * Resolves the storage key for an upload.
104
+ *
105
+ * The guard is a backstop, not a sanitizer: it refuses a key rather than
106
+ * rewriting one, because a client computing the same key to decide replace
107
+ * mode would then disagree with the server. Run {@link sanitizeFileName}
108
+ * inside the scope's `path()` and both sides stay identical.
109
+ */
223
110
  declare const resolveKey: <T extends Record<string, ScopeConfig>>(registry: ScopeRegistry<T>, name: string, entityId: string, file: FileLike) => string;
224
111
 
225
- declare const validateExtension: (fileName: string, allowed: FileExtension[]) => ValidationResult;
226
- declare const validateSize: (size: number, maxBytes: number) => ValidationResult;
112
+ declare const validateExtension: (fileName: string, allowed: FileExtension[], labels?: Partial<UploaderLabels>) => ValidationResult;
113
+ declare const validateSize: (size: number, maxBytes: number, labels?: Partial<UploaderLabels>) => ValidationResult;
227
114
  /**
228
115
  * Compares the file's leading bytes against the signature its extension
229
116
  * claims. Extensions without a known signature pass — absence of a signature
230
117
  * is not evidence of tampering.
231
118
  */
232
- declare const validateMagicNumbers: (file: FileLike) => Promise<ValidationResult>;
119
+ declare const validateMagicNumbers: (file: FileLike, labels?: Partial<UploaderLabels>) => Promise<ValidationResult>;
233
120
  /**
234
121
  * Runs the checks cheapest-first so a 2 GB file is rejected on its size before
235
122
  * anything reads its bytes.
@@ -242,4 +129,4 @@ type FileValidation = {
242
129
  /** Validates every file and reports each result; never short-circuits. */
243
130
  declare const validateFiles: (files: FileLike[], options?: ValidationOptions) => Promise<FileValidation[]>;
244
131
 
245
- export { type AudioExtension, type CertificateExtension, type CompressOptions, type CryptoHooks, type DataExtension, type DocumentExtension, FILE_CATEGORY_CONFIG, type FileCategory, type FileCategoryConfig, type FileExtension, type FileLike, type FileValidation, GB, type ImageExtension, KB, type KeyExtension, MAGIC_NUMBERS, MB, MIME_TYPES, type ProviderCapabilities, type PutInput, type ScopeConfig, ScopeError, type ScopeRegistry, type SignedUrlOptions, type StorageProvider, type StoredFile, type UploadStatus, type ValidationCode, type ValidationOptions, type ValidationResult, type VideoExtension, type Visibility, assertProviderSupports, defineScopes, formatFileSize, fromMulterFile, getFileExtension, getMimeType, isKnownExtension, resolveKey, toAcceptAttribute, validateExtension, validateFile, validateFiles, validateForScope, validateMagicNumbers, validateSize };
132
+ export { FILE_CATEGORY_CONFIG, FileCategory, FileCategoryConfig, FileExtension, FileLike, type FileValidation, GB, KB, MAGIC_NUMBERS, MB, MIME_TYPES, ReplaceMode, ScopeConfig, ScopeError, ScopeRegistry, StorageProvider, UploaderLabels, ValidationOptions, ValidationResult, assertProviderSupports, defineScopes, formatFileSize, fromMulterFile, getFileExtension, getMimeType, hasStableKey, isKnownExtension, resolveKey, resolveReplaceMode, resolveScopePrefix, sanitizeFileName, toAcceptAttribute, validateExtension, validateFile, validateFiles, validateForScope, validateMagicNumbers, validateSize };