@nymbalhq/media-s3 0.0.0-stage → 1.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.
@@ -0,0 +1,69 @@
1
+ import { MediaStorage, AdapterContext } from '@nymbalhq/types';
2
+
3
+ /** Where the URL of an object starts, and where its key starts in the bucket. */
4
+ interface S3ObjectUrlOptions {
5
+ bucket: string;
6
+ /** `us-east-1` when not given. */
7
+ region?: string;
8
+ /** An S3-compatible API (MinIO, R2); its objects are served path-style under it. */
9
+ endpoint?: string;
10
+ /** Prepended to every key, its edge slashes trimmed; none when empty. */
11
+ prefix?: string;
12
+ /** Where the bucket's objects are served from (a CDN), when not the bucket's own URL. */
13
+ publicUrl?: string;
14
+ }
15
+ interface S3MediaStorageOptions extends S3ObjectUrlOptions {
16
+ /** True by default when an endpoint is set: an S3-compatible server rarely has per-bucket hostnames. */
17
+ forcePathStyle?: boolean;
18
+ /** For tests only: a store gets its credentials from the AWS SDK's default chain. */
19
+ credentials?: {
20
+ accessKeyId: string;
21
+ secretAccessKey: string;
22
+ };
23
+ }
24
+ declare const S3_MEDIA_CAPABILITIES: readonly string[];
25
+ declare const DEFAULT_S3_REGION = "us-east-1";
26
+ /** The connection fields every client of this package is made from. */
27
+ interface S3ClientOptions {
28
+ /** `us-east-1` when not given. */
29
+ region?: string;
30
+ endpoint?: string;
31
+ /** True by default when an endpoint is set: an S3-compatible server rarely has per-bucket hostnames. */
32
+ forcePathStyle?: boolean;
33
+ /** When not given, the AWS SDK's default chain provides them (the environment, a profile, a role). */
34
+ credentials?: {
35
+ accessKeyId: string;
36
+ secretAccessKey: string;
37
+ };
38
+ }
39
+ /** The public URL of the object stored under `key`: the base, then each segment of the object key percent-encoded. */
40
+ declare function s3ObjectUrl(options: S3ObjectUrlOptions, key: string): string;
41
+ /**
42
+ * Media stored as objects in a bucket the operator owns: it never creates buckets or sets policies, so the bucket (or
43
+ * the `publicUrl` in front of it) must already serve its objects publicly.
44
+ */
45
+ declare function createS3MediaStorage(options: S3MediaStorageOptions): MediaStorage;
46
+
47
+ interface S3ObjectWriterOptions extends S3ClientOptions {
48
+ bucket: string;
49
+ /** Prepended to every key, its edge slashes trimmed; none when empty. */
50
+ prefix?: string;
51
+ }
52
+ /** Writes whole objects; what it writes is read back only with credentials. */
53
+ interface S3ObjectWriter {
54
+ put(key: string, body: Uint8Array, contentType: string): Promise<void>;
55
+ }
56
+ /**
57
+ * Private objects in a bucket the operator owns, such as the event archive: unlike media storage it answers no URL,
58
+ * and it sets no ACL, so an object is as private as its bucket. Never point it at the public media bucket.
59
+ */
60
+ declare function createS3ObjectWriter(options: S3ObjectWriterOptions): S3ObjectWriter;
61
+
62
+ /**
63
+ * The kernel's entry: `infrastructure.media: 's3'`, with the `s3` block as the platform resolved it (its environment
64
+ * fallbacks and defaults applied). Credentials are never in it: the AWS SDK's default chain provides them.
65
+ */
66
+ declare function createAdapter(ctx: AdapterContext): MediaStorage;
67
+
68
+ export { DEFAULT_S3_REGION, S3_MEDIA_CAPABILITIES, createAdapter, createS3MediaStorage, createS3ObjectWriter, s3ObjectUrl };
69
+ export type { S3MediaStorageOptions, S3ObjectUrlOptions, S3ObjectWriter, S3ObjectWriterOptions };
package/dist/index.js ADDED
@@ -0,0 +1,120 @@
1
+ // src/index.ts
2
+ import { ConfigError } from "@nymbalhq/types";
3
+
4
+ // src/s3-media-storage.ts
5
+ import { HeadBucketCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
6
+ import { boundedHealthCheck } from "@nymbalhq/types";
7
+ var S3_MEDIA_CAPABILITIES = ["put"];
8
+ var DEFAULT_S3_REGION = "us-east-1";
9
+ var trimTrailingSlashes = (s) => s.replace(/\/+$/, "");
10
+ function s3Client(options) {
11
+ return new S3Client({
12
+ region: options.region ?? DEFAULT_S3_REGION,
13
+ forcePathStyle: options.forcePathStyle ?? options.endpoint !== void 0,
14
+ ...options.endpoint !== void 0 && { endpoint: options.endpoint },
15
+ ...options.credentials !== void 0 && { credentials: options.credentials }
16
+ });
17
+ }
18
+ function objectKey(prefix, key) {
19
+ const trimmed = (prefix ?? "").replace(/^\/+|\/+$/g, "");
20
+ return trimmed === "" ? key : `${trimmed}/${key}`;
21
+ }
22
+ function baseUrl(options) {
23
+ if (options.publicUrl !== void 0) return trimTrailingSlashes(options.publicUrl);
24
+ if (options.endpoint !== void 0) {
25
+ return `${trimTrailingSlashes(options.endpoint)}/${options.bucket}`;
26
+ }
27
+ return `https://${options.bucket}.s3.${options.region ?? DEFAULT_S3_REGION}.amazonaws.com`;
28
+ }
29
+ function s3ObjectUrl(options, key) {
30
+ const path = objectKey(options.prefix, key).split("/").map(encodeURIComponent).join("/");
31
+ return `${baseUrl(options)}/${path}`;
32
+ }
33
+ function createS3MediaStorage(options) {
34
+ const client = s3Client(options);
35
+ return {
36
+ capabilities: () => S3_MEDIA_CAPABILITIES,
37
+ async put(key, body, contentType) {
38
+ await client.send(
39
+ new PutObjectCommand({
40
+ Bucket: options.bucket,
41
+ Key: objectKey(options.prefix, key),
42
+ Body: body,
43
+ ContentType: contentType
44
+ })
45
+ );
46
+ return { url: s3ObjectUrl(options, key) };
47
+ },
48
+ // HeadBucket reads the bucket's existence and our access to it; aborted at the bound, retries included.
49
+ healthCheck() {
50
+ return boundedHealthCheck(
51
+ async (signal) => {
52
+ await client.send(new HeadBucketCommand({ Bucket: options.bucket }), {
53
+ abortSignal: signal
54
+ });
55
+ return { status: "healthy", detail: options.bucket };
56
+ },
57
+ {
58
+ redact: [
59
+ options.credentials?.accessKeyId ?? "",
60
+ options.credentials?.secretAccessKey ?? ""
61
+ ]
62
+ }
63
+ );
64
+ }
65
+ };
66
+ }
67
+
68
+ // src/s3-object-writer.ts
69
+ import { PutObjectCommand as PutObjectCommand2 } from "@aws-sdk/client-s3";
70
+ function createS3ObjectWriter(options) {
71
+ const client = s3Client(options);
72
+ return {
73
+ async put(key, body, contentType) {
74
+ await client.send(
75
+ new PutObjectCommand2({
76
+ Bucket: options.bucket,
77
+ Key: objectKey(options.prefix, key),
78
+ Body: body,
79
+ ContentType: contentType
80
+ })
81
+ );
82
+ }
83
+ };
84
+ }
85
+
86
+ // src/index.ts
87
+ var optionalString = (value) => typeof value === "string" && value !== "" ? value : void 0;
88
+ function createAdapter(ctx) {
89
+ if (ctx.slot !== "media") {
90
+ throw new ConfigError(`@nymbalhq/media-s3 serves media, not ${ctx.slot}`, {
91
+ code: "adapter.wrong_slot",
92
+ context: { slot: ctx.slot }
93
+ });
94
+ }
95
+ const bucket = optionalString(ctx.config["bucket"]);
96
+ if (bucket === void 0) {
97
+ throw new ConfigError("@nymbalhq/media-s3 needs a bucket", {
98
+ code: "config.invalid",
99
+ context: { slot: ctx.slot }
100
+ });
101
+ }
102
+ const options = { bucket };
103
+ for (const field of ["region", "endpoint", "prefix", "publicUrl"]) {
104
+ const value = optionalString(ctx.config[field]);
105
+ if (value !== void 0) options[field] = value;
106
+ }
107
+ if (typeof ctx.config["forcePathStyle"] === "boolean") {
108
+ options.forcePathStyle = ctx.config["forcePathStyle"];
109
+ }
110
+ return createS3MediaStorage(options);
111
+ }
112
+ export {
113
+ DEFAULT_S3_REGION,
114
+ S3_MEDIA_CAPABILITIES,
115
+ createAdapter,
116
+ createS3MediaStorage,
117
+ createS3ObjectWriter,
118
+ s3ObjectUrl
119
+ };
120
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/s3-media-storage.ts","../src/s3-object-writer.ts"],"sourcesContent":["import { ConfigError, type AdapterContext, type MediaStorage } from '@nymbalhq/types'\nimport { createS3MediaStorage, type S3MediaStorageOptions } from './s3-media-storage.js'\n\nexport {\n createS3MediaStorage,\n DEFAULT_S3_REGION,\n S3_MEDIA_CAPABILITIES,\n s3ObjectUrl,\n type S3MediaStorageOptions,\n type S3ObjectUrlOptions,\n} from './s3-media-storage.js'\nexport {\n createS3ObjectWriter,\n type S3ObjectWriter,\n type S3ObjectWriterOptions,\n} from './s3-object-writer.js'\n\nconst optionalString = (value: unknown): string | undefined =>\n typeof value === 'string' && value !== '' ? value : undefined\n\n/**\n * The kernel's entry: `infrastructure.media: 's3'`, with the `s3` block as the platform resolved it (its environment\n * fallbacks and defaults applied). Credentials are never in it: the AWS SDK's default chain provides them.\n */\nexport function createAdapter(ctx: AdapterContext): MediaStorage {\n if (ctx.slot !== 'media') {\n throw new ConfigError(`@nymbalhq/media-s3 serves media, not ${ctx.slot}`, {\n code: 'adapter.wrong_slot',\n context: { slot: ctx.slot },\n })\n }\n const bucket = optionalString(ctx.config['bucket'])\n if (bucket === undefined) {\n throw new ConfigError('@nymbalhq/media-s3 needs a bucket', {\n code: 'config.invalid',\n context: { slot: ctx.slot },\n })\n }\n const options: S3MediaStorageOptions = { bucket }\n for (const field of ['region', 'endpoint', 'prefix', 'publicUrl'] as const) {\n const value = optionalString(ctx.config[field])\n if (value !== undefined) options[field] = value\n }\n if (typeof ctx.config['forcePathStyle'] === 'boolean') {\n options.forcePathStyle = ctx.config['forcePathStyle']\n }\n return createS3MediaStorage(options)\n}\n","import { HeadBucketCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'\nimport { boundedHealthCheck, type HealthReport, type MediaStorage } from '@nymbalhq/types'\n\n/** Where the URL of an object starts, and where its key starts in the bucket. */\nexport interface S3ObjectUrlOptions {\n bucket: string\n /** `us-east-1` when not given. */\n region?: string\n /** An S3-compatible API (MinIO, R2); its objects are served path-style under it. */\n endpoint?: string\n /** Prepended to every key, its edge slashes trimmed; none when empty. */\n prefix?: string\n /** Where the bucket's objects are served from (a CDN), when not the bucket's own URL. */\n publicUrl?: string\n}\n\nexport interface S3MediaStorageOptions extends S3ObjectUrlOptions {\n /** True by default when an endpoint is set: an S3-compatible server rarely has per-bucket hostnames. */\n forcePathStyle?: boolean\n /** For tests only: a store gets its credentials from the AWS SDK's default chain. */\n credentials?: { accessKeyId: string; secretAccessKey: string }\n}\n\nexport const S3_MEDIA_CAPABILITIES: readonly string[] = ['put']\n\nexport const DEFAULT_S3_REGION = 'us-east-1'\n\nconst trimTrailingSlashes = (s: string): string => s.replace(/\\/+$/, '')\n\n/** The connection fields every client of this package is made from. */\nexport interface S3ClientOptions {\n /** `us-east-1` when not given. */\n region?: string\n endpoint?: string\n /** True by default when an endpoint is set: an S3-compatible server rarely has per-bucket hostnames. */\n forcePathStyle?: boolean\n /** When not given, the AWS SDK's default chain provides them (the environment, a profile, a role). */\n credentials?: { accessKeyId: string; secretAccessKey: string }\n}\n\nexport function s3Client(options: S3ClientOptions): S3Client {\n return new S3Client({\n region: options.region ?? DEFAULT_S3_REGION,\n forcePathStyle: options.forcePathStyle ?? options.endpoint !== undefined,\n ...(options.endpoint !== undefined && { endpoint: options.endpoint }),\n ...(options.credentials !== undefined && { credentials: options.credentials }),\n })\n}\n\n/** The object's key in the bucket. Only the prefix is normalised: the key stays verbatim, so distinct keys stay distinct objects. */\nexport function objectKey(prefix: string | undefined, key: string): string {\n const trimmed = (prefix ?? '').replace(/^\\/+|\\/+$/g, '')\n return trimmed === '' ? key : `${trimmed}/${key}`\n}\n\nfunction baseUrl(options: S3ObjectUrlOptions): string {\n if (options.publicUrl !== undefined) return trimTrailingSlashes(options.publicUrl)\n if (options.endpoint !== undefined) {\n return `${trimTrailingSlashes(options.endpoint)}/${options.bucket}`\n }\n return `https://${options.bucket}.s3.${options.region ?? DEFAULT_S3_REGION}.amazonaws.com`\n}\n\n/** The public URL of the object stored under `key`: the base, then each segment of the object key percent-encoded. */\nexport function s3ObjectUrl(options: S3ObjectUrlOptions, key: string): string {\n const path = objectKey(options.prefix, key).split('/').map(encodeURIComponent).join('/')\n return `${baseUrl(options)}/${path}`\n}\n\n/**\n * Media stored as objects in a bucket the operator owns: it never creates buckets or sets policies, so the bucket (or\n * the `publicUrl` in front of it) must already serve its objects publicly.\n */\nexport function createS3MediaStorage(options: S3MediaStorageOptions): MediaStorage {\n const client = s3Client(options)\n return {\n capabilities: () => S3_MEDIA_CAPABILITIES,\n async put(key, body, contentType) {\n await client.send(\n new PutObjectCommand({\n Bucket: options.bucket,\n Key: objectKey(options.prefix, key),\n Body: body,\n ContentType: contentType,\n }),\n )\n return { url: s3ObjectUrl(options, key) }\n },\n // HeadBucket reads the bucket's existence and our access to it; aborted at the bound, retries included.\n healthCheck(): Promise<HealthReport> {\n return boundedHealthCheck(\n async (signal) => {\n await client.send(new HeadBucketCommand({ Bucket: options.bucket }), {\n abortSignal: signal,\n })\n return { status: 'healthy', detail: options.bucket }\n },\n {\n redact: [\n options.credentials?.accessKeyId ?? '',\n options.credentials?.secretAccessKey ?? '',\n ],\n },\n )\n },\n }\n}\n","import { PutObjectCommand } from '@aws-sdk/client-s3'\nimport { objectKey, s3Client, type S3ClientOptions } from './s3-media-storage.js'\n\nexport interface S3ObjectWriterOptions extends S3ClientOptions {\n bucket: string\n /** Prepended to every key, its edge slashes trimmed; none when empty. */\n prefix?: string\n}\n\n/** Writes whole objects; what it writes is read back only with credentials. */\nexport interface S3ObjectWriter {\n put(key: string, body: Uint8Array, contentType: string): Promise<void>\n}\n\n/**\n * Private objects in a bucket the operator owns, such as the event archive: unlike media storage it answers no URL,\n * and it sets no ACL, so an object is as private as its bucket. Never point it at the public media bucket.\n */\nexport function createS3ObjectWriter(options: S3ObjectWriterOptions): S3ObjectWriter {\n const client = s3Client(options)\n return {\n async put(key, body, contentType) {\n await client.send(\n new PutObjectCommand({\n Bucket: options.bucket,\n Key: objectKey(options.prefix, key),\n Body: body,\n ContentType: contentType,\n }),\n )\n },\n }\n}\n"],"mappings":"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","names":["PutObjectCommand","PutObjectCommand"]}
package/package.json CHANGED
@@ -1,6 +1,48 @@
1
1
  {
2
2
  "name": "@nymbalhq/media-s3",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.2.0",
4
+ "description": "S3 media storage for Nymbal: `infrastructure.media: 's3'`, on AWS or any S3-compatible bucket",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/nymbalhq/nymbal.git",
9
+ "directory": "packages/media-s3"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "type": "module",
15
+ "engines": {
16
+ "node": ">=22"
17
+ },
18
+ "main": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js"
24
+ },
25
+ "./recipes/*": "./recipes/*"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "recipes"
30
+ ],
31
+ "dependencies": {
32
+ "@nymbalhq/types": "1.2.0"
33
+ },
34
+ "peerDependencies": {
35
+ "@aws-sdk/client-s3": "^3.600.0"
36
+ },
37
+ "devDependencies": {
38
+ "@aws-sdk/client-s3": "^3.1153.0",
39
+ "@typescript/native": "npm:typescript@^7.0.2",
40
+ "tsup": "^9.0.0",
41
+ "typescript": "npm:@typescript/typescript6@^6.0.2"
42
+ },
43
+ "scripts": {
44
+ "build": "tsup",
45
+ "dev": "tsup --watch",
46
+ "typecheck": "tsc -p tsconfig.json --noEmit --rootDir ../.."
47
+ }
6
48
  }
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "s3",
3
+ "summary": "Store product images and other media in an S3-compatible bucket (AWS S3, R2, MinIO).",
4
+ "install": ["@nymbalhq/media-s3", "@aws-sdk/client-s3"],
5
+ "set": [{ "path": "infrastructure.media", "value": "s3" }],
6
+ "env": [
7
+ {
8
+ "name": "S3_BUCKET",
9
+ "example": "my-store-media",
10
+ "required": true,
11
+ "comment": "The bucket media is stored in; it must already serve its objects publicly"
12
+ },
13
+ {
14
+ "name": "S3_ENDPOINT",
15
+ "example": "http://localhost:9000",
16
+ "required": false,
17
+ "comment": "An S3-compatible API (MinIO, R2); leave unset for AWS"
18
+ },
19
+ {
20
+ "name": "S3_PUBLIC_URL",
21
+ "example": "https://media.example.com",
22
+ "required": false,
23
+ "comment": "Where the bucket's objects are served from (a CDN); defaults to the bucket's own URL"
24
+ },
25
+ {
26
+ "name": "NYMBAL_S3_PREFIX",
27
+ "example": "media",
28
+ "required": false,
29
+ "comment": "Starts every object key; none by default"
30
+ },
31
+ {
32
+ "name": "AWS_REGION",
33
+ "example": "us-east-1",
34
+ "required": false,
35
+ "comment": "The bucket's region (default us-east-1)"
36
+ },
37
+ {
38
+ "name": "AWS_ACCESS_KEY_ID",
39
+ "example": "",
40
+ "required": false,
41
+ "comment": "Credentials for the AWS SDK; a profile or a role works instead"
42
+ },
43
+ {
44
+ "name": "AWS_SECRET_ACCESS_KEY",
45
+ "example": "",
46
+ "required": false,
47
+ "comment": "The secret for AWS_ACCESS_KEY_ID"
48
+ }
49
+ ],
50
+ "next": [
51
+ "Create the bucket and let it serve its objects publicly: the store never creates buckets or sets policies.",
52
+ "Give the AWS SDK credentials: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in .env, a profile or a role."
53
+ ]
54
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.