@12-apps/storage 2.5.0 → 2.6.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/storage",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
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.0.0",
51
+ "@12-apps/i18n": "^1.1.0",
52
52
  "@12-apps/typescript-config": "^1.20.1",
53
- "@12-apps/wiring": "^1.10.0",
53
+ "@12-apps/wiring": "^1.12.1",
54
54
  "@aws-sdk/client-s3": "^3.703.0",
55
55
  "@testing-library/react": "^16.1.0",
56
56
  "@types/react": "19.2.2",
@@ -57,11 +57,20 @@ function asWireRoute(route: StorageRoute): {
57
57
  method: StorageRoute['method'];
58
58
  path: string;
59
59
  kind: 'authenticated' | 'public';
60
+ wildcardParam?: string;
60
61
  handle(request: WireRequest): Promise<WireRouteAnswer>;
61
62
  } {
62
63
  return {
63
64
  method: route.method,
64
65
  path: route.path,
66
+ // Forwarded, and it has to be: the serve route's key is FOUR segments
67
+ // (`products/<scope>/<uuid>/card-320.webp`), so an adapter that registers
68
+ // only `path` answers the prefix and 404s every real object. `./hono`
69
+ // spells the Hono form from this name; the contract carries the name so any
70
+ // other adapter can spell its own. Dropping it here is what made this
71
+ // package unmountable through a consumer at all — silently, because the
72
+ // upload route worked and only reads 404'd.
73
+ ...(route.wildcardParam === undefined ? {} : { wildcardParam: route.wildcardParam }),
65
74
  // The descriptor's own `auth` flag IS the contract's route kind: a
66
75
  // `public` object read is anonymous by design, and the host's gates read
67
76
  // this rather than a second table that could disagree with it.
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 messages: StorageMessages = config.messages({ limit: megabytes(config.maxBytes) });
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),
@@ -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
- messages: StorageMessages;
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: deps.messages.unsupported_content_type } };
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: deps.messages.file_too_large } };
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