@basaltkit/files 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/files.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { BasaltError, type DurationInput, type HookBus } from '@basaltkit/core';
2
2
  import type { Disk } from '@basaltkit/storage';
3
3
  import { type FileRecord, type FileStore } from './store.js';
4
+ /** Default upload cap (25 MiB) applied when `validate.maxSize` is not set. */
5
+ export declare const DEFAULT_MAX_FILE_SIZE: number;
4
6
  export declare class FileTooLargeError extends BasaltError {
5
7
  readonly status = 413;
6
8
  constructor(size: number, max: number);
@@ -68,7 +70,15 @@ export declare class Files {
68
70
  record: FileRecord;
69
71
  content: Buffer;
70
72
  }>;
71
- temporaryUrl(id: string, expiresIn: DurationInput, tenantId?: string): Promise<string>;
73
+ /**
74
+ * Signed download URL — served `Content-Disposition: attachment` by default
75
+ * so an uploaded HTML/SVG file can never render top-level on the storage
76
+ * origin; pass `{ disposition: 'inline' }` when in-browser rendering is
77
+ * deliberate (embedded <img>/<video> uses render regardless).
78
+ */
79
+ temporaryUrl(id: string, expiresIn: DurationInput, tenantId?: string, options?: {
80
+ disposition?: 'attachment' | 'inline';
81
+ }): Promise<string>;
72
82
  delete(id: string, tenantId?: string): Promise<void>;
73
83
  /** Records the result of an out-of-band scan (antivirus, moderation, …). */
74
84
  markScanned(id: string, result: {
package/dist/files.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { BasaltError, runWithContext, tryCtx } from '@basaltkit/core';
3
3
  import { MemoryFileStore } from './store.js';
4
+ /** Default upload cap (25 MiB) applied when `validate.maxSize` is not set. */
5
+ export const DEFAULT_MAX_FILE_SIZE = 25 * 1024 * 1024;
4
6
  export class FileTooLargeError extends BasaltError {
5
7
  status = 413;
6
8
  constructor(size, max) {
@@ -52,7 +54,9 @@ export class Files {
52
54
  this.disk = options.disk;
53
55
  this.store = options.store ?? new MemoryFileStore();
54
56
  this.hooks = options.hooks;
55
- this.validation = options.validate ?? {};
57
+ // Secure by default (review 2026-08-b, S-3): uploads are capped even when
58
+ // the app configures nothing. Raise (or set Infinity) via validate.maxSize.
59
+ this.validation = { maxSize: DEFAULT_MAX_FILE_SIZE, ...options.validate };
56
60
  this.maxTotalBytes = options.maxTotalBytes;
57
61
  this.checkQuota = options.checkQuota;
58
62
  this.now = options.now ?? Date.now;
@@ -96,12 +100,18 @@ export class Files {
96
100
  const content = await this.inTenant(resolved, () => this.disk.get(record.path));
97
101
  return { record, content };
98
102
  }
99
- async temporaryUrl(id, expiresIn, tenantId) {
103
+ /**
104
+ * Signed download URL — served `Content-Disposition: attachment` by default
105
+ * so an uploaded HTML/SVG file can never render top-level on the storage
106
+ * origin; pass `{ disposition: 'inline' }` when in-browser rendering is
107
+ * deliberate (embedded <img>/<video> uses render regardless).
108
+ */
109
+ async temporaryUrl(id, expiresIn, tenantId, options = {}) {
100
110
  const resolved = this.tenant(tenantId);
101
111
  const record = await this.store.find(resolved, id);
102
112
  if (!record)
103
113
  throw new FileNotFoundError();
104
- return this.inTenant(resolved, () => this.disk.temporaryUrl(record.path, expiresIn));
114
+ return this.inTenant(resolved, () => this.disk.temporaryUrl(record.path, expiresIn, options));
105
115
  }
106
116
  async delete(id, tenantId) {
107
117
  const resolved = this.tenant(tenantId);
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { Files, FileTooLargeError, FileTypeNotAllowedError, StorageQuotaExceededError, FileNotFoundError, FileTenantRequiredError, type FilesOptions, type FileValidation, type UploadInput, } from './files.js';
1
+ export { Files, DEFAULT_MAX_FILE_SIZE, FileTooLargeError, FileTypeNotAllowedError, StorageQuotaExceededError, FileNotFoundError, FileTenantRequiredError, type FilesOptions, type FileValidation, type UploadInput, } from './files.js';
2
2
  export { MemoryFileStore, type FileRecord, type FileStore, type FilePatch, } from './store.js';
3
3
  export { filesPlugin, fileRoutes, FILES, type FilesPluginOptions } from './plugin.js';
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { Files, FileTooLargeError, FileTypeNotAllowedError, StorageQuotaExceededError, FileNotFoundError, FileTenantRequiredError, } from './files.js';
1
+ export { Files, DEFAULT_MAX_FILE_SIZE, FileTooLargeError, FileTypeNotAllowedError, StorageQuotaExceededError, FileNotFoundError, FileTenantRequiredError, } from './files.js';
2
2
  export { MemoryFileStore, } from './store.js';
3
3
  export { filesPlugin, fileRoutes, FILES } from './plugin.js';
package/dist/plugin.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Disk } from '@basaltkit/storage';
2
- import { type BasaltRoute } from '@basaltkit/fastify';
2
+ import { type BasaltRoute } from '@basaltkit/http';
3
3
  import { Files, type FileValidation, type FilesOptions } from './files.js';
4
4
  import type { FileRecord, FileStore } from './store.js';
5
5
  declare module '@basaltkit/core' {
package/dist/plugin.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createToken, ctx, definePlugin } from '@basaltkit/core';
2
2
  import { STORAGE } from '@basaltkit/storage';
3
- import { route } from '@basaltkit/fastify';
3
+ import { route } from '@basaltkit/http';
4
4
  import { z } from 'zod';
5
5
  import { Files } from './files.js';
6
6
  export const FILES = createToken('files');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basaltkit/files",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "File uploads for Basalt: validation (type/size), per-tenant storage quota, metadata records, signed URLs, and hooks (antivirus/thumbnails) over @basaltkit/storage.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,9 +14,9 @@
14
14
  "dist"
15
15
  ],
16
16
  "dependencies": {
17
- "@basaltkit/core": "^1.1.2",
18
- "@basaltkit/storage": "^1.2.1",
19
- "@basaltkit/fastify": "^1.6.1"
17
+ "@basaltkit/core": "^1.3.0",
18
+ "@basaltkit/http": "^1.10.0",
19
+ "@basaltkit/storage": "^1.3.0"
20
20
  },
21
21
  "peerDependencies": {
22
22
  "zod": "^3.24.0 || ^4.0.0"
@@ -26,6 +26,7 @@
26
26
  "typescript": "^7.0.2",
27
27
  "vitest": "^4.1.11",
28
28
  "zod": "^3.24.0 || ^4.0.0",
29
+ "@basaltkit/fastify": "^1.7.0",
29
30
  "@basaltkit/tsconfig": "^0.24.0"
30
31
  },
31
32
  "publishConfig": {