@usegraft/assets 0.0.0-canary-20260831153011
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/LICENSE +21 -0
- package/README.md +37 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +101 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anderson Joseph
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @usegraft/assets
|
|
2
|
+
|
|
3
|
+
> S3-compatible object storage for binary assets. Cloudflare R2, MinIO, Tigris, or anything speaking the same API.
|
|
4
|
+
|
|
5
|
+
Part of [Graft](https://github.com/AndersonDesign1/graft), a CMS built so an AI agent is the primary operator.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i @usegraft/assets
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Use
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createStorage, storageConfigFromEnv } from "@usegraft/assets";
|
|
17
|
+
|
|
18
|
+
const storage = createStorage(storageConfigFromEnv());
|
|
19
|
+
await storage.put(key, bytes, { contentType });
|
|
20
|
+
const url = await storage.url(key);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`storageConfigFromEnv` reads `S3_BUCKET`, `S3_ENDPOINT`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY` and the optional `S3_PUBLIC_URL`. Set `S3_PUBLIC_URL` for a stable public base; without it, reads come back as presigned GETs.
|
|
24
|
+
|
|
25
|
+
## Keys
|
|
26
|
+
|
|
27
|
+
`defaultKeyFor` and `contentTypeFor` derive a content-addressed key and a MIME type from a filename, so an agent uploading a file does not have to invent either.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { defaultKeyFor, contentTypeFor } from "@usegraft/assets";
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Upload from the CLI with `graft asset put <file> [key]`, and reference the result from a collection with `field.asset()`.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
MIT. [Repository](https://github.com/AndersonDesign1/graft) · [Changelog](https://github.com/AndersonDesign1/graft/blob/main/packages/assets/CHANGELOG.md)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage configuration, read from S3-compatible env vars.
|
|
3
|
+
* Targets Cloudflare R2 by default; MinIO (docker) is the self-host alternative.
|
|
4
|
+
*/
|
|
5
|
+
interface StorageConfig {
|
|
6
|
+
endpoint: string;
|
|
7
|
+
region: string;
|
|
8
|
+
accessKeyId: string;
|
|
9
|
+
secretAccessKey: string;
|
|
10
|
+
bucket: string;
|
|
11
|
+
/**
|
|
12
|
+
* Public base URL for the bucket (e.g. an R2 public/custom domain). When set,
|
|
13
|
+
* `storage.url()` returns stable public URLs instead of presigned GETs.
|
|
14
|
+
*/
|
|
15
|
+
publicBaseUrl?: string;
|
|
16
|
+
}
|
|
17
|
+
declare function storageConfigFromEnv(env?: Record<string, string | undefined>): StorageConfig;
|
|
18
|
+
|
|
19
|
+
/** MIME type inferred from a file path or asset key extension. */
|
|
20
|
+
declare function contentTypeFor(fileOrKey: string): string;
|
|
21
|
+
/** Default key: assets/<lowercased filename>, sanitized to the asset-key alphabet. */
|
|
22
|
+
declare function defaultKeyFor(file: string): string;
|
|
23
|
+
|
|
24
|
+
interface PresignOptions {
|
|
25
|
+
expiresIn?: number;
|
|
26
|
+
}
|
|
27
|
+
interface Storage {
|
|
28
|
+
put(key: string, body: Uint8Array | string, contentType?: string): Promise<void>;
|
|
29
|
+
get(key: string): Promise<Uint8Array>;
|
|
30
|
+
delete(key: string): Promise<void>;
|
|
31
|
+
exists(key: string): Promise<boolean>;
|
|
32
|
+
/** A presigned PUT URL so a client (or agent) can upload directly. */
|
|
33
|
+
presignPut(key: string, options?: PresignOptions): Promise<string>;
|
|
34
|
+
/** A presigned GET URL for reading from a private bucket. */
|
|
35
|
+
presignGet(key: string, options?: PresignOptions): Promise<string>;
|
|
36
|
+
/**
|
|
37
|
+
* A renderable URL for the key: stable public URL when `publicBaseUrl` is
|
|
38
|
+
* configured, otherwise a presigned GET (default 900s expiry).
|
|
39
|
+
*/
|
|
40
|
+
url(key: string, options?: PresignOptions): Promise<string>;
|
|
41
|
+
}
|
|
42
|
+
declare function createStorage(config?: StorageConfig): Storage;
|
|
43
|
+
|
|
44
|
+
export { type PresignOptions, type Storage, type StorageConfig, contentTypeFor, createStorage, defaultKeyFor, storageConfigFromEnv };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
var REQUIRED = ["S3_ENDPOINT", "S3_ACCESS_KEY", "S3_SECRET_KEY", "S3_BUCKET"];
|
|
3
|
+
function storageConfigFromEnv(env = process.env) {
|
|
4
|
+
const endpoint = env.S3_ENDPOINT;
|
|
5
|
+
const accessKeyId = env.S3_ACCESS_KEY;
|
|
6
|
+
const secretAccessKey = env.S3_SECRET_KEY;
|
|
7
|
+
const bucket = env.S3_BUCKET;
|
|
8
|
+
if (!endpoint || !accessKeyId || !secretAccessKey || !bucket) {
|
|
9
|
+
const missing = REQUIRED.find((key) => !env[key]);
|
|
10
|
+
throw new Error(`Missing required env ${missing}. Set it in .env (see .env.example).`);
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
endpoint,
|
|
14
|
+
region: env.S3_REGION ?? "auto",
|
|
15
|
+
accessKeyId,
|
|
16
|
+
secretAccessKey,
|
|
17
|
+
bucket,
|
|
18
|
+
publicBaseUrl: env.S3_PUBLIC_URL
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/keys.ts
|
|
23
|
+
import { basename, extname } from "path";
|
|
24
|
+
var CONTENT_TYPES = {
|
|
25
|
+
".avif": "image/avif",
|
|
26
|
+
".gif": "image/gif",
|
|
27
|
+
".jpeg": "image/jpeg",
|
|
28
|
+
".jpg": "image/jpeg",
|
|
29
|
+
".pdf": "application/pdf",
|
|
30
|
+
".png": "image/png",
|
|
31
|
+
".svg": "image/svg+xml",
|
|
32
|
+
".webp": "image/webp"
|
|
33
|
+
};
|
|
34
|
+
function contentTypeFor(fileOrKey) {
|
|
35
|
+
return CONTENT_TYPES[extname(fileOrKey).toLowerCase()] ?? "application/octet-stream";
|
|
36
|
+
}
|
|
37
|
+
function defaultKeyFor(file) {
|
|
38
|
+
const name = basename(file).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+/, "");
|
|
39
|
+
return `assets/${name}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/storage.ts
|
|
43
|
+
import { AwsClient } from "aws4fetch";
|
|
44
|
+
function createStorage(config = storageConfigFromEnv()) {
|
|
45
|
+
const aws = new AwsClient({
|
|
46
|
+
accessKeyId: config.accessKeyId,
|
|
47
|
+
secretAccessKey: config.secretAccessKey,
|
|
48
|
+
region: config.region,
|
|
49
|
+
service: "s3"
|
|
50
|
+
});
|
|
51
|
+
const base = `${config.endpoint.replace(/\/+$/, "")}/${config.bucket}`;
|
|
52
|
+
const urlFor = (key) => `${base}/${key.split("/").map(encodeURIComponent).join("/")}`;
|
|
53
|
+
const presign = async (key, method, options) => {
|
|
54
|
+
const url = new URL(urlFor(key));
|
|
55
|
+
url.searchParams.set("X-Amz-Expires", String(options.expiresIn ?? 900));
|
|
56
|
+
const signed = await aws.sign(url.toString(), { method, aws: { signQuery: true } });
|
|
57
|
+
return signed.url;
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
async put(key, body, contentType) {
|
|
61
|
+
const res = await aws.fetch(urlFor(key), {
|
|
62
|
+
method: "PUT",
|
|
63
|
+
body,
|
|
64
|
+
headers: contentType ? { "content-type": contentType } : void 0
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok) throw new Error(`storage put failed (${res.status}): ${await res.text()}`);
|
|
67
|
+
},
|
|
68
|
+
async get(key) {
|
|
69
|
+
const res = await aws.fetch(urlFor(key));
|
|
70
|
+
if (!res.ok) throw new Error(`storage get failed (${res.status})`);
|
|
71
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
72
|
+
},
|
|
73
|
+
async delete(key) {
|
|
74
|
+
const res = await aws.fetch(urlFor(key), { method: "DELETE" });
|
|
75
|
+
if (!res.ok && res.status !== 404) throw new Error(`storage delete failed (${res.status})`);
|
|
76
|
+
},
|
|
77
|
+
async exists(key) {
|
|
78
|
+
const res = await aws.fetch(urlFor(key), { method: "HEAD" });
|
|
79
|
+
return res.ok;
|
|
80
|
+
},
|
|
81
|
+
async presignPut(key, options = {}) {
|
|
82
|
+
return presign(key, "PUT", options);
|
|
83
|
+
},
|
|
84
|
+
async presignGet(key, options = {}) {
|
|
85
|
+
return presign(key, "GET", options);
|
|
86
|
+
},
|
|
87
|
+
async url(key, options = {}) {
|
|
88
|
+
if (config.publicBaseUrl) {
|
|
89
|
+
const publicBase = config.publicBaseUrl.replace(/\/+$/, "");
|
|
90
|
+
return `${publicBase}/${key.split("/").map(encodeURIComponent).join("/")}`;
|
|
91
|
+
}
|
|
92
|
+
return presign(key, "GET", options);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export {
|
|
97
|
+
contentTypeFor,
|
|
98
|
+
createStorage,
|
|
99
|
+
defaultKeyFor,
|
|
100
|
+
storageConfigFromEnv
|
|
101
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usegraft/assets",
|
|
3
|
+
"version": "0.0.0-canary-20260831153011",
|
|
4
|
+
"description": "S3-compatible object storage for binary assets: Cloudflare R2, MinIO, Tigris.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agent",
|
|
7
|
+
"ai",
|
|
8
|
+
"assets",
|
|
9
|
+
"cms",
|
|
10
|
+
"graft",
|
|
11
|
+
"headless-cms",
|
|
12
|
+
"mcp",
|
|
13
|
+
"minio",
|
|
14
|
+
"r2",
|
|
15
|
+
"s3",
|
|
16
|
+
"storage",
|
|
17
|
+
"typescript"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://github.com/AndersonDesign1/graft#readme",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/AndersonDesign1/graft.git",
|
|
24
|
+
"directory": "packages/assets"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"type": "module",
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"module": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"import": "./dist/index.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"aws4fetch": "^1.0.20"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=22.16"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
50
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"test": "vitest run --passWithNoTests"
|
|
53
|
+
}
|
|
54
|
+
}
|