@12-apps/storage 2.1.0 → 2.2.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 +9 -5
- package/src/manifest/index.ts +34 -0
- package/src/manifest/server.ts +95 -0
package/package.json
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/storage",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.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
|
-
"./
|
|
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.
|
|
46
|
+
"@12-apps/ui": "^6.3.1",
|
|
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;
|