@telorun/s3 0.1.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/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ # SUSTAINABLE USE LICENSE (Fair-code)
2
+
3
+ Copyright (c) 2026 CodeNet Sp. z o.o.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, and distribute the Software for any purpose—including commercial purposes—subject to the following conditions:
6
+
7
+ 1. ANTI-COMPETITION RESTRICTION: The Software may not be provided to third parties as a managed service, commercial SaaS (Software-as-a-Service), PaaS (Platform-as-a-Service), BaaS (Backend-as-a-Service), or similar offering where the primary value provided to the user is the functionality of the Software itself, without a separate commercial license from the copyright holder.
8
+
9
+ 2. PERMITTED COMMERCIAL USE: You are free to use the Software to build, host, and monetize your own commercial applications, products, and services, provided such use does not violate Clause 1.
10
+
11
+ 3. ATTRIBUTION: This copyright notice and license must be included in all copies or substantial portions of the Software.
12
+
13
+ 4. CONTRIBUTIONS: Contributions to the Software are welcome and encouraged. By contributing, you agree that your contributions may be incorporated into the Software and distributed under this license.
14
+
15
+ 5. DISCLAIMER: The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software.
16
+
17
+ For commercial licensing, managed hosting exemptions, or enterprise inquiries, please contact <contact@codenet.pl>.
package/README.md ADDED
@@ -0,0 +1,246 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/telorun/telo/main/assets/telo.png" alt="Telo" width="200" />
3
+ </p>
4
+
5
+ <h1 align="center">Telo</h1>
6
+
7
+ <p align="center">Runtime for declarative backends.</p>
8
+
9
+ <p align="center">
10
+ <a href="https://github.com/telorun/telo/actions/workflows/test.yml"><img alt="Tests" src="https://github.com/telorun/telo/actions/workflows/test.yml/badge.svg" /></a>
11
+ <a href="https://www.npmjs.com/package/@telorun/cli"><img alt="node" src="https://img.shields.io/node/v/@telorun/cli" /></a>
12
+ <br />
13
+ <a href="https://github.com/telorun/telo/commits/main"><img alt="Last commit" src="https://img.shields.io/github/last-commit/telorun/telo" /></a>
14
+ <a href="https://github.com/telorun/telo/issues"><img alt="Issues" src="https://img.shields.io/github/issues/telorun/telo" /></a>
15
+ <a href="https://github.com/telorun/telo/pulls"><img alt="Pull requests" src="https://img.shields.io/github/issues-pr/telorun/telo" /></a>
16
+ <br />
17
+ <img alt="Changesets" src="https://img.shields.io/badge/maintained%20with-changesets-176de3" />
18
+ </p>
19
+
20
+ Telo is an execution engine (Micro-Kernel) that runs logic defined entirely in YAML manifests. Instead of writing imperative backend code, you define your routes, databases, schemas, and AI workflows as atomic, interconnected YAML documents. Telo takes those manifests and runs them.
21
+
22
+ Built to be language-agnostic and infinitely extensible.
23
+
24
+ ```bash
25
+ # Reconcile your manifest into a running backend
26
+ $ telo ./examples/hello-api.yaml
27
+
28
+ {"level":30,"time":1771610393008,"pid":1310178,"hostname":"dev","msg":"Server listening at http://127.0.0.1:8844"}
29
+ ```
30
+
31
+ ## Why use Telo?
32
+
33
+ - **Open Standards:** Built on YAML, JSON Schema, and CEL — no proprietary DSL.
34
+ - **Static Analysis:** CEL type checking, reference validation, and IDE diagnostics catch errors before runtime.
35
+ - **Micro-Kernel Architecture:** Telo itself knows nothing about HTTP or SQL. Everything is a module you import, scope, and compose with typed variable and secret contracts.
36
+ - **Language Agnostic:** Available as a Node.js runtime today, with a shared YAML runtime contract that allows for future Rust or Go implementations without changing your manifests.
37
+
38
+ ## What It Does
39
+
40
+ - **Loads** YAML resources and compiles CEL expressions (`${{ }}`) into an in-memory registry.
41
+ - **Resolves** resource dependencies via a multi-pass init loop, handling ordering automatically.
42
+ - **Indexes** resources by Kind and Name for constant-time lookup.
43
+ - **Dispatches** execution to the controller that owns each Kind.
44
+
45
+ ## Example manifest
46
+
47
+ Here is an example Telo application that defines a simple HTTP API:
48
+
49
+ ```yaml
50
+ kind: Telo.Application
51
+ metadata:
52
+ name: feedback
53
+ version: 1.0.0
54
+ description: |
55
+ A complete feedback collection REST API — no code, pure YAML.
56
+ Persists entries to SQLite and serves them over HTTP.
57
+ targets:
58
+ - Migrations
59
+ - Server
60
+ ---
61
+ kind: Telo.Import
62
+ metadata:
63
+ name: Http
64
+ source: std/http-server@0.4.0
65
+ ---
66
+ kind: Telo.Import
67
+ metadata:
68
+ name: Sql
69
+ source: std/sql@0.2.3
70
+ ---
71
+ # SQLite database — swap driver/host/database for PostgreSQL with zero YAML changes
72
+ kind: Sql.Connection
73
+ metadata:
74
+ name: Db
75
+ driver: sqlite
76
+ file: ./tmp/feedback.db
77
+ ---
78
+ # Migrations: applied automatically before the server starts
79
+ kind: Sql.Migrations
80
+ metadata:
81
+ name: Migrations
82
+ connection:
83
+ kind: Sql.Connection
84
+ name: Db
85
+ ---
86
+ kind: Sql.Migration
87
+ metadata:
88
+ name: Migration_20260413_182154_CreateFeedback
89
+ version: 20260413_182154_CreateFeedback
90
+ sql: |
91
+ CREATE TABLE IF NOT EXISTS feedback (
92
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
93
+ text TEXT NOT NULL,
94
+ source TEXT,
95
+ score INTEGER NOT NULL DEFAULT 0,
96
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
97
+ )
98
+ ---
99
+ kind: Http.Server
100
+ metadata:
101
+ name: Server
102
+ baseUrl: http://localhost:8844
103
+ port: 8844
104
+ logger: true
105
+ openapi:
106
+ info:
107
+ title: Feedback API
108
+ version: 1.0.0
109
+ mounts:
110
+ - path: /v1
111
+ type: Http.Api.FeedbackRoutes
112
+ ---
113
+ kind: Http.Api
114
+ metadata:
115
+ name: FeedbackRoutes
116
+ routes:
117
+ # POST /v1/feedback — insert a new entry, score derived from body length heuristic
118
+ - request:
119
+ path: /feedback
120
+ method: POST
121
+ schema:
122
+ body:
123
+ type: object
124
+ properties:
125
+ text:
126
+ type: string
127
+ minLength: 1
128
+ source:
129
+ type: string
130
+ required: [text]
131
+ handler:
132
+ kind: Sql.Exec
133
+ connection:
134
+ kind: Sql.Connection
135
+ name: Db
136
+ inputs:
137
+ sql: "INSERT INTO feedback (text, source, score) VALUES (?, ?, ?)"
138
+ bindings:
139
+ - "${{ request.body.text }}"
140
+ - "${{ request.body.source }}"
141
+ - "${{ size(request.body.text) }}"
142
+ response:
143
+ - status: 201
144
+ headers:
145
+ Content-Type: application/json
146
+ body:
147
+ ok: true
148
+ message: Feedback received
149
+
150
+ # GET /v1/feedback — list all entries, newest first
151
+ - request:
152
+ path: /feedback
153
+ method: GET
154
+ handler:
155
+ kind: Sql.Select
156
+ connection:
157
+ kind: Sql.Connection
158
+ name: Db
159
+ from: feedback
160
+ columns: [id, text, source, score, created_at]
161
+ orderBy:
162
+ - { column: created_at, direction: desc }
163
+ response:
164
+ - status: 200
165
+ headers:
166
+ Content-Type: application/json
167
+ body: "${{ result.rows }}"
168
+
169
+ # GET /v1/feedback/{id} — fetch a single entry
170
+ - request:
171
+ path: /feedback/{id}
172
+ method: GET
173
+ schema:
174
+ params:
175
+ type: object
176
+ properties:
177
+ id:
178
+ type: integer
179
+ required: [id]
180
+ handler:
181
+ kind: Sql.Select
182
+ connection:
183
+ kind: Sql.Connection
184
+ name: Db
185
+ from: feedback
186
+ columns: [id, text, source, score, created_at]
187
+ where:
188
+ - { column: id, op: "=", value: "${{ request.params.id }}" }
189
+ response:
190
+ - status: 200
191
+ when: "size(result.rows) > 0"
192
+ headers:
193
+ Content-Type: application/json
194
+ body: "${{ result.rows[0] }}"
195
+ - status: 404
196
+ headers:
197
+ Content-Type: application/json
198
+ body:
199
+ ok: false
200
+ message: Not found
201
+ ```
202
+
203
+ ## Status
204
+
205
+ Telo is under **active development**. The core runtime, module system, and standard library are functional, but the API surface — including YAML shapes — may change without notice. Not yet recommended for production use.
206
+
207
+ ## The Meaning of Telo
208
+
209
+ The name Telo is derived from the Greek root Telos - meaning the "end goal", "purpose", or "final state". That is exactly the philosophy behind this runtime. In standard imperative programming, you have to write thousands of lines of code to tell a server exactly how to start. With Telo, you simply declare your desired final state.
210
+
211
+ You define the end state. Telo makes it real.
212
+
213
+ ## Philosophy
214
+
215
+ Modern platforms often spend disproportionate effort on technical mechanics-wiring frameworks, managing infrastructure, and negotiating toolchains-while the original business problem gets delayed or diluted. Telo pushes in the opposite direction: it treats kernel execution as a stable, predictable host so teams can concentrate on the **business logic and outcomes** instead of the plumbing.
216
+
217
+ By separating "what the system should do" from "how it is hosted", the runtime reduces friction for domain‑level changes. Teams can move faster on product requirements, experiment more safely, and keep conversations centered on value delivered rather than implementation trivia.
218
+
219
+ Telo also aims to **join forces across all programming language communities**, so the best ideas, patterns, and implementations can converge into a shared kernel truth without forcing everyone into a single stack.
220
+
221
+ YAML also makes the system more **AI‑friendly** than traditional programming languages: it is explicit, structured, and easier for tools to generate, review, and transform without losing intent.
222
+
223
+ ## Modularity
224
+
225
+ Telo is built around **modules** that own specific resource kinds. A module is loaded from a manifest, declares which kinds it implements, and then receives only the resources of those kinds. This keeps concerns isolated and lets teams compose systems from focused building blocks rather than monolithic services.
226
+
227
+ At kernel execution time, execution is always routed by **Kind.Name**. The kernel resolves the Kind to its owning module and hands off execution. Modules can call back into the kernel to execute other resources, enabling composition without tight coupling.
228
+
229
+ ## Architecture
230
+
231
+ The architecture is inspired by Kubernetes-style manifests: declarative resources, explicit kinds, and a control plane that routes work based on those definitions.
232
+ Those manifests were taken to the next level by allowing them to run inside a standalone runtime host.
233
+
234
+ ## See more at
235
+
236
+ - [Telo Kernel](./kernel/README.md)
237
+ - [Telo SDK for module authors](sdk/README.md)
238
+ - [Modules](modules/README.md)
239
+
240
+ ## License
241
+
242
+ See [LICENSE](https://github.com/telorun/telo/blob/main/LICENSE).
243
+
244
+ ## Contribution Note
245
+
246
+ By contributing, you agree that code and examples in this repository may be translated or re‑implemented in other programming languages (including by AI systems) to support the project’s polyglot goals.
@@ -0,0 +1,22 @@
1
+ import { S3Client } from "@aws-sdk/client-s3";
2
+ import { Static } from "@sinclair/typebox";
3
+ import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
4
+ export declare const schema: import("@sinclair/typebox").TObject<{
5
+ bucketName: import("@sinclair/typebox").TString;
6
+ endpoint: import("@sinclair/typebox").TString;
7
+ accessKeyId: import("@sinclair/typebox").TString;
8
+ secretAccessKey: import("@sinclair/typebox").TString;
9
+ forcePathStyle: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
10
+ createIfMissing: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
11
+ publicRead: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
12
+ }>;
13
+ export type S3BucketManifest = Static<typeof schema>;
14
+ export declare class S3BucketResource implements ResourceInstance {
15
+ readonly client: S3Client;
16
+ readonly bucketName: string;
17
+ constructor(manifest: S3BucketManifest);
18
+ getClient(): S3Client;
19
+ snapshot(): {};
20
+ }
21
+ export declare function register(): void;
22
+ export declare function create(resource: S3BucketManifest, ctx: ResourceContext): Promise<S3BucketResource>;
@@ -0,0 +1,90 @@
1
+ import { CreateBucketCommand, HeadBucketCommand, PutBucketPolicyCommand, S3Client, S3ServiceException, } from "@aws-sdk/client-s3";
2
+ import { Type } from "@sinclair/typebox";
3
+ export const schema = Type.Object({
4
+ bucketName: Type.String(),
5
+ endpoint: Type.String(),
6
+ accessKeyId: Type.String(),
7
+ secretAccessKey: Type.String(),
8
+ forcePathStyle: Type.Optional(Type.Boolean()),
9
+ createIfMissing: Type.Optional(Type.Boolean()),
10
+ publicRead: Type.Optional(Type.Boolean()),
11
+ });
12
+ export class S3BucketResource {
13
+ client;
14
+ bucketName;
15
+ constructor(manifest) {
16
+ this.bucketName = manifest.bucketName;
17
+ this.client = new S3Client({
18
+ region: "auto",
19
+ endpoint: manifest.endpoint,
20
+ forcePathStyle: manifest.forcePathStyle ?? false,
21
+ credentials: { accessKeyId: manifest.accessKeyId, secretAccessKey: manifest.secretAccessKey },
22
+ });
23
+ }
24
+ getClient() {
25
+ return this.client;
26
+ }
27
+ snapshot() {
28
+ return {};
29
+ }
30
+ }
31
+ export function register() { }
32
+ export async function create(resource, ctx) {
33
+ ctx.validateSchema(resource, schema);
34
+ const instance = new S3BucketResource(resource);
35
+ if (resource.createIfMissing) {
36
+ await ensureBucket(instance);
37
+ }
38
+ if (resource.publicRead) {
39
+ // await applyPublicReadPolicy(instance);
40
+ }
41
+ return instance;
42
+ }
43
+ async function ensureBucket(instance) {
44
+ if (await bucketExists(instance)) {
45
+ return;
46
+ }
47
+ try {
48
+ await instance.getClient().send(new CreateBucketCommand({ Bucket: instance.bucketName }));
49
+ }
50
+ catch (err) {
51
+ if (err instanceof S3ServiceException) {
52
+ if (err.name === "BucketAlreadyOwnedByYou" || err.name === "BucketAlreadyExists") {
53
+ return;
54
+ }
55
+ }
56
+ throw err;
57
+ }
58
+ }
59
+ async function bucketExists(instance) {
60
+ try {
61
+ await instance.getClient().send(new HeadBucketCommand({ Bucket: instance.bucketName }));
62
+ return true;
63
+ }
64
+ catch (err) {
65
+ if (err instanceof S3ServiceException) {
66
+ if (err.name === "NotFound" || err.$metadata?.httpStatusCode === 404) {
67
+ return false;
68
+ }
69
+ }
70
+ throw err;
71
+ }
72
+ }
73
+ async function applyPublicReadPolicy(instance) {
74
+ const policy = {
75
+ Version: "2012-10-17",
76
+ Statement: [
77
+ {
78
+ Sid: "PublicReadGetObject",
79
+ Effect: "Allow",
80
+ Principal: "*",
81
+ Action: "s3:GetObject",
82
+ Resource: `arn:aws:s3:::${instance.bucketName}/*`,
83
+ },
84
+ ],
85
+ };
86
+ await instance.getClient().send(new PutBucketPolicyCommand({
87
+ Bucket: instance.bucketName,
88
+ Policy: JSON.stringify(policy),
89
+ }));
90
+ }
@@ -0,0 +1,22 @@
1
+ import { Stream, type ResourceContext, type ResourceInstance } from "@telorun/sdk";
2
+ interface S3GetManifest {
3
+ bucketRef: {
4
+ name: string;
5
+ };
6
+ }
7
+ interface S3GetInputs {
8
+ key: string;
9
+ }
10
+ interface S3GetOutput {
11
+ output: Stream<Uint8Array>;
12
+ contentType?: string;
13
+ }
14
+ declare class S3GetResource implements ResourceInstance<S3GetInputs, S3GetOutput> {
15
+ private readonly manifest;
16
+ private readonly ctx;
17
+ constructor(manifest: S3GetManifest, ctx: ResourceContext);
18
+ invoke(input: S3GetInputs): Promise<S3GetOutput>;
19
+ }
20
+ export declare function register(): void;
21
+ export declare function create(resource: S3GetManifest, ctx: ResourceContext): Promise<S3GetResource>;
22
+ export {};
@@ -0,0 +1,45 @@
1
+ import { GetObjectCommand, S3ServiceException } from "@aws-sdk/client-s3";
2
+ import { InvokeError, Stream } from "@telorun/sdk";
3
+ class S3GetResource {
4
+ manifest;
5
+ ctx;
6
+ constructor(manifest, ctx) {
7
+ this.manifest = manifest;
8
+ this.ctx = ctx;
9
+ }
10
+ async invoke(input) {
11
+ const bucketRefName = this.ctx.expandValue(this.manifest.bucketRef.name, input ?? {});
12
+ const bucket = this.ctx.moduleContext.getInstance(bucketRefName);
13
+ if (!bucket) {
14
+ throw new InvokeError("ERR_INVALID_REFERENCE", `S3.Bucket "${bucketRefName}" not found`);
15
+ }
16
+ let response;
17
+ try {
18
+ response = await bucket.getClient().send(new GetObjectCommand({ Bucket: bucket.bucketName, Key: input.key }));
19
+ }
20
+ catch (err) {
21
+ if (err instanceof S3ServiceException &&
22
+ (err.name === "NoSuchKey" || err.$metadata?.httpStatusCode === 404)) {
23
+ throw new InvokeError("ERR_NOT_FOUND", `S3 object not found: ${input.key}`);
24
+ }
25
+ throw err;
26
+ }
27
+ const body = response.Body;
28
+ if (!body || typeof body[Symbol.asyncIterator] !== "function") {
29
+ throw new InvokeError("ERR_INVALID_RESPONSE", `S3 GetObject returned no iterable body for key '${input.key}'.`);
30
+ }
31
+ return {
32
+ output: new Stream(toUint8ArrayIterable(body)),
33
+ contentType: response.ContentType,
34
+ };
35
+ }
36
+ }
37
+ async function* toUint8ArrayIterable(source) {
38
+ for await (const chunk of source) {
39
+ yield chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
40
+ }
41
+ }
42
+ export function register() { }
43
+ export async function create(resource, ctx) {
44
+ return new S3GetResource(resource, ctx);
45
+ }
@@ -0,0 +1,17 @@
1
+ import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
2
+ interface S3ListManifest {
3
+ bucketRef: {
4
+ name: string;
5
+ };
6
+ }
7
+ declare class S3ListResource implements ResourceInstance {
8
+ private readonly manifest;
9
+ private readonly ctx;
10
+ constructor(manifest: S3ListManifest, ctx: ResourceContext);
11
+ invoke(input: any): Promise<{
12
+ keys: string[];
13
+ }>;
14
+ }
15
+ export declare function register(): void;
16
+ export declare function create(resource: S3ListManifest, ctx: ResourceContext): Promise<S3ListResource>;
17
+ export {};
@@ -0,0 +1,27 @@
1
+ import { ListObjectsCommand } from "@aws-sdk/client-s3";
2
+ class S3ListResource {
3
+ manifest;
4
+ ctx;
5
+ constructor(manifest, ctx) {
6
+ this.manifest = manifest;
7
+ this.ctx = ctx;
8
+ }
9
+ async invoke(input) {
10
+ const ctx = this.ctx;
11
+ const m = this.manifest;
12
+ const bucketRefName = ctx.expandValue(m.bucketRef.name, input ?? {});
13
+ const prefix = input?.prefix ?? "";
14
+ const bucket = ctx.moduleContext.getInstance(bucketRefName);
15
+ if (!bucket) {
16
+ throw new Error(`S3.Bucket "${bucketRefName}" not found`);
17
+ }
18
+ const client = bucket.getClient();
19
+ const result = await client.send(new ListObjectsCommand({ Bucket: bucket.bucketName, Prefix: prefix }));
20
+ const keys = (result.Contents ?? []).map((obj) => obj.Key ?? "");
21
+ return { keys };
22
+ }
23
+ }
24
+ export function register() { }
25
+ export async function create(resource, ctx) {
26
+ return new S3ListResource(resource, ctx);
27
+ }
@@ -0,0 +1,21 @@
1
+ import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
2
+ interface S3PutManifest {
3
+ bucketRef: {
4
+ name: string;
5
+ };
6
+ }
7
+ declare class S3PutResource implements ResourceInstance {
8
+ private readonly manifest;
9
+ private readonly ctx;
10
+ constructor(manifest: S3PutManifest, ctx: ResourceContext);
11
+ invoke(input: {
12
+ key: string;
13
+ body: string;
14
+ contentType?: string;
15
+ }): Promise<{
16
+ key: string;
17
+ }>;
18
+ }
19
+ export declare function register(): void;
20
+ export declare function create(resource: S3PutManifest, ctx: ResourceContext): Promise<S3PutResource>;
21
+ export {};
@@ -0,0 +1,27 @@
1
+ import { PutObjectCommand } from "@aws-sdk/client-s3";
2
+ class S3PutResource {
3
+ manifest;
4
+ ctx;
5
+ constructor(manifest, ctx) {
6
+ this.manifest = manifest;
7
+ this.ctx = ctx;
8
+ }
9
+ async invoke(input) {
10
+ const bucketRefName = this.ctx.expandValue(this.manifest.bucketRef.name, input ?? {});
11
+ const bucket = this.ctx.moduleContext.getInstance(bucketRefName);
12
+ if (!bucket) {
13
+ throw new Error(`S3.Bucket "${bucketRefName}" not found`);
14
+ }
15
+ await bucket.getClient().send(new PutObjectCommand({
16
+ Bucket: bucket.bucketName,
17
+ Key: input.key,
18
+ Body: input.body,
19
+ ContentType: input.contentType ?? "application/octet-stream",
20
+ }));
21
+ return { key: input.key };
22
+ }
23
+ }
24
+ export function register() { }
25
+ export async function create(resource, ctx) {
26
+ return new S3PutResource(resource, ctx);
27
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@telorun/s3",
3
+ "version": "0.1.0",
4
+ "description": "Telo S3 module - AWS S3 resource kinds for Telo manifests.",
5
+ "keywords": [
6
+ "telo",
7
+ "s3",
8
+ "aws",
9
+ "storage"
10
+ ],
11
+ "author": "Bartosz Pasiński <bartosz.pasinski@codenet.pl>",
12
+ "license": "SEE LICENSE IN LICENSE",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/telorun/telo.git",
16
+ "directory": "modules/s3/nodejs"
17
+ },
18
+ "homepage": "https://github.com/telorun/telo#readme",
19
+ "bugs": {
20
+ "url": "https://github.com/telorun/telo/issues"
21
+ },
22
+ "type": "module",
23
+ "exports": {
24
+ "./s3-bucket": {
25
+ "bun": "./src/s3-bucket-controller.ts",
26
+ "import": "./dist/s3-bucket-controller.js"
27
+ },
28
+ "./s3-list": {
29
+ "bun": "./src/s3-list-controller.ts",
30
+ "import": "./dist/s3-list-controller.js"
31
+ },
32
+ "./s3-put": {
33
+ "bun": "./src/s3-put-controller.ts",
34
+ "import": "./dist/s3-put-controller.js"
35
+ },
36
+ "./s3-get": {
37
+ "bun": "./src/s3-get-controller.ts",
38
+ "import": "./dist/s3-get-controller.js"
39
+ }
40
+ },
41
+ "files": [
42
+ "dist",
43
+ "src/**"
44
+ ],
45
+ "dependencies": {
46
+ "@aws-sdk/client-s3": "^3.0.0",
47
+ "@sinclair/typebox": "^0.34.48"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^20.0.0",
51
+ "typescript": "^5.0.0"
52
+ },
53
+ "peerDependencies": {
54
+ "@telorun/sdk": "0.12.0"
55
+ },
56
+ "scripts": {
57
+ "build": "tsc -p tsconfig.lib.json"
58
+ }
59
+ }
@@ -0,0 +1,111 @@
1
+ import {
2
+ CreateBucketCommand,
3
+ HeadBucketCommand,
4
+ PutBucketPolicyCommand,
5
+ S3Client,
6
+ S3ServiceException,
7
+ } from "@aws-sdk/client-s3";
8
+ import { Static, Type } from "@sinclair/typebox";
9
+ import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
10
+
11
+ export const schema = Type.Object({
12
+ bucketName: Type.String(),
13
+ endpoint: Type.String(),
14
+ accessKeyId: Type.String(),
15
+ secretAccessKey: Type.String(),
16
+ forcePathStyle: Type.Optional(Type.Boolean()),
17
+ createIfMissing: Type.Optional(Type.Boolean()),
18
+ publicRead: Type.Optional(Type.Boolean()),
19
+ });
20
+ export type S3BucketManifest = Static<typeof schema>;
21
+
22
+ export class S3BucketResource implements ResourceInstance {
23
+ readonly client: S3Client;
24
+ readonly bucketName: string;
25
+
26
+ constructor(manifest: S3BucketManifest) {
27
+ this.bucketName = manifest.bucketName;
28
+ this.client = new S3Client({
29
+ region: "auto",
30
+ endpoint: manifest.endpoint,
31
+ forcePathStyle: manifest.forcePathStyle ?? false,
32
+ credentials: { accessKeyId: manifest.accessKeyId, secretAccessKey: manifest.secretAccessKey },
33
+ });
34
+ }
35
+
36
+ getClient() {
37
+ return this.client;
38
+ }
39
+
40
+ snapshot() {
41
+ return {};
42
+ }
43
+ }
44
+
45
+ export function register(): void {}
46
+
47
+ export async function create(
48
+ resource: S3BucketManifest,
49
+ ctx: ResourceContext,
50
+ ): Promise<S3BucketResource> {
51
+ ctx.validateSchema(resource, schema);
52
+ const instance = new S3BucketResource(resource);
53
+ if (resource.createIfMissing) {
54
+ await ensureBucket(instance);
55
+ }
56
+ if (resource.publicRead) {
57
+ // await applyPublicReadPolicy(instance);
58
+ }
59
+ return instance;
60
+ }
61
+
62
+ async function ensureBucket(instance: S3BucketResource): Promise<void> {
63
+ if (await bucketExists(instance)) {
64
+ return;
65
+ }
66
+ try {
67
+ await instance.getClient().send(new CreateBucketCommand({ Bucket: instance.bucketName }));
68
+ } catch (err) {
69
+ if (err instanceof S3ServiceException) {
70
+ if (err.name === "BucketAlreadyOwnedByYou" || err.name === "BucketAlreadyExists") {
71
+ return;
72
+ }
73
+ }
74
+ throw err;
75
+ }
76
+ }
77
+
78
+ async function bucketExists(instance: S3BucketResource): Promise<boolean> {
79
+ try {
80
+ await instance.getClient().send(new HeadBucketCommand({ Bucket: instance.bucketName }));
81
+ return true;
82
+ } catch (err) {
83
+ if (err instanceof S3ServiceException) {
84
+ if (err.name === "NotFound" || err.$metadata?.httpStatusCode === 404) {
85
+ return false;
86
+ }
87
+ }
88
+ throw err;
89
+ }
90
+ }
91
+
92
+ async function applyPublicReadPolicy(instance: S3BucketResource): Promise<void> {
93
+ const policy = {
94
+ Version: "2012-10-17",
95
+ Statement: [
96
+ {
97
+ Sid: "PublicReadGetObject",
98
+ Effect: "Allow",
99
+ Principal: "*",
100
+ Action: "s3:GetObject",
101
+ Resource: `arn:aws:s3:::${instance.bucketName}/*`,
102
+ },
103
+ ],
104
+ };
105
+ await instance.getClient().send(
106
+ new PutBucketPolicyCommand({
107
+ Bucket: instance.bucketName,
108
+ Policy: JSON.stringify(policy),
109
+ }),
110
+ );
111
+ }
@@ -0,0 +1,76 @@
1
+ import { GetObjectCommand, S3ServiceException } from "@aws-sdk/client-s3";
2
+ import { InvokeError, Stream, type ResourceContext, type ResourceInstance } from "@telorun/sdk";
3
+ import { S3BucketResource } from "./s3-bucket-controller.js";
4
+
5
+ interface S3GetManifest {
6
+ bucketRef: { name: string };
7
+ }
8
+
9
+ interface S3GetInputs {
10
+ key: string;
11
+ }
12
+
13
+ interface S3GetOutput {
14
+ output: Stream<Uint8Array>;
15
+ contentType?: string;
16
+ }
17
+
18
+ class S3GetResource implements ResourceInstance<S3GetInputs, S3GetOutput> {
19
+ constructor(
20
+ private readonly manifest: S3GetManifest,
21
+ private readonly ctx: ResourceContext,
22
+ ) {}
23
+
24
+ async invoke(input: S3GetInputs): Promise<S3GetOutput> {
25
+ const bucketRefName = this.ctx.expandValue(this.manifest.bucketRef.name, input ?? {}) as string;
26
+ const bucket = this.ctx.moduleContext.getInstance(bucketRefName) as S3BucketResource | undefined;
27
+ if (!bucket) {
28
+ throw new InvokeError("ERR_INVALID_REFERENCE", `S3.Bucket "${bucketRefName}" not found`);
29
+ }
30
+
31
+ let response;
32
+ try {
33
+ response = await bucket.getClient().send(
34
+ new GetObjectCommand({ Bucket: bucket.bucketName, Key: input.key }),
35
+ );
36
+ } catch (err) {
37
+ if (
38
+ err instanceof S3ServiceException &&
39
+ (err.name === "NoSuchKey" || err.$metadata?.httpStatusCode === 404)
40
+ ) {
41
+ throw new InvokeError("ERR_NOT_FOUND", `S3 object not found: ${input.key}`);
42
+ }
43
+ throw err;
44
+ }
45
+
46
+ const body = response.Body;
47
+ if (!body || typeof (body as AsyncIterable<unknown>)[Symbol.asyncIterator] !== "function") {
48
+ throw new InvokeError(
49
+ "ERR_INVALID_RESPONSE",
50
+ `S3 GetObject returned no iterable body for key '${input.key}'.`,
51
+ );
52
+ }
53
+
54
+ return {
55
+ output: new Stream(toUint8ArrayIterable(body as AsyncIterable<Uint8Array | Buffer>)),
56
+ contentType: response.ContentType,
57
+ };
58
+ }
59
+ }
60
+
61
+ async function* toUint8ArrayIterable(
62
+ source: AsyncIterable<Uint8Array | Buffer>,
63
+ ): AsyncIterable<Uint8Array> {
64
+ for await (const chunk of source) {
65
+ yield chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
66
+ }
67
+ }
68
+
69
+ export function register(): void {}
70
+
71
+ export async function create(
72
+ resource: S3GetManifest,
73
+ ctx: ResourceContext,
74
+ ): Promise<S3GetResource> {
75
+ return new S3GetResource(resource, ctx);
76
+ }
@@ -0,0 +1,43 @@
1
+ import { ListObjectsCommand } from "@aws-sdk/client-s3";
2
+ import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
3
+ import { S3BucketResource } from "./s3-bucket-controller.js";
4
+
5
+ interface S3ListManifest {
6
+ bucketRef: { name: string };
7
+ }
8
+
9
+ class S3ListResource implements ResourceInstance {
10
+ constructor(
11
+ private readonly manifest: S3ListManifest,
12
+ private readonly ctx: ResourceContext,
13
+ ) {}
14
+
15
+ async invoke(input: any): Promise<{ keys: string[] }> {
16
+ const ctx = this.ctx;
17
+ const m = this.manifest;
18
+ const bucketRefName = ctx.expandValue(m.bucketRef.name, input ?? {}) as string;
19
+ const prefix = (input?.prefix as string) ?? "";
20
+
21
+ const bucket: S3BucketResource = ctx.moduleContext.getInstance(bucketRefName) as any;
22
+ if (!bucket) {
23
+ throw new Error(`S3.Bucket "${bucketRefName}" not found`);
24
+ }
25
+
26
+ const client = bucket.getClient();
27
+ const result = await client.send(
28
+ new ListObjectsCommand({ Bucket: bucket.bucketName as string, Prefix: prefix }),
29
+ );
30
+
31
+ const keys = (result.Contents ?? []).map((obj) => obj.Key ?? "");
32
+ return { keys };
33
+ }
34
+ }
35
+
36
+ export function register(): void {}
37
+
38
+ export async function create(
39
+ resource: S3ListManifest,
40
+ ctx: ResourceContext,
41
+ ): Promise<S3ListResource> {
42
+ return new S3ListResource(resource, ctx);
43
+ }
@@ -0,0 +1,47 @@
1
+ import { PutObjectCommand } from "@aws-sdk/client-s3";
2
+ import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
3
+ import { S3BucketResource } from "./s3-bucket-controller.js";
4
+
5
+ interface S3PutManifest {
6
+ bucketRef: { name: string };
7
+ }
8
+
9
+ class S3PutResource implements ResourceInstance {
10
+ constructor(
11
+ private readonly manifest: S3PutManifest,
12
+ private readonly ctx: ResourceContext,
13
+ ) {}
14
+
15
+ async invoke(input: {
16
+ key: string;
17
+ body: string;
18
+ contentType?: string;
19
+ }): Promise<{ key: string }> {
20
+ const bucketRefName = this.ctx.expandValue(this.manifest.bucketRef.name, input ?? {}) as string;
21
+
22
+ const bucket: S3BucketResource = this.ctx.moduleContext.getInstance(bucketRefName) as any;
23
+ if (!bucket) {
24
+ throw new Error(`S3.Bucket "${bucketRefName}" not found`);
25
+ }
26
+
27
+ await bucket.getClient().send(
28
+ new PutObjectCommand({
29
+ Bucket: bucket.bucketName,
30
+ Key: input.key,
31
+ Body: input.body,
32
+ ContentType: input.contentType ?? "application/octet-stream",
33
+ }),
34
+ );
35
+
36
+ return { key: input.key };
37
+ }
38
+ }
39
+
40
+ export function register(): void {}
41
+
42
+ export async function create(
43
+ resource: S3PutManifest,
44
+ ctx: ResourceContext,
45
+ ): Promise<S3PutResource> {
46
+ return new S3PutResource(resource, ctx);
47
+ }