@byline/host-tanstack-start 1.7.0 → 1.7.1

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.
@@ -0,0 +1,48 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ export interface UploadsHandlerOptions {
9
+ /**
10
+ * URL prefix to match. Trailing slash required. Default `'/uploads/'`.
11
+ * Must match the `baseUrl` you passed to `localStorageProvider`.
12
+ */
13
+ prefix?: string;
14
+ /**
15
+ * Filesystem directory to serve. Default `<process.cwd()>/uploads`.
16
+ * Must match the `uploadDir` you passed to `localStorageProvider`.
17
+ */
18
+ dir?: string;
19
+ /**
20
+ * `Cache-Control` header. Default
21
+ * `'public, max-age=31536000, immutable'` — safe because the local
22
+ * provider UUID-prefixes filenames, so a given URL never points at
23
+ * different bytes across uploads.
24
+ */
25
+ cacheControl?: string;
26
+ /**
27
+ * Extra `extension → MIME type` entries merged on top of the built-in
28
+ * map. Use this to add formats the default map doesn't cover.
29
+ */
30
+ mimeTypes?: Record<string, string>;
31
+ }
32
+ /**
33
+ * Build a `/uploads/*` handler with custom configuration. Returns a
34
+ * function suitable for use inside `createServerEntry({ fetch })` —
35
+ * resolves to a `Response` when the request matched and the file
36
+ * existed, and to `null` when the request should fall through to the
37
+ * framework handler.
38
+ */
39
+ export declare function createUploadsHandler(options?: UploadsHandlerOptions): (request: Request) => Promise<Response | null>;
40
+ /**
41
+ * Default-configured `/uploads/*` handler. Matches
42
+ * `localStorageProvider({ uploadDir: './uploads', baseUrl: '/uploads' })`
43
+ * — the shape produced by `@byline/cli` host scaffolding.
44
+ *
45
+ * Resolved at module-load time, which fixes `process.cwd()` to the
46
+ * server's startup directory.
47
+ */
48
+ export declare const serveUploads: (request: Request) => Promise<Response | null>;
@@ -0,0 +1,69 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ const DEFAULT_MIME = {
4
+ '.avif': 'image/avif',
5
+ '.webp': 'image/webp',
6
+ '.jpg': 'image/jpeg',
7
+ '.jpeg': 'image/jpeg',
8
+ '.png': 'image/png',
9
+ '.gif': 'image/gif',
10
+ '.svg': 'image/svg+xml',
11
+ '.mp4': 'video/mp4',
12
+ '.webm': 'video/webm',
13
+ '.pdf': 'application/pdf',
14
+ '.json': 'application/json',
15
+ '.txt': 'text/plain; charset=utf-8'
16
+ };
17
+ const DEFAULT_CACHE_CONTROL = 'public, max-age=31536000, immutable';
18
+ function createUploadsHandler(options = {}) {
19
+ const prefix = options.prefix ?? '/uploads/';
20
+ const dir = resolve(options.dir ?? `${process.cwd()}/uploads`);
21
+ const cacheControl = options.cacheControl ?? DEFAULT_CACHE_CONTROL;
22
+ const mime = options.mimeTypes ? {
23
+ ...DEFAULT_MIME,
24
+ ...options.mimeTypes
25
+ } : DEFAULT_MIME;
26
+ return async function(request) {
27
+ if ('GET' !== request.method && 'HEAD' !== request.method) return null;
28
+ const url = new URL(request.url);
29
+ if (!url.pathname.startsWith(prefix)) return null;
30
+ let rel;
31
+ try {
32
+ rel = decodeURIComponent(url.pathname.slice(prefix.length));
33
+ } catch {
34
+ return new Response('Bad Request', {
35
+ status: 400
36
+ });
37
+ }
38
+ if (!rel) return null;
39
+ const abs = resolve(dir, rel);
40
+ if (abs !== dir && !abs.startsWith(`${dir}/`)) return new Response('Forbidden', {
41
+ status: 403
42
+ });
43
+ let info;
44
+ try {
45
+ info = await stat(abs);
46
+ } catch {
47
+ return null;
48
+ }
49
+ if (!info.isFile()) return null;
50
+ const dot = abs.lastIndexOf('.');
51
+ const ext = dot >= 0 ? abs.slice(dot).toLowerCase() : '';
52
+ const type = mime[ext] || 'application/octet-stream';
53
+ const headers = new Headers({
54
+ 'Content-Type': type,
55
+ 'Content-Length': info.size.toString(),
56
+ 'Last-Modified': new Date(info.mtimeMs).toUTCString(),
57
+ 'Cache-Control': cacheControl
58
+ });
59
+ if ('HEAD' === request.method) return new Response(null, {
60
+ headers
61
+ });
62
+ const body = await readFile(abs);
63
+ return new Response(body, {
64
+ headers
65
+ });
66
+ };
67
+ }
68
+ const serveUploads = createUploadsHandler();
69
+ export { createUploadsHandler, serveUploads };
@@ -16,7 +16,7 @@ export declare const listRegisteredAbilities: import("@tanstack/react-start").Op
16
16
  label: string;
17
17
  description: string | null;
18
18
  group: string;
19
- source: "collection" | "admin" | "plugin" | "core" | null;
19
+ source: "admin" | "collection" | "plugin" | "core" | null;
20
20
  }[];
21
21
  groups: {
22
22
  group: string;
@@ -25,7 +25,7 @@ export declare const listRegisteredAbilities: import("@tanstack/react-start").Op
25
25
  label: string;
26
26
  description: string | null;
27
27
  group: string;
28
- source: "collection" | "admin" | "plugin" | "core" | null;
28
+ source: "admin" | "collection" | "plugin" | "core" | null;
29
29
  }[];
30
30
  }[];
31
31
  total: number;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "private": false,
4
4
  "type": "module",
5
5
  "license": "MPL-2.0",
6
- "version": "1.7.0",
6
+ "version": "1.7.1",
7
7
  "engines": {
8
8
  "node": ">=20.9.0"
9
9
  },
@@ -101,11 +101,11 @@
101
101
  "react-swipeable": "^7.0.2",
102
102
  "uuid": "^14.0.0",
103
103
  "zod": "^4.4.2",
104
- "@byline/auth": "1.7.0",
105
- "@byline/core": "1.7.0",
106
- "@byline/client": "1.7.0",
107
- "@byline/admin": "1.7.0",
108
- "@byline/ui": "1.7.0"
104
+ "@byline/admin": "1.7.1",
105
+ "@byline/client": "1.7.1",
106
+ "@byline/core": "1.7.1",
107
+ "@byline/auth": "1.7.1",
108
+ "@byline/ui": "1.7.1"
109
109
  },
110
110
  "peerDependencies": {
111
111
  "@tanstack/react-router": "^1.167.0",
@@ -0,0 +1,159 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ /**
10
+ * Runtime `/uploads/*` handler for TanStack Start + Nitro hosts.
11
+ *
12
+ * The local storage provider writes to a directory on disk; this helper
13
+ * streams that directory back on every request so newly-uploaded files
14
+ * appear without a rebuild.
15
+ *
16
+ * Why not `nitro.publicAssets`? `publicAssets` is a build-time copy
17
+ * (`copyPublicAssets` in `nitro/_build/common.mjs`) and the static handler
18
+ * reads from a virtual asset registry baked at build time
19
+ * (`runtime/internal/static.mjs` → `getAsset(id)` from
20
+ * `#nitro/virtual/public-assets`). Files written after the build never
21
+ * land in that registry, so they 404 forever. A request-time handler is
22
+ * the only correct shape for user-uploaded content.
23
+ *
24
+ * Usage in `src/server.ts`:
25
+ *
26
+ * ```ts
27
+ * import handler, { createServerEntry } from '@tanstack/react-start/server-entry'
28
+ * import { serveUploads } from '@byline/host-tanstack-start/integrations/serve-uploads'
29
+ *
30
+ * export default createServerEntry({
31
+ * async fetch(request) {
32
+ * const upload = await serveUploads(request)
33
+ * if (upload) return upload
34
+ * return handler.fetch(request)
35
+ * },
36
+ * })
37
+ * ```
38
+ *
39
+ * The default-configured `serveUploads` matches
40
+ * `localStorageProvider({ uploadDir: './uploads', baseUrl: '/uploads' })`.
41
+ * For a different directory or URL prefix, build your own with
42
+ * `createUploadsHandler({ dir, prefix })`.
43
+ */
44
+
45
+ import { readFile, stat } from 'node:fs/promises'
46
+ import { resolve as resolvePath } from 'node:path'
47
+
48
+ export interface UploadsHandlerOptions {
49
+ /**
50
+ * URL prefix to match. Trailing slash required. Default `'/uploads/'`.
51
+ * Must match the `baseUrl` you passed to `localStorageProvider`.
52
+ */
53
+ prefix?: string
54
+ /**
55
+ * Filesystem directory to serve. Default `<process.cwd()>/uploads`.
56
+ * Must match the `uploadDir` you passed to `localStorageProvider`.
57
+ */
58
+ dir?: string
59
+ /**
60
+ * `Cache-Control` header. Default
61
+ * `'public, max-age=31536000, immutable'` — safe because the local
62
+ * provider UUID-prefixes filenames, so a given URL never points at
63
+ * different bytes across uploads.
64
+ */
65
+ cacheControl?: string
66
+ /**
67
+ * Extra `extension → MIME type` entries merged on top of the built-in
68
+ * map. Use this to add formats the default map doesn't cover.
69
+ */
70
+ mimeTypes?: Record<string, string>
71
+ }
72
+
73
+ const DEFAULT_MIME: Record<string, string> = {
74
+ '.avif': 'image/avif',
75
+ '.webp': 'image/webp',
76
+ '.jpg': 'image/jpeg',
77
+ '.jpeg': 'image/jpeg',
78
+ '.png': 'image/png',
79
+ '.gif': 'image/gif',
80
+ '.svg': 'image/svg+xml',
81
+ '.mp4': 'video/mp4',
82
+ '.webm': 'video/webm',
83
+ '.pdf': 'application/pdf',
84
+ '.json': 'application/json',
85
+ '.txt': 'text/plain; charset=utf-8',
86
+ }
87
+
88
+ const DEFAULT_CACHE_CONTROL = 'public, max-age=31536000, immutable'
89
+
90
+ /**
91
+ * Build a `/uploads/*` handler with custom configuration. Returns a
92
+ * function suitable for use inside `createServerEntry({ fetch })` —
93
+ * resolves to a `Response` when the request matched and the file
94
+ * existed, and to `null` when the request should fall through to the
95
+ * framework handler.
96
+ */
97
+ export function createUploadsHandler(
98
+ options: UploadsHandlerOptions = {}
99
+ ): (request: Request) => Promise<Response | null> {
100
+ const prefix = options.prefix ?? '/uploads/'
101
+ const dir = resolvePath(options.dir ?? `${process.cwd()}/uploads`)
102
+ const cacheControl = options.cacheControl ?? DEFAULT_CACHE_CONTROL
103
+ const mime = options.mimeTypes ? { ...DEFAULT_MIME, ...options.mimeTypes } : DEFAULT_MIME
104
+
105
+ return async function serveUploadsHandler(request: Request): Promise<Response | null> {
106
+ if (request.method !== 'GET' && request.method !== 'HEAD') return null
107
+
108
+ const url = new URL(request.url)
109
+ if (!url.pathname.startsWith(prefix)) return null
110
+
111
+ let rel: string
112
+ try {
113
+ rel = decodeURIComponent(url.pathname.slice(prefix.length))
114
+ } catch {
115
+ return new Response('Bad Request', { status: 400 })
116
+ }
117
+ if (!rel) return null
118
+
119
+ // Path-traversal guard: `resolve` collapses `..` segments before we
120
+ // compare. The resolved absolute path must stay within `dir`.
121
+ const abs = resolvePath(dir, rel)
122
+ if (abs !== dir && !abs.startsWith(`${dir}/`)) {
123
+ return new Response('Forbidden', { status: 403 })
124
+ }
125
+
126
+ let info
127
+ try {
128
+ info = await stat(abs)
129
+ } catch {
130
+ return null // fall through to the framework handler / SPA 404
131
+ }
132
+ if (!info.isFile()) return null
133
+
134
+ const dot = abs.lastIndexOf('.')
135
+ const ext = dot >= 0 ? abs.slice(dot).toLowerCase() : ''
136
+ const type = mime[ext] || 'application/octet-stream'
137
+
138
+ const headers = new Headers({
139
+ 'Content-Type': type,
140
+ 'Content-Length': info.size.toString(),
141
+ 'Last-Modified': new Date(info.mtimeMs).toUTCString(),
142
+ 'Cache-Control': cacheControl,
143
+ })
144
+
145
+ if (request.method === 'HEAD') return new Response(null, { headers })
146
+ const body = await readFile(abs)
147
+ return new Response(body, { headers })
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Default-configured `/uploads/*` handler. Matches
153
+ * `localStorageProvider({ uploadDir: './uploads', baseUrl: '/uploads' })`
154
+ * — the shape produced by `@byline/cli` host scaffolding.
155
+ *
156
+ * Resolved at module-load time, which fixes `process.cwd()` to the
157
+ * server's startup directory.
158
+ */
159
+ export const serveUploads = createUploadsHandler()