@12-apps/storage 2.1.0 → 2.3.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 CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "@12-apps/storage",
3
- "version": "2.1.0",
3
+ "version": "2.3.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": {
7
7
  ".": "./src/index.ts",
8
- "./server": "./src/server/index.ts",
9
- "./react": "./src/react/index.ts",
10
8
  "./hono": "./src/hono/index.ts",
9
+ "./manifest": "./src/manifest/index.ts",
10
+ "./manifest/server": "./src/manifest/server.ts",
11
+ "./package.json": "./package.json",
12
+ "./react": "./src/react/index.ts",
11
13
  "./s3": "./src/s3/index.ts",
12
- "./package.json": "./package.json"
14
+ "./server": "./src/server/index.ts"
13
15
  },
14
16
  "scripts": {
15
17
  "clean": "rm -rf node_modules coverage",
@@ -20,6 +22,7 @@
20
22
  "typecheck": "tsc --noEmit"
21
23
  },
22
24
  "peerDependencies": {
25
+ "@12-apps/wiring": ">=1.9.0",
23
26
  "@aws-sdk/client-s3": ">=3.700.0",
24
27
  "hono": ">=4.0.0",
25
28
  "react": ">=19.0.0",
@@ -40,12 +43,13 @@
40
43
  }
41
44
  },
42
45
  "dependencies": {
43
- "@12-apps/ui": "^6.2.0",
46
+ "@12-apps/ui": "^6.4.0",
44
47
  "zod": "^4.3.5"
45
48
  },
46
49
  "devDependencies": {
47
50
  "@12-apps/eslint-config": "^1.21.1",
48
51
  "@12-apps/typescript-config": "^1.20.1",
52
+ "@12-apps/wiring": "^1.10.0",
49
53
  "@aws-sdk/client-s3": "^3.703.0",
50
54
  "@testing-library/react": "^16.1.0",
51
55
  "@types/react": "19.2.2",
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `@12-apps/storage/manifest` — the SHARED wiring manifest.
3
+ *
4
+ * Identity and the runtime inventory. Three absences are deliberate rather
5
+ * than unfinished:
6
+ *
7
+ * - **No `db`.** This package owns no table. What an uploaded object leaves
8
+ * behind is a KEY, and the row that key hangs off is the host's own — a
9
+ * product image, a store logo — so there is nothing here to compose.
10
+ * - **No `env`.** `./server`'s own header states the rule this manifest would
11
+ * otherwise have to restate: there are no `process.env` reads in shipped
12
+ * source, because every deployment-shaped decision (the driver, the byte
13
+ * ceiling, the key prefix, the bucket) is an ARGUMENT. A package that reads
14
+ * the environment decides for its host; this one asks.
15
+ * - **No `web` inventory.** The React half is components a host renders, not
16
+ * a mountable surface with its own routes.
17
+ *
18
+ * `observability` is mandatory here because `http` is: a refused upload or a
19
+ * driver that could not be reached files under `storage` rather than under
20
+ * whichever host happened to mount it.
21
+ *
22
+ * `@12-apps/wiring` is a TYPE-ONLY devDependency (the report-builder move):
23
+ * the manifests are plain `satisfies`-checked values, and the producer
24
+ * factories' runtime assertions run in this package's own test suite.
25
+ */
26
+
27
+ import type { PackageManifest } from '@12-apps/wiring';
28
+
29
+ export const storageManifest = {
30
+ name: '@12-apps/storage',
31
+ contract: 1,
32
+ observability: { namespace: 'storage' },
33
+ server: ['http'],
34
+ } as const satisfies PackageManifest;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * `@12-apps/storage/manifest/server` — the server capabilities.
3
+ *
4
+ * `http.create` wraps `createApiStorage` in a WIRE VIEW that finally makes the
5
+ * response rendering the PACKAGE's. `StorageRouteResponse` is a three-way
6
+ * union — a JSON answer, an object's bytes, or a redirect to where the object
7
+ * lives — and until now every host turned those three arms into a `Response`
8
+ * itself. That is mechanism, identical for every host, and one arm of it is
9
+ * a security detail rather than a formatting one:
10
+ *
11
+ * const body = response.bytes.slice().buffer;
12
+ *
13
+ * The `.slice()` is load-bearing. A `Uint8Array` from Node is a view over a
14
+ * POOLED allocation, so handing `.buffer` straight to `Response` serves the
15
+ * client whatever else happens to share that pool. A host that re-derives this
16
+ * renderer has to know that; a host that binds this manifest does not.
17
+ *
18
+ * The bytes and redirect arms therefore answer the contract's RAW half
19
+ * (`{ response }`), which wiring 1.9.0 added for exactly this: answers
20
+ * `{ status, body }` cannot express — a stream, a redirect whose headers are
21
+ * the payload, bytes with their own content type.
22
+ */
23
+
24
+ import type { AnyServerManifest, WireRequest, WireRouteAnswer } from '@12-apps/wiring';
25
+
26
+ import {
27
+ createApiStorage,
28
+ type ApiStorage,
29
+ type ApiStorageConfig,
30
+ type StorageRoute,
31
+ type StorageRouteResponse,
32
+ } from '../server';
33
+
34
+ /** The descriptor's chosen answer, as the wiring contract carries it. */
35
+ export function asWireAnswer(response: StorageRouteResponse): WireRouteAnswer {
36
+ if ('redirect' in response) {
37
+ return { response: Response.redirect(response.redirect, 302) };
38
+ }
39
+ if ('bytes' in response) {
40
+ // Its OWN ArrayBuffer — see the note above; a view over the pool leaks.
41
+ const body = response.bytes.slice().buffer as ArrayBuffer;
42
+ return {
43
+ response: new Response(body, {
44
+ status: response.status,
45
+ headers: {
46
+ ...response.headers,
47
+ 'content-length': String(response.bytes.byteLength),
48
+ },
49
+ }),
50
+ };
51
+ }
52
+ return { status: response.status, body: response.body };
53
+ }
54
+
55
+ /** One `StorageRoute` as the wiring contract reads it. */
56
+ function asWireRoute(route: StorageRoute): {
57
+ method: StorageRoute['method'];
58
+ path: string;
59
+ kind: 'authenticated' | 'public';
60
+ handle(request: WireRequest): Promise<WireRouteAnswer>;
61
+ } {
62
+ return {
63
+ method: route.method,
64
+ path: route.path,
65
+ // The descriptor's own `auth` flag IS the contract's route kind: a
66
+ // `public` object read is anonymous by design, and the host's gates read
67
+ // this rather than a second table that could disagree with it.
68
+ kind: route.auth === 'public' ? 'public' : 'authenticated',
69
+ handle: async (request) => {
70
+ if (!request.request) {
71
+ throw new Error('@12-apps/storage needs the raw request — bind an adapter that forwards it.');
72
+ }
73
+ return asWireAnswer(
74
+ await route.handle({
75
+ actor: (request.actor ?? null) as Parameters<StorageRoute['handle']>[0]['actor'],
76
+ params: request.params,
77
+ request: request.request,
78
+ }),
79
+ );
80
+ },
81
+ };
82
+ }
83
+
84
+ /** `createApiStorage`, its routes re-shaped for the aggregate. */
85
+ export function createWireApiStorage(
86
+ config: ApiStorageConfig,
87
+ ): Omit<ApiStorage, 'routes'> & { routes: ReturnType<typeof asWireRoute>[] } {
88
+ const api = createApiStorage(config);
89
+ return { ...api, routes: api.routes.map(asWireRoute) };
90
+ }
91
+
92
+ export const storageServerManifest = {
93
+ name: '@12-apps/storage',
94
+ http: { create: createWireApiStorage },
95
+ } as const satisfies AnyServerManifest;
@@ -85,6 +85,7 @@ export function createWebStorage(config: WebStorageConfig): WebStorage {
85
85
  value={imageKey}
86
86
  onChange={setImageKey}
87
87
  label={messages.fieldLabel}
88
+ removeLabel={messages.fieldRemove}
88
89
  helperText={messages.fieldHelper}
89
90
  />
90
91
  </Stack>
@@ -100,13 +100,15 @@ export interface WebStorageMessages {
100
100
  /**
101
101
  * The standalone upload page's own chrome — heading, the one-line
102
102
  * instruction (which carries the ceiling, same rule as the refusals), the
103
- * field label and its helper. These rendered as literals inside
104
- * `createWebStorage` until copy became required config.
103
+ * field label, its helper, and the button that clears a chosen image.
104
+ * These rendered as literals inside `createWebStorage` until copy became
105
+ * required config.
105
106
  */
106
107
  pageTitle: string;
107
108
  pageIntro: string;
108
109
  fieldLabel: string;
109
110
  fieldHelper: string;
111
+ fieldRemove: string;
110
112
  }
111
113
 
112
114
  /** Copy that only makes sense with the configured ceiling in it. */
@@ -34,7 +34,10 @@ export interface ImageFieldProps {
34
34
  previewUrl?: string | null;
35
35
  /** Called with the new key, or `null` when the image is removed. */
36
36
  onChange: (imageKey: string | null) => void;
37
- label?: string;
37
+ /** The dropzone's own label. REQUIRED — this package ships no default copy. */
38
+ label: string;
39
+ /** The button that clears a chosen image. REQUIRED, for the same reason. */
40
+ removeLabel: string;
38
41
  helperText?: string;
39
42
  /** Passed through per upload — `{ optimize: false }` for a rendered canvas. */
40
43
  uploadOptions?: UploadOptions;
@@ -89,7 +92,7 @@ export function ImageField(props: BoundImageFieldProps): JSX.Element {
89
92
  ) : null}
90
93
  <UploadButton
91
94
  variant="dropzone"
92
- label={props.label ?? 'Enviar imagem'}
95
+ label={props.label}
93
96
  accept={ACCEPT}
94
97
  disabled={props.disabled || uploader.uploading}
95
98
  uploading={uploader.uploading}
@@ -100,7 +103,7 @@ export function ImageField(props: BoundImageFieldProps): JSX.Element {
100
103
  {preview ? (
101
104
  <Box>
102
105
  <Button variant="text" onClick={handleRemove} dataTestId={`${testId}-remove`}>
103
- Remover imagem
106
+ {props.removeLabel}
104
107
  </Button>
105
108
  </Box>
106
109
  ) : null}
@@ -34,5 +34,6 @@ export function PT_BR_WEB_STORAGE_MESSAGES(
34
34
  pageIntro: `Envie um PNG, JPG, WebP ou GIF de até ${context.limit}.`,
35
35
  fieldLabel: 'Foto do produto',
36
36
  fieldHelper: 'A imagem é reduzida no navegador antes do envio.',
37
+ fieldRemove: 'Remover imagem',
37
38
  };
38
39
  }