@12-apps/storage 2.5.0 → 2.6.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/package.json +3 -3
- package/src/problems.ts +9 -0
- package/src/server/create-api-storage.ts +6 -3
- package/src/server/routes.ts +21 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/storage",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Image uploads that land at YOUR OWN ORIGIN, never at the bucket (12-20). Isomorphic core (format allowlist, upload ceiling, object-key grammar, rendition set, refusal taxonomy, wire schemas), host-mounted backend surface (./server: createApiStorage, the local-disk driver, the image-pipeline port, the orphan reclaim), Hono adapter (./hono), the S3-compatible driver behind its own subpath (./s3) and the bound picker + upload hook (./react). Standardized adoption contract in ADOPTING.md.",
|
|
6
6
|
"exports": {
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@12-apps/eslint-config": "^1.21.1",
|
|
51
|
-
"@12-apps/i18n": "^1.
|
|
51
|
+
"@12-apps/i18n": "^1.1.0",
|
|
52
52
|
"@12-apps/typescript-config": "^1.20.1",
|
|
53
|
-
"@12-apps/wiring": "^1.
|
|
53
|
+
"@12-apps/wiring": "^1.11.0",
|
|
54
54
|
"@aws-sdk/client-s3": "^3.703.0",
|
|
55
55
|
"@testing-library/react": "^16.1.0",
|
|
56
56
|
"@types/react": "19.2.2",
|
package/src/problems.ts
CHANGED
|
@@ -54,6 +54,15 @@ export type StorageMessages = Readonly<Record<StorageProblem, string>>;
|
|
|
54
54
|
export interface StorageMessageContext {
|
|
55
55
|
/** `8 MB`, already formatted. */
|
|
56
56
|
limit: string;
|
|
57
|
+
/**
|
|
58
|
+
* The language to answer this caller in, as a BCP-47 tag.
|
|
59
|
+
*
|
|
60
|
+
* This field is why `messages` was already the right SHAPE: a factory called
|
|
61
|
+
* per refusal can answer per reader, where a value handed over at the mount
|
|
62
|
+
* cannot. Absent means the host's adapter populated nothing — a single
|
|
63
|
+
* -audience host — and the factory should then answer as it always did.
|
|
64
|
+
*/
|
|
65
|
+
locale?: string;
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
/**
|
|
@@ -172,11 +172,14 @@ const SILENT: StorageLogger = { error: () => undefined };
|
|
|
172
172
|
export function createApiStorage(config: ApiStorageConfig): ApiStorage {
|
|
173
173
|
const specs = config.renditions ?? CATALOG_RENDITIONS;
|
|
174
174
|
const keyPrefix = config.keyPrefix ?? DEFAULT_KEY_PREFIX;
|
|
175
|
-
const
|
|
175
|
+
const limit = megabytes(config.maxBytes);
|
|
176
|
+
// A host write holds no request and no reader, so it takes the words resolved
|
|
177
|
+
// in the deployment's own language. The ROUTES resolve per caller instead —
|
|
178
|
+
// see `storageRoutes` below, which is handed the factory rather than this.
|
|
176
179
|
const writeDeps: StoreImageDeps = {
|
|
177
180
|
driver: config.driver,
|
|
178
181
|
pipeline: config.imagePipeline,
|
|
179
|
-
messages,
|
|
182
|
+
messages: config.messages({ limit }),
|
|
180
183
|
maxBytes: config.maxBytes,
|
|
181
184
|
specs,
|
|
182
185
|
keyPrefix,
|
|
@@ -192,7 +195,7 @@ export function createApiStorage(config: ApiStorageConfig): ApiStorage {
|
|
|
192
195
|
const resolve = (key: string): string => config.driver.publicUrl(key);
|
|
193
196
|
|
|
194
197
|
return {
|
|
195
|
-
routes: storageRoutes(writeDeps),
|
|
198
|
+
routes: storageRoutes({ ...writeDeps, messages: config.messages, limit }),
|
|
196
199
|
limits: {
|
|
197
200
|
maxBytes: config.maxBytes,
|
|
198
201
|
maxBytesLabel: megabytes(config.maxBytes),
|
package/src/server/routes.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CONTENT_TYPE_BY_EXTENSION, acceptedContentTypeOf } from '../content-types';
|
|
2
2
|
import { parseObjectMemberKey } from '../keys';
|
|
3
3
|
import { STORAGE_PATHS } from '../paths';
|
|
4
|
-
import { isStorageProblem, type StorageMessages } from '../problems';
|
|
4
|
+
import { isStorageProblem, type StorageMessageContext, type StorageMessages } from '../problems';
|
|
5
5
|
import type { StorageDriver } from './driver';
|
|
6
6
|
import { readBodyCapped } from './read-body';
|
|
7
7
|
import { storeImage, type StoreImageDeps } from './store-image';
|
|
@@ -54,6 +54,12 @@ export interface StorageRouteContext {
|
|
|
54
54
|
actor: StorageActor | null;
|
|
55
55
|
params: Record<string, string | undefined>;
|
|
56
56
|
request: Request;
|
|
57
|
+
/**
|
|
58
|
+
* The language to answer in — the same field `@12-apps/wiring`'s
|
|
59
|
+
* `WireRequest` carries, mirrored here because this surface builds its own
|
|
60
|
+
* context. Populated by the host's adapter; absent is not an error.
|
|
61
|
+
*/
|
|
62
|
+
locale?: string;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
/** A JSON answer, an object's bytes, or a redirect to where the object lives. */
|
|
@@ -76,9 +82,15 @@ export interface StorageRoute {
|
|
|
76
82
|
handle(context: StorageRouteContext): Promise<StorageRouteResponse>;
|
|
77
83
|
}
|
|
78
84
|
|
|
79
|
-
interface RoutesDeps extends StoreImageDeps {
|
|
85
|
+
interface RoutesDeps extends Omit<StoreImageDeps, "messages"> {
|
|
80
86
|
driver: StorageDriver;
|
|
81
|
-
|
|
87
|
+
/**
|
|
88
|
+
* The factory, unresolved, plus the limit it needs. Resolving into a value
|
|
89
|
+
* here is what would re-freeze the language into the mount — this object is
|
|
90
|
+
* built once, per process.
|
|
91
|
+
*/
|
|
92
|
+
messages: (context: StorageMessageContext) => StorageMessages;
|
|
93
|
+
limit: string;
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
/** Keys are content-addressed (a uuid), so an object may be cached forever. */
|
|
@@ -96,9 +108,12 @@ async function handleUpload(
|
|
|
96
108
|
// override it (`WebStorageMessages.forbidden`).
|
|
97
109
|
return { status: 403, body: { error: 'forbidden' } };
|
|
98
110
|
}
|
|
111
|
+
// Worded HERE, per request, from the tag the adapter put on the context —
|
|
112
|
+
// never at the mount, which ran once at boot.
|
|
113
|
+
const messages = deps.messages({ limit: deps.limit, locale: context.locale });
|
|
99
114
|
const contentType = acceptedContentTypeOf(context.request.headers.get('content-type'));
|
|
100
115
|
if (!contentType) {
|
|
101
|
-
return { status: 400, body: { error:
|
|
116
|
+
return { status: 400, body: { error: messages.unsupported_content_type } };
|
|
102
117
|
}
|
|
103
118
|
// Capped WHILE STREAMING, so an oversize body is refused without ever being
|
|
104
119
|
// held whole in memory.
|
|
@@ -111,12 +126,12 @@ async function handleUpload(
|
|
|
111
126
|
// "o limite é 8 MB": a ceiling nothing enforced, in the one package whose
|
|
112
127
|
// headline claim is that there is exactly ONE number. The sentence is built
|
|
113
128
|
// from this mount's `maxBytes` and carries this mount's `messages` overrides.
|
|
114
|
-
return { status: 413, body: { error:
|
|
129
|
+
return { status: 413, body: { error: messages.file_too_large } };
|
|
115
130
|
}
|
|
116
131
|
try {
|
|
117
132
|
// No byte check here: `storeImage` owns it, so the endpoint and a host write
|
|
118
133
|
// holding raw bytes cannot disagree about whether it ran (see store-image.ts).
|
|
119
|
-
const imageKey = await storeImage(deps, { bytes, contentType, scope: actor.scope });
|
|
134
|
+
const imageKey = await storeImage({ ...deps, messages }, { bytes, contentType, scope: actor.scope });
|
|
120
135
|
return { status: 200, body: { data: { imageKey } } };
|
|
121
136
|
} catch (error) {
|
|
122
137
|
// The write path speaks in StorageProblemError precisely so both entrances
|