@kirkelliott/zap 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/README.md +287 -0
- package/demo/counter.zap +6 -0
- package/demo/echo.zap +11 -0
- package/demo/hello.zap +4 -0
- package/demo/kv.zap +18 -0
- package/demo/live.zap +4 -0
- package/demo/proxy.zap +14 -0
- package/dist/cli.js +144 -0
- package/dist/cron.js +60 -0
- package/dist/dev.js +42 -0
- package/dist/eval.js +36 -0
- package/dist/handler.js +46 -0
- package/dist/init.js +153 -0
- package/dist/kv.js +19 -0
- package/dist/types.js +6 -0
- package/examples/api.zap +5 -0
- package/examples/counter.zap +5 -0
- package/examples/greet.zap +4 -0
- package/examples/heartbeat.zap +6 -0
- package/examples/proxy.zap +19 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# zap
|
|
2
|
+
|
|
3
|
+
> Drop a `.zap` file in S3. It becomes an API endpoint.
|
|
4
|
+
|
|
5
|
+
Like PHP — but JavaScript, serverless, and free on AWS forever.
|
|
6
|
+
|
|
7
|
+
One Lambda function runs permanently as the runtime. When a request arrives at `/proxy`, it fetches `proxy.zap` from your S3 bucket, evaluates it, and returns the response. To change behavior, upload a new file. No redeploy. No CI. No infra changes.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone https://github.com/dmvjs/s3node && cd s3node
|
|
15
|
+
npm install
|
|
16
|
+
npm run init
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`init` provisions the full AWS stack and prints your endpoint URL. Requires AWS credentials (`aws configure`). Takes about 30 seconds.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
building runtime ✓
|
|
23
|
+
creating bucket ✓ zap-a3f2b8c1
|
|
24
|
+
creating kv table ✓ zap-kv
|
|
25
|
+
configuring iam ✓
|
|
26
|
+
deploying lambda ✓
|
|
27
|
+
creating endpoint ✓
|
|
28
|
+
|
|
29
|
+
→ https://abc123.lambda-url.us-east-1.on.aws
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Local dev
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm run dev # → http://localhost:3000
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Place `.zap` files in the project root. `GET /proxy` maps to `./proxy.zap`. Uses real DynamoDB for `kv` — same table as production.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## The .zap format
|
|
45
|
+
|
|
46
|
+
A `.zap` file exports a default async function. That function is the handler.
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
export default async (req) => {
|
|
50
|
+
return { status: 200, body: 'hello' }
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Request
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
req.method // 'GET' | 'POST' | 'PUT' | 'DELETE' | ...
|
|
58
|
+
req.path // '/proxy'
|
|
59
|
+
req.query // Record<string, string> e.g. { url: 'https://...' }
|
|
60
|
+
req.headers // Record<string, string>
|
|
61
|
+
req.body // string | null
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Response
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
{
|
|
68
|
+
status?: number // default 200
|
|
69
|
+
headers?: Record<string, string>
|
|
70
|
+
body?: string | object // objects are JSON-serialized
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Built-ins
|
|
75
|
+
|
|
76
|
+
Everything available globally inside every `.zap` file:
|
|
77
|
+
|
|
78
|
+
| Name | Description |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `fetch` | Standard web `fetch` |
|
|
81
|
+
| `kv` | Persistent key/value store (DynamoDB) |
|
|
82
|
+
| `zap(name)` | Import another `.zap` file from S3 |
|
|
83
|
+
| `crypto` | Web Crypto API |
|
|
84
|
+
| `URL`, `URLSearchParams` | URL utilities |
|
|
85
|
+
| `Buffer` | Node.js Buffer |
|
|
86
|
+
| `console` | Logging (goes to CloudWatch) |
|
|
87
|
+
| `setTimeout`, `clearTimeout` | Timers |
|
|
88
|
+
| `process.env` | Environment variables |
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## kv — persistent storage
|
|
93
|
+
|
|
94
|
+
`kv` is a built-in key/value store backed by DynamoDB. No setup, no imports, no config — it's just there.
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
await kv.set('key', value) // value can be string, number, object, array
|
|
98
|
+
await kv.get('key') // returns the value, or null if not found
|
|
99
|
+
await kv.del('key') // delete
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
// counter.zap
|
|
104
|
+
export default async (req) => {
|
|
105
|
+
const count = ((await kv.get('visits')) ?? 0) + 1
|
|
106
|
+
await kv.set('visits', count)
|
|
107
|
+
return { body: { visits: count } }
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## zap() — imports
|
|
114
|
+
|
|
115
|
+
Any `.zap` file can import another using `zap(name)`. S3 is the module registry.
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
// utils/greet.zap
|
|
119
|
+
export default {
|
|
120
|
+
hello: (name) => `hello ${name}`,
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
// api.zap
|
|
126
|
+
export default async (req) => {
|
|
127
|
+
const greet = await zap('utils/greet')
|
|
128
|
+
return { body: greet.hello(req.query.name ?? 'world') }
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`zap('utils/greet')` fetches `utils/greet.zap` from the same bucket. Imports can be nested — a `.zap` file can `zap()` other `.zap` files. The bucket is the module system.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## @cron — scheduled handlers
|
|
137
|
+
|
|
138
|
+
Add `// @cron <expr>` as the first line. When deployed, `zap deploy` automatically creates an EventBridge rule that triggers the handler on schedule.
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
// @cron 0 * * * *
|
|
142
|
+
export default async () => {
|
|
143
|
+
await kv.set('heartbeat', new Date().toISOString())
|
|
144
|
+
console.log('tick')
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Standard 5-field cron expressions (`minute hour day month weekday`). `zap rm` removes the EventBridge rule alongside the S3 file.
|
|
149
|
+
|
|
150
|
+
Cron handlers receive no request argument. All built-ins (`kv`, `fetch`, `zap()`, etc.) are available.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Examples
|
|
155
|
+
|
|
156
|
+
### CORS proxy
|
|
157
|
+
|
|
158
|
+
Fetch any remote URL server-side, bypassing browser CORS restrictions.
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
// proxy.zap
|
|
162
|
+
export default async (req) => {
|
|
163
|
+
const url = req.query.url
|
|
164
|
+
if (!url) return { status: 400, body: 'Missing ?url=' }
|
|
165
|
+
|
|
166
|
+
const upstream = await fetch(url)
|
|
167
|
+
return {
|
|
168
|
+
status: upstream.status,
|
|
169
|
+
headers: {
|
|
170
|
+
'content-type': upstream.headers.get('content-type') ?? 'text/plain',
|
|
171
|
+
'access-control-allow-origin': '*',
|
|
172
|
+
},
|
|
173
|
+
body: await upstream.text(),
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
GET https://your-endpoint/proxy?url=https://api.example.com/data
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Stateful counter
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
// counter.zap
|
|
186
|
+
export default async (req) => {
|
|
187
|
+
const count = ((await kv.get('visits')) ?? 0) + 1
|
|
188
|
+
await kv.set('visits', count)
|
|
189
|
+
return { body: { visits: count } }
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Hourly heartbeat
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
// @cron 0 * * * *
|
|
197
|
+
export default async () => {
|
|
198
|
+
await kv.set('heartbeat', new Date().toISOString())
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## CLI
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
zap init Provision AWS infra and deploy the runtime
|
|
208
|
+
zap deploy <file|directory> Upload .zap file(s) to S3
|
|
209
|
+
zap rm <name> Remove a handler (and its cron rule if any)
|
|
210
|
+
zap ls List deployed handlers
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Options**
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
-r, --region <region> AWS region for init (default: us-east-1)
|
|
217
|
+
-b, --bucket <bucket> S3 bucket name (default: reads from .zaprc)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
After `init`, a `.zaprc` file is written to the project root. All subsequent commands read the bucket name and function ARN from it automatically — no flags needed.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## AWS infrastructure
|
|
225
|
+
|
|
226
|
+
Everything provisioned by `npm run init`:
|
|
227
|
+
|
|
228
|
+
| Resource | Purpose |
|
|
229
|
+
|---|---|
|
|
230
|
+
| S3 bucket | Stores `.zap` files |
|
|
231
|
+
| DynamoDB table (`zap-kv`) | Backs the `kv` built-in |
|
|
232
|
+
| IAM role (`zap-runtime-role`) | Lambda execution role |
|
|
233
|
+
| Lambda function (`zap-runtime`, Node 20) | The runtime |
|
|
234
|
+
| Lambda Function URL | Public HTTPS endpoint — no API Gateway |
|
|
235
|
+
| EventBridge rules | One per `@cron` handler (created on deploy) |
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Cost
|
|
240
|
+
|
|
241
|
+
Runs within AWS always-free tier limits at zero cost for typical personal or small-team usage:
|
|
242
|
+
|
|
243
|
+
| Service | Free tier |
|
|
244
|
+
|---|---|
|
|
245
|
+
| Lambda | 1M requests/month, 400K GB-seconds — permanent |
|
|
246
|
+
| S3 | 5GB storage, 20K GET requests/month — permanent |
|
|
247
|
+
| DynamoDB | 25 WCU/RCU, 25GB storage — permanent |
|
|
248
|
+
| EventBridge | 14M scheduled invocations/month — permanent |
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## How it works
|
|
253
|
+
|
|
254
|
+
The runtime is a single Lambda function. On each HTTP request:
|
|
255
|
+
|
|
256
|
+
1. Parse the URL path → derive the S3 key (`/proxy` → `proxy.zap`)
|
|
257
|
+
2. Fetch the `.zap` source from S3
|
|
258
|
+
3. Evaluate it in a `vm` sandbox with the built-in globals
|
|
259
|
+
4. Call the exported handler with the request
|
|
260
|
+
5. Return the response
|
|
261
|
+
|
|
262
|
+
For `@cron` handlers, EventBridge sends `{ zap: { cron: "handler-name" } }` as the Lambda payload. The runtime detects this and invokes the handler with no request argument.
|
|
263
|
+
|
|
264
|
+
`zap(name)` inside a handler triggers the same fetch-and-eval cycle recursively, with the same loader — so the entire module graph lives in S3.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
MIT
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
The file in S3 is just a carrier. You don't need it.
|
|
273
|
+
|
|
274
|
+
```js
|
|
275
|
+
// live.zap
|
|
276
|
+
export default async (req) => {
|
|
277
|
+
if (!req.body) return { status: 400, body: 'POST a function' }
|
|
278
|
+
return { body: await eval(`(${req.body})`)({ kv, fetch }) }
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
curl -X POST https://your-endpoint/live \
|
|
284
|
+
-d 'async ({ kv }) => kv.get("visits")'
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Deploy `live.zap` once. Then POST JavaScript directly — no S3, no CLI, no deploy step. The runtime running inside itself.
|
package/demo/counter.zap
ADDED
package/demo/echo.zap
ADDED
package/demo/hello.zap
ADDED
package/demo/kv.zap
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export default async (req) => {
|
|
2
|
+
const { key, value } = req.query
|
|
3
|
+
|
|
4
|
+
if (!key) return { status: 400, body: 'Missing ?key=' }
|
|
5
|
+
|
|
6
|
+
if (req.method === 'DELETE') {
|
|
7
|
+
await kv.del(key)
|
|
8
|
+
return { body: { deleted: key } }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (value !== undefined) {
|
|
12
|
+
await kv.set(key, value)
|
|
13
|
+
return { body: { set: key, value } }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const stored = await kv.get(key)
|
|
17
|
+
return { body: { key, value: stored } }
|
|
18
|
+
}
|
package/demo/live.zap
ADDED
package/demo/proxy.zap
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export default async (req) => {
|
|
2
|
+
const url = req.query.url
|
|
3
|
+
if (!url) return { status: 400, body: 'Missing ?url=' }
|
|
4
|
+
|
|
5
|
+
const upstream = await fetch(url, { headers: { 'user-agent': 'zap-proxy/1.0' } })
|
|
6
|
+
return {
|
|
7
|
+
status: upstream.status,
|
|
8
|
+
headers: {
|
|
9
|
+
'content-type': upstream.headers.get('content-type') ?? 'text/plain',
|
|
10
|
+
'access-control-allow-origin': '*',
|
|
11
|
+
},
|
|
12
|
+
body: await upstream.text(),
|
|
13
|
+
}
|
|
14
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
38
|
+
const commander_1 = require("commander");
|
|
39
|
+
const promises_1 = require("node:fs/promises");
|
|
40
|
+
const node_fs_1 = require("node:fs");
|
|
41
|
+
const node_path_1 = require("node:path");
|
|
42
|
+
const cron_1 = require("./cron");
|
|
43
|
+
const s3 = new client_s3_1.S3Client({});
|
|
44
|
+
function readConfig() {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse((0, node_fs_1.readFileSync)('.zaprc', 'utf8'));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function bucket(opts) {
|
|
53
|
+
const b = opts.bucket ?? process.env.ZAP_BUCKET ?? readConfig().bucket;
|
|
54
|
+
if (!b) {
|
|
55
|
+
console.error('error: bucket required (--bucket, ZAP_BUCKET, or run: npm run init)');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
return b;
|
|
59
|
+
}
|
|
60
|
+
async function walkZap(dir, prefix = '') {
|
|
61
|
+
const entries = await (0, promises_1.readdir)(dir, { withFileTypes: true });
|
|
62
|
+
const results = [];
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
const filePath = (0, node_path_1.join)(dir, entry.name);
|
|
65
|
+
const key = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
66
|
+
if (entry.isDirectory())
|
|
67
|
+
results.push(...await walkZap(filePath, key));
|
|
68
|
+
else if (entry.name.endsWith('.zap'))
|
|
69
|
+
results.push({ filePath, key });
|
|
70
|
+
}
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
73
|
+
async function deployFile(b, filePath, key) {
|
|
74
|
+
const source = await (0, promises_1.readFile)(filePath, 'utf8');
|
|
75
|
+
await s3.send(new client_s3_1.PutObjectCommand({ Bucket: b, Key: key, Body: source, ContentType: 'application/javascript' }));
|
|
76
|
+
const name = key.replace(/\.zap$/, '');
|
|
77
|
+
const cronExpr = (0, cron_1.parseCron)(source);
|
|
78
|
+
if (cronExpr) {
|
|
79
|
+
const { functionArn } = readConfig();
|
|
80
|
+
if (!functionArn) {
|
|
81
|
+
console.error('run npm run init first');
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
await (0, cron_1.upsertCron)(name, cronExpr, functionArn);
|
|
85
|
+
console.log(`+ ${name} ↻ ${cronExpr}`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
console.log(`+ ${name}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const program = new commander_1.Command()
|
|
92
|
+
.name('zap')
|
|
93
|
+
.description('Deploy .zap handlers to S3')
|
|
94
|
+
.version('0.1.0');
|
|
95
|
+
program
|
|
96
|
+
.command('init')
|
|
97
|
+
.description('provision AWS infrastructure and deploy the runtime')
|
|
98
|
+
.option('-r, --region <region>', 'AWS region', 'us-east-1')
|
|
99
|
+
.action(async (opts) => {
|
|
100
|
+
const { init } = await Promise.resolve().then(() => __importStar(require('./init')));
|
|
101
|
+
await init(opts.region);
|
|
102
|
+
});
|
|
103
|
+
program
|
|
104
|
+
.command('deploy <path>')
|
|
105
|
+
.description('upload a .zap file or directory to S3')
|
|
106
|
+
.option('-b, --bucket <bucket>', 'S3 bucket (or set ZAP_BUCKET)')
|
|
107
|
+
.action(async (path, opts) => {
|
|
108
|
+
const b = bucket(opts);
|
|
109
|
+
const info = await (0, promises_1.stat)(path);
|
|
110
|
+
if (info.isDirectory()) {
|
|
111
|
+
const files = await walkZap(path);
|
|
112
|
+
await Promise.all(files.map(({ filePath, key }) => deployFile(b, filePath, key)));
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const key = path.replace(/^\.\//, '');
|
|
116
|
+
await deployFile(b, path, key);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
program
|
|
120
|
+
.command('rm <name>')
|
|
121
|
+
.description('remove a handler from S3')
|
|
122
|
+
.option('-b, --bucket <bucket>', 'S3 bucket (or set ZAP_BUCKET)')
|
|
123
|
+
.action(async (name, opts) => {
|
|
124
|
+
const b = bucket(opts);
|
|
125
|
+
const key = name.endsWith('.zap') ? name : `${name}.zap`;
|
|
126
|
+
await s3.send(new client_s3_1.DeleteObjectCommand({ Bucket: b, Key: key }));
|
|
127
|
+
const { functionArn } = readConfig();
|
|
128
|
+
if (functionArn)
|
|
129
|
+
await (0, cron_1.removeCron)(name.replace(/\.zap$/, ''), functionArn);
|
|
130
|
+
console.log(`- ${name.replace(/\.zap$/, '')}`);
|
|
131
|
+
});
|
|
132
|
+
program
|
|
133
|
+
.command('ls')
|
|
134
|
+
.description('list deployed handlers')
|
|
135
|
+
.option('-b, --bucket <bucket>', 'S3 bucket (or set ZAP_BUCKET)')
|
|
136
|
+
.action(async (opts) => {
|
|
137
|
+
const b = bucket(opts);
|
|
138
|
+
const { Contents = [] } = await s3.send(new client_s3_1.ListObjectsV2Command({ Bucket: b }));
|
|
139
|
+
const handlers = Contents.filter(o => o.Key?.endsWith('.zap'));
|
|
140
|
+
if (!handlers.length)
|
|
141
|
+
return console.log('no handlers deployed');
|
|
142
|
+
handlers.forEach(o => console.log(o.Key.replace(/\.zap$/, '')));
|
|
143
|
+
});
|
|
144
|
+
program.parse();
|
package/dist/cron.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseCron = parseCron;
|
|
4
|
+
exports.upsertCron = upsertCron;
|
|
5
|
+
exports.removeCron = removeCron;
|
|
6
|
+
const client_eventbridge_1 = require("@aws-sdk/client-eventbridge");
|
|
7
|
+
const client_lambda_1 = require("@aws-sdk/client-lambda");
|
|
8
|
+
const eb = new client_eventbridge_1.EventBridgeClient({});
|
|
9
|
+
const lambda = new client_lambda_1.LambdaClient({});
|
|
10
|
+
// Parse // @cron <expr> from .zap source
|
|
11
|
+
function parseCron(source) {
|
|
12
|
+
const match = source.match(/^\/\/\s*@cron\s+(.+)/m);
|
|
13
|
+
return match ? match[1].trim() : null;
|
|
14
|
+
}
|
|
15
|
+
// Convert standard 5-field cron to EventBridge format
|
|
16
|
+
// EventBridge requires one of day-of-month or day-of-week to be ?
|
|
17
|
+
function toSchedule(expr) {
|
|
18
|
+
const [min, hour, dom, month, dow] = expr.split(' ');
|
|
19
|
+
const evbDom = dow !== '*' ? '?' : dom;
|
|
20
|
+
const evbDow = dow !== '*' ? dow : '?';
|
|
21
|
+
return `cron(${min} ${hour} ${evbDom} ${month} ${evbDow} *)`;
|
|
22
|
+
}
|
|
23
|
+
const ruleId = (name) => `zap-cron-${name.replace(/\//g, '-')}`;
|
|
24
|
+
async function upsertCron(name, expr, functionArn) {
|
|
25
|
+
const rule = ruleId(name);
|
|
26
|
+
const { RuleArn } = await eb.send(new client_eventbridge_1.PutRuleCommand({
|
|
27
|
+
Name: rule,
|
|
28
|
+
ScheduleExpression: toSchedule(expr),
|
|
29
|
+
State: 'ENABLED',
|
|
30
|
+
}));
|
|
31
|
+
await eb.send(new client_eventbridge_1.PutTargetsCommand({
|
|
32
|
+
Rule: rule,
|
|
33
|
+
Targets: [{ Id: 'zap', Arn: functionArn, Input: JSON.stringify({ zap: { cron: name } }) }],
|
|
34
|
+
}));
|
|
35
|
+
try {
|
|
36
|
+
await lambda.send(new client_lambda_1.AddPermissionCommand({
|
|
37
|
+
FunctionName: functionArn,
|
|
38
|
+
StatementId: ruleId(name),
|
|
39
|
+
Action: 'lambda:InvokeFunction',
|
|
40
|
+
Principal: 'events.amazonaws.com',
|
|
41
|
+
SourceArn: RuleArn,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
if (err.name !== 'ResourceConflictException')
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function removeCron(name, functionArn) {
|
|
50
|
+
const rule = ruleId(name);
|
|
51
|
+
try {
|
|
52
|
+
await eb.send(new client_eventbridge_1.RemoveTargetsCommand({ Rule: rule, Ids: ['zap'] }));
|
|
53
|
+
await eb.send(new client_eventbridge_1.DeleteRuleCommand({ Name: rule }));
|
|
54
|
+
await lambda.send(new client_lambda_1.RemovePermissionCommand({ FunctionName: functionArn, StatementId: ruleId(name) }));
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
if (err.name !== 'ResourceNotFoundException')
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
}
|
package/dist/dev.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const node_http_1 = require("node:http");
|
|
4
|
+
const promises_1 = require("node:fs/promises");
|
|
5
|
+
const eval_1 = require("./eval");
|
|
6
|
+
const types_1 = require("./types");
|
|
7
|
+
const PORT = Number(process.env.PORT ?? 3000);
|
|
8
|
+
const loader = (name) => (0, promises_1.readFile)(`${name}.zap`, 'utf8');
|
|
9
|
+
function readBody(req) {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
let data = '';
|
|
12
|
+
req.on('data', (chunk) => (data += chunk.toString()));
|
|
13
|
+
req.on('end', () => resolve(data || null));
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
const server = (0, node_http_1.createServer)(async (req, res) => {
|
|
17
|
+
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
18
|
+
const path = url.pathname;
|
|
19
|
+
const zapFile = `.${path}.zap`;
|
|
20
|
+
const zapReq = {
|
|
21
|
+
method: req.method,
|
|
22
|
+
path,
|
|
23
|
+
query: Object.fromEntries(url.searchParams),
|
|
24
|
+
headers: req.headers,
|
|
25
|
+
body: await readBody(req),
|
|
26
|
+
};
|
|
27
|
+
try {
|
|
28
|
+
const source = await (0, promises_1.readFile)(zapFile, 'utf8');
|
|
29
|
+
const zapRes = await (0, eval_1.run)(source, zapReq, loader);
|
|
30
|
+
res.writeHead(zapRes.status ?? 200, zapRes.headers);
|
|
31
|
+
res.end((0, types_1.serialize)(zapRes.body));
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
if (err.code === 'ENOENT') {
|
|
35
|
+
res.writeHead(404).end(`No handler: ${zapFile}`);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
res.writeHead(500).end(err.message);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
server.listen(PORT, () => console.log(`zap dev → http://localhost:${PORT}`));
|
package/dist/eval.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.evalModule = evalModule;
|
|
7
|
+
exports.run = run;
|
|
8
|
+
const node_vm_1 = __importDefault(require("node:vm"));
|
|
9
|
+
const kv_1 = require("./kv");
|
|
10
|
+
const BASE_SANDBOX = {
|
|
11
|
+
fetch,
|
|
12
|
+
console,
|
|
13
|
+
URL,
|
|
14
|
+
URLSearchParams,
|
|
15
|
+
crypto,
|
|
16
|
+
Buffer,
|
|
17
|
+
setTimeout,
|
|
18
|
+
clearTimeout,
|
|
19
|
+
process: { env: process.env },
|
|
20
|
+
kv: kv_1.kv,
|
|
21
|
+
};
|
|
22
|
+
function evalModule(source, loader) {
|
|
23
|
+
const code = source.replace(/^export\s+default\s+/m, 'module.exports = ');
|
|
24
|
+
const mod = { exports: {} };
|
|
25
|
+
node_vm_1.default.runInNewContext(code, {
|
|
26
|
+
...BASE_SANDBOX,
|
|
27
|
+
module: mod,
|
|
28
|
+
exports: mod.exports,
|
|
29
|
+
zap: (name) => loader(name).then(src => evalModule(src, loader)),
|
|
30
|
+
});
|
|
31
|
+
return mod.exports;
|
|
32
|
+
}
|
|
33
|
+
async function run(source, req, loader) {
|
|
34
|
+
const handler = evalModule(source, loader);
|
|
35
|
+
return handler(req);
|
|
36
|
+
}
|
package/dist/handler.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handler = void 0;
|
|
4
|
+
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
5
|
+
const eval_1 = require("./eval");
|
|
6
|
+
const types_1 = require("./types");
|
|
7
|
+
const s3 = new client_s3_1.S3Client({});
|
|
8
|
+
const BUCKET = process.env.ZAP_BUCKET;
|
|
9
|
+
const loader = async (name) => {
|
|
10
|
+
const { Body } = await s3.send(new client_s3_1.GetObjectCommand({ Bucket: BUCKET, Key: `${name}.zap` }));
|
|
11
|
+
return Body.transformToString();
|
|
12
|
+
};
|
|
13
|
+
const handler = async (event) => {
|
|
14
|
+
// Cron invocation from EventBridge
|
|
15
|
+
if (event?.zap?.cron) {
|
|
16
|
+
try {
|
|
17
|
+
const source = await loader(event.zap.cron);
|
|
18
|
+
const fn = (0, eval_1.evalModule)(source, loader);
|
|
19
|
+
await fn();
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
console.error(`cron error [${event.zap.cron}]:`, err.message);
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// HTTP invocation from Function URL
|
|
27
|
+
const e = event;
|
|
28
|
+
const req = {
|
|
29
|
+
method: e.requestContext.http.method,
|
|
30
|
+
path: e.rawPath,
|
|
31
|
+
query: Object.fromEntries(new URLSearchParams(e.rawQueryString ?? '').entries()),
|
|
32
|
+
headers: e.headers,
|
|
33
|
+
body: e.body ?? null,
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
const source = await loader(req.path.replace(/^\//, ''));
|
|
37
|
+
const res = await (0, eval_1.run)(source, req, loader);
|
|
38
|
+
return { statusCode: res.status ?? 200, headers: res.headers, body: (0, types_1.serialize)(res.body) };
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
if (err.name === 'NoSuchKey')
|
|
42
|
+
return { statusCode: 404, body: `No handler for ${req.path}` };
|
|
43
|
+
return { statusCode: 500, body: err.message };
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
exports.handler = handler;
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.init = init;
|
|
4
|
+
const client_iam_1 = require("@aws-sdk/client-iam");
|
|
5
|
+
const client_lambda_1 = require("@aws-sdk/client-lambda");
|
|
6
|
+
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
7
|
+
const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
|
|
8
|
+
const node_child_process_1 = require("node:child_process");
|
|
9
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
|
+
const node_fs_1 = require("node:fs");
|
|
11
|
+
const ROLE = 'zap-runtime-role';
|
|
12
|
+
const FUNCTION = 'zap-runtime';
|
|
13
|
+
const TABLE = 'zap-kv';
|
|
14
|
+
const TRUST = JSON.stringify({
|
|
15
|
+
Version: '2012-10-17',
|
|
16
|
+
Statement: [{ Effect: 'Allow', Principal: { Service: 'lambda.amazonaws.com' }, Action: 'sts:AssumeRole' }],
|
|
17
|
+
});
|
|
18
|
+
const policy = (bucket) => JSON.stringify({
|
|
19
|
+
Version: '2012-10-17',
|
|
20
|
+
Statement: [
|
|
21
|
+
{ Effect: 'Allow', Action: ['s3:GetObject', 's3:ListBucket'], Resource: [`arn:aws:s3:::${bucket}`, `arn:aws:s3:::${bucket}/*`] },
|
|
22
|
+
{ Effect: 'Allow', Action: ['dynamodb:GetItem', 'dynamodb:PutItem', 'dynamodb:DeleteItem'], Resource: `arn:aws:dynamodb:*:*:table/${TABLE}` },
|
|
23
|
+
],
|
|
24
|
+
});
|
|
25
|
+
function step(label) {
|
|
26
|
+
process.stdout.write(` ${label.padEnd(24)}`);
|
|
27
|
+
return (note = '') => console.log(`✓${note ? ' ' + note : ''}`);
|
|
28
|
+
}
|
|
29
|
+
async function init(region) {
|
|
30
|
+
const s3 = new client_s3_1.S3Client({ region });
|
|
31
|
+
const iam = new client_iam_1.IAMClient({ region: 'us-east-1' });
|
|
32
|
+
const lambda = new client_lambda_1.LambdaClient({ region });
|
|
33
|
+
const dynamo = new client_dynamodb_1.DynamoDBClient({ region });
|
|
34
|
+
let config = {};
|
|
35
|
+
try {
|
|
36
|
+
config = JSON.parse((0, node_fs_1.readFileSync)('.zaprc', 'utf8'));
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
// Build
|
|
40
|
+
let done = step('building runtime');
|
|
41
|
+
(0, node_child_process_1.execSync)('npx tsc', { stdio: 'pipe' });
|
|
42
|
+
(0, node_child_process_1.execSync)('zip -j dist/runtime.zip dist/*.js', { stdio: 'pipe' });
|
|
43
|
+
done();
|
|
44
|
+
// Bucket
|
|
45
|
+
const bucket = config.bucket ?? `zap-${(0, node_crypto_1.randomBytes)(4).toString('hex')}`;
|
|
46
|
+
done = step('creating bucket');
|
|
47
|
+
try {
|
|
48
|
+
await s3.send(new client_s3_1.HeadBucketCommand({ Bucket: bucket }));
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (err.name !== 'NotFound' && err.$metadata?.httpStatusCode !== 404)
|
|
52
|
+
throw err;
|
|
53
|
+
await s3.send(new client_s3_1.CreateBucketCommand({
|
|
54
|
+
Bucket: bucket,
|
|
55
|
+
...(region !== 'us-east-1' && { CreateBucketConfiguration: { LocationConstraint: region } }),
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
done(bucket);
|
|
59
|
+
// KV table
|
|
60
|
+
done = step('creating kv table');
|
|
61
|
+
try {
|
|
62
|
+
await dynamo.send(new client_dynamodb_1.DescribeTableCommand({ TableName: TABLE }));
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
if (err.name !== 'ResourceNotFoundException')
|
|
66
|
+
throw err;
|
|
67
|
+
await dynamo.send(new client_dynamodb_1.CreateTableCommand({
|
|
68
|
+
TableName: TABLE,
|
|
69
|
+
KeySchema: [{ AttributeName: 'k', KeyType: 'HASH' }],
|
|
70
|
+
AttributeDefinitions: [{ AttributeName: 'k', AttributeType: 'S' }],
|
|
71
|
+
BillingMode: 'PAY_PER_REQUEST',
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
done(TABLE);
|
|
75
|
+
// IAM role
|
|
76
|
+
done = step('configuring iam');
|
|
77
|
+
let roleArn;
|
|
78
|
+
let isNew = false;
|
|
79
|
+
try {
|
|
80
|
+
const { Role } = await iam.send(new client_iam_1.GetRoleCommand({ RoleName: ROLE }));
|
|
81
|
+
roleArn = Role.Arn;
|
|
82
|
+
await iam.send(new client_iam_1.PutRolePolicyCommand({ RoleName: ROLE, PolicyName: 'zap-access', PolicyDocument: policy(bucket) }));
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
if (err.name !== 'NoSuchEntityException')
|
|
86
|
+
throw err;
|
|
87
|
+
isNew = true;
|
|
88
|
+
const { Role } = await iam.send(new client_iam_1.CreateRoleCommand({ RoleName: ROLE, AssumeRolePolicyDocument: TRUST }));
|
|
89
|
+
roleArn = Role.Arn;
|
|
90
|
+
await iam.send(new client_iam_1.AttachRolePolicyCommand({ RoleName: ROLE, PolicyArn: 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' }));
|
|
91
|
+
await iam.send(new client_iam_1.PutRolePolicyCommand({ RoleName: ROLE, PolicyName: 'zap-access', PolicyDocument: policy(bucket) }));
|
|
92
|
+
}
|
|
93
|
+
if (isNew) {
|
|
94
|
+
process.stdout.write('propagating...');
|
|
95
|
+
await new Promise(r => setTimeout(r, 10_000));
|
|
96
|
+
process.stdout.write('\r configuring iam ');
|
|
97
|
+
}
|
|
98
|
+
done();
|
|
99
|
+
// Lambda
|
|
100
|
+
done = step('deploying lambda');
|
|
101
|
+
const zip = (0, node_fs_1.readFileSync)('dist/runtime.zip');
|
|
102
|
+
const env = { ZAP_BUCKET: bucket, ZAP_TABLE: TABLE };
|
|
103
|
+
let functionArn;
|
|
104
|
+
try {
|
|
105
|
+
const { Configuration } = await lambda.send(new client_lambda_1.GetFunctionCommand({ FunctionName: FUNCTION }));
|
|
106
|
+
functionArn = Configuration.FunctionArn;
|
|
107
|
+
await lambda.send(new client_lambda_1.UpdateFunctionCodeCommand({ FunctionName: FUNCTION, ZipFile: zip }));
|
|
108
|
+
await lambda.send(new client_lambda_1.UpdateFunctionConfigurationCommand({ FunctionName: FUNCTION, Environment: { Variables: env } }));
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
if (err.name !== 'ResourceNotFoundException')
|
|
112
|
+
throw err;
|
|
113
|
+
const { FunctionArn } = await lambda.send(new client_lambda_1.CreateFunctionCommand({
|
|
114
|
+
FunctionName: FUNCTION,
|
|
115
|
+
Runtime: 'nodejs20.x',
|
|
116
|
+
Role: roleArn,
|
|
117
|
+
Handler: 'handler.handler',
|
|
118
|
+
Code: { ZipFile: zip },
|
|
119
|
+
Environment: { Variables: env },
|
|
120
|
+
Timeout: 30,
|
|
121
|
+
MemorySize: 256,
|
|
122
|
+
}));
|
|
123
|
+
functionArn = FunctionArn;
|
|
124
|
+
}
|
|
125
|
+
done();
|
|
126
|
+
// Function URL
|
|
127
|
+
done = step('creating endpoint');
|
|
128
|
+
let url;
|
|
129
|
+
try {
|
|
130
|
+
const { FunctionUrl } = await lambda.send(new client_lambda_1.GetFunctionUrlConfigCommand({ FunctionName: FUNCTION }));
|
|
131
|
+
url = FunctionUrl;
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
if (err.name !== 'ResourceNotFoundException')
|
|
135
|
+
throw err;
|
|
136
|
+
const { FunctionUrl } = await lambda.send(new client_lambda_1.CreateFunctionUrlConfigCommand({
|
|
137
|
+
FunctionName: FUNCTION,
|
|
138
|
+
AuthType: 'NONE',
|
|
139
|
+
Cors: { AllowOrigins: ['*'], AllowMethods: ['*'], AllowHeaders: ['*'] },
|
|
140
|
+
}));
|
|
141
|
+
await lambda.send(new client_lambda_1.AddPermissionCommand({
|
|
142
|
+
FunctionName: FUNCTION,
|
|
143
|
+
StatementId: 'public-access',
|
|
144
|
+
Action: 'lambda:InvokeFunctionUrl',
|
|
145
|
+
Principal: '*',
|
|
146
|
+
FunctionUrlAuthType: 'NONE',
|
|
147
|
+
}));
|
|
148
|
+
url = FunctionUrl;
|
|
149
|
+
}
|
|
150
|
+
done();
|
|
151
|
+
(0, node_fs_1.writeFileSync)('.zaprc', JSON.stringify({ bucket, function: FUNCTION, table: TABLE, region, url, functionArn }, null, 2));
|
|
152
|
+
console.log(`\n → ${url.trim()}\n`);
|
|
153
|
+
}
|
package/dist/kv.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.kv = void 0;
|
|
4
|
+
const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
|
|
5
|
+
const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
|
|
6
|
+
const TABLE = process.env.ZAP_TABLE ?? 'zap-kv';
|
|
7
|
+
const db = lib_dynamodb_1.DynamoDBDocumentClient.from(new client_dynamodb_1.DynamoDBClient({}));
|
|
8
|
+
exports.kv = {
|
|
9
|
+
get: async (key) => {
|
|
10
|
+
const { Item } = await db.send(new lib_dynamodb_1.GetCommand({ TableName: TABLE, Key: { k: key } }));
|
|
11
|
+
return Item?.v ?? null;
|
|
12
|
+
},
|
|
13
|
+
set: async (key, value) => {
|
|
14
|
+
await db.send(new lib_dynamodb_1.PutCommand({ TableName: TABLE, Item: { k: key, v: value } }));
|
|
15
|
+
},
|
|
16
|
+
del: async (key) => {
|
|
17
|
+
await db.send(new lib_dynamodb_1.DeleteCommand({ TableName: TABLE, Key: { k: key } }));
|
|
18
|
+
},
|
|
19
|
+
};
|
package/dist/types.js
ADDED
package/examples/api.zap
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export default async (req) => {
|
|
2
|
+
const url = req.query.url
|
|
3
|
+
if (!url) return { status: 400, body: 'Missing ?url= parameter' }
|
|
4
|
+
|
|
5
|
+
const upstream = await fetch(url, {
|
|
6
|
+
method: req.method === 'GET' ? 'GET' : req.method,
|
|
7
|
+
headers: { 'user-agent': 'zap-proxy/1.0' },
|
|
8
|
+
body: req.body ?? undefined,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
status: upstream.status,
|
|
13
|
+
headers: {
|
|
14
|
+
'content-type': upstream.headers.get('content-type') ?? 'application/octet-stream',
|
|
15
|
+
'access-control-allow-origin': '*',
|
|
16
|
+
},
|
|
17
|
+
body: await upstream.text(),
|
|
18
|
+
}
|
|
19
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kirkelliott/zap",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Drop a .zap file in S3. It becomes an API endpoint.",
|
|
5
|
+
"main": "dist/handler.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"zap": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsx src/dev.ts",
|
|
12
|
+
"init": "tsx src/cli.ts init",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"examples",
|
|
18
|
+
"demo"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/dmvjs/s3node.git"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/dmvjs/s3node",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@aws-sdk/client-dynamodb": "^3.1000.0",
|
|
31
|
+
"@aws-sdk/client-eventbridge": "^3.1000.0",
|
|
32
|
+
"@aws-sdk/client-iam": "^3.1000.0",
|
|
33
|
+
"@aws-sdk/client-lambda": "^3.1000.0",
|
|
34
|
+
"@aws-sdk/client-s3": "^3",
|
|
35
|
+
"@aws-sdk/lib-dynamodb": "^3.1000.0",
|
|
36
|
+
"commander": "^14.0.3"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/aws-lambda": "^8",
|
|
40
|
+
"@types/node": "^22",
|
|
41
|
+
"tsx": "^4",
|
|
42
|
+
"typescript": "^5"
|
|
43
|
+
}
|
|
44
|
+
}
|