@kirkelliott/zap 0.1.11 → 0.1.13
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 +75 -102
- package/demo/visitors.zap +21 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,24 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
> Drop a `.zap` file in S3. It becomes an API endpoint.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
You already know S3. You've been dropping files in buckets for years. This makes those files executable.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Drop `hello.zap` → GET `/hello` returns a response. Change the file → behavior changes instantly. No redeploy. No CI. No config.
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Setup
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
npm run init
|
|
14
|
+
mkdir my-project && cd my-project
|
|
15
|
+
npx @kirkelliott/zap init
|
|
17
16
|
```
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
This provisions everything on your AWS account and prints a URL. Takes about 30 seconds. Requires AWS credentials — run `aws configure` first if you haven't.
|
|
20
19
|
|
|
21
20
|
```
|
|
22
|
-
|
|
21
|
+
packaging runtime ✓
|
|
23
22
|
creating bucket ✓ zap-a3f2b8c1
|
|
24
23
|
creating kv table ✓ zap-kv
|
|
25
24
|
configuring iam ✓
|
|
@@ -31,33 +30,38 @@ npm run init
|
|
|
31
30
|
|
|
32
31
|
---
|
|
33
32
|
|
|
34
|
-
##
|
|
33
|
+
## Deploy a handler
|
|
35
34
|
|
|
36
35
|
```bash
|
|
37
|
-
|
|
36
|
+
npx @kirkelliott/zap deploy hello.zap
|
|
38
37
|
```
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
Or install the CLI once and drop the `npx`:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install -g @kirkelliott/zap
|
|
43
|
+
zap deploy hello.zap
|
|
44
|
+
```
|
|
41
45
|
|
|
42
46
|
---
|
|
43
47
|
|
|
44
48
|
## The .zap format
|
|
45
49
|
|
|
46
|
-
A `.zap` file
|
|
50
|
+
A `.zap` file is a JavaScript module that exports one function. That function handles the request.
|
|
47
51
|
|
|
48
52
|
```js
|
|
49
53
|
export default async (req) => {
|
|
50
|
-
return {
|
|
54
|
+
return { body: `hello ${req.query.name ?? 'world'}` }
|
|
51
55
|
}
|
|
52
56
|
```
|
|
53
57
|
|
|
54
58
|
### Request
|
|
55
59
|
|
|
56
60
|
```ts
|
|
57
|
-
req.method // 'GET' | 'POST' |
|
|
58
|
-
req.path // '/
|
|
59
|
-
req.query //
|
|
60
|
-
req.headers //
|
|
61
|
+
req.method // 'GET' | 'POST' | ...
|
|
62
|
+
req.path // '/hello'
|
|
63
|
+
req.query // { name: 'elliott' }
|
|
64
|
+
req.headers // { 'content-type': '...' }
|
|
61
65
|
req.body // string | null
|
|
62
66
|
```
|
|
63
67
|
|
|
@@ -67,35 +71,34 @@ req.body // string | null
|
|
|
67
71
|
{
|
|
68
72
|
status?: number // default 200
|
|
69
73
|
headers?: Record<string, string>
|
|
70
|
-
body?: string | object // objects
|
|
74
|
+
body?: string | object // objects become JSON automatically
|
|
71
75
|
}
|
|
72
76
|
```
|
|
73
77
|
|
|
74
|
-
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Built-ins
|
|
75
81
|
|
|
76
|
-
|
|
82
|
+
These are available in every `.zap` file — no imports needed:
|
|
77
83
|
|
|
78
|
-
| Name |
|
|
84
|
+
| Name | What it does |
|
|
79
85
|
|---|---|
|
|
80
|
-
| `fetch` |
|
|
81
|
-
| `kv` | Persistent key/value
|
|
82
|
-
| `zap(name)` |
|
|
86
|
+
| `fetch` | HTTP requests |
|
|
87
|
+
| `kv` | Persistent key/value storage |
|
|
88
|
+
| `zap(name)` | Load another `.zap` from the same bucket |
|
|
83
89
|
| `crypto` | Web Crypto API |
|
|
84
|
-
| `URL`, `URLSearchParams` | URL
|
|
85
|
-
| `Buffer` |
|
|
86
|
-
| `console` |
|
|
87
|
-
| `setTimeout`, `clearTimeout` | Timers |
|
|
90
|
+
| `URL`, `URLSearchParams` | URL parsing |
|
|
91
|
+
| `Buffer` | Binary data |
|
|
92
|
+
| `console` | Logs to CloudWatch |
|
|
88
93
|
| `process.env` | Environment variables |
|
|
89
94
|
|
|
90
95
|
---
|
|
91
96
|
|
|
92
|
-
## kv —
|
|
93
|
-
|
|
94
|
-
`kv` is a built-in key/value store backed by DynamoDB. No setup, no imports, no config — it's just there.
|
|
97
|
+
## kv — storage that persists
|
|
95
98
|
|
|
96
99
|
```js
|
|
97
|
-
await kv.set('key', value) //
|
|
98
|
-
await kv.get('key') // returns the value, or null
|
|
100
|
+
await kv.set('key', value) // string, number, object, array — anything
|
|
101
|
+
await kv.get('key') // returns the value, or null
|
|
99
102
|
await kv.del('key') // delete
|
|
100
103
|
```
|
|
101
104
|
|
|
@@ -108,46 +111,45 @@ export default async (req) => {
|
|
|
108
111
|
}
|
|
109
112
|
```
|
|
110
113
|
|
|
111
|
-
|
|
114
|
+
No database to provision. It's just there.
|
|
112
115
|
|
|
113
|
-
|
|
116
|
+
---
|
|
114
117
|
|
|
115
|
-
|
|
118
|
+
## zap() — one handler can load another
|
|
116
119
|
|
|
117
120
|
```js
|
|
118
|
-
// utils/
|
|
121
|
+
// utils/auth.zap
|
|
119
122
|
export default {
|
|
120
|
-
|
|
123
|
+
verify: (token) => token === process.env.SECRET,
|
|
121
124
|
}
|
|
122
125
|
```
|
|
123
126
|
|
|
124
127
|
```js
|
|
125
128
|
// api.zap
|
|
126
129
|
export default async (req) => {
|
|
127
|
-
const
|
|
128
|
-
|
|
130
|
+
const auth = await zap('utils/auth')
|
|
131
|
+
if (!auth.verify(req.headers.authorization))
|
|
132
|
+
return { status: 401, body: 'unauthorized' }
|
|
133
|
+
return { body: 'ok' }
|
|
129
134
|
}
|
|
130
135
|
```
|
|
131
136
|
|
|
132
|
-
|
|
137
|
+
S3 is the module system. Drop a file in, import it anywhere.
|
|
133
138
|
|
|
134
139
|
---
|
|
135
140
|
|
|
136
|
-
## @cron —
|
|
141
|
+
## @cron — run on a schedule
|
|
137
142
|
|
|
138
|
-
|
|
143
|
+
First line `// @cron <expr>` turns any handler into a scheduled job. `zap deploy` wires up the EventBridge rule automatically.
|
|
139
144
|
|
|
140
145
|
```js
|
|
141
146
|
// @cron 0 * * * *
|
|
142
147
|
export default async () => {
|
|
143
148
|
await kv.set('heartbeat', new Date().toISOString())
|
|
144
|
-
console.log('tick')
|
|
145
149
|
}
|
|
146
150
|
```
|
|
147
151
|
|
|
148
|
-
Standard
|
|
149
|
-
|
|
150
|
-
Cron handlers receive no request argument. All built-ins (`kv`, `fetch`, `zap()`, etc.) are available.
|
|
152
|
+
Standard cron syntax: `minute hour day month weekday`. `zap rm` removes the schedule when you remove the file.
|
|
151
153
|
|
|
152
154
|
---
|
|
153
155
|
|
|
@@ -155,8 +157,6 @@ Cron handlers receive no request argument. All built-ins (`kv`, `fetch`, `zap()`
|
|
|
155
157
|
|
|
156
158
|
### CORS proxy
|
|
157
159
|
|
|
158
|
-
Fetch any remote URL server-side, bypassing browser CORS restrictions.
|
|
159
|
-
|
|
160
160
|
```js
|
|
161
161
|
// proxy.zap
|
|
162
162
|
export default async (req) => {
|
|
@@ -179,89 +179,62 @@ export default async (req) => {
|
|
|
179
179
|
GET https://your-endpoint/proxy?url=https://api.example.com/data
|
|
180
180
|
```
|
|
181
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
182
|
---
|
|
203
183
|
|
|
204
184
|
## CLI
|
|
205
185
|
|
|
206
186
|
```
|
|
207
|
-
zap init Provision AWS
|
|
187
|
+
zap init Provision AWS and deploy the runtime
|
|
208
188
|
zap deploy <file|directory> Upload .zap file(s) to S3
|
|
209
|
-
zap rm <name> Remove a handler (and its cron rule
|
|
189
|
+
zap rm <name> Remove a handler (and its cron rule)
|
|
210
190
|
zap ls List deployed handlers
|
|
191
|
+
zap demo Deploy the built-in demo handlers
|
|
192
|
+
zap repair Fix Lambda permissions if the URL stops working
|
|
211
193
|
```
|
|
212
194
|
|
|
213
|
-
|
|
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.
|
|
195
|
+
`init` writes a `.zaprc` to the project directory. All other commands read bucket and region from it — no flags needed.
|
|
221
196
|
|
|
222
197
|
---
|
|
223
198
|
|
|
224
|
-
##
|
|
199
|
+
## What gets created
|
|
225
200
|
|
|
226
|
-
|
|
201
|
+
`zap init` provisions six things on your AWS account:
|
|
227
202
|
|
|
228
|
-
| Resource |
|
|
203
|
+
| Resource | What it is |
|
|
229
204
|
|---|---|
|
|
230
|
-
| S3 bucket |
|
|
231
|
-
| DynamoDB table
|
|
232
|
-
| IAM role
|
|
233
|
-
| Lambda function
|
|
234
|
-
| Lambda Function URL |
|
|
235
|
-
| EventBridge rules | One per `@cron` handler
|
|
205
|
+
| S3 bucket | Where your `.zap` files live |
|
|
206
|
+
| DynamoDB table | Backs `kv` |
|
|
207
|
+
| IAM role | Lets Lambda read S3 and write DynamoDB |
|
|
208
|
+
| Lambda function | The runtime that runs your handlers |
|
|
209
|
+
| Lambda Function URL | Your public HTTPS endpoint |
|
|
210
|
+
| EventBridge rules | One per `@cron` handler |
|
|
236
211
|
|
|
237
212
|
---
|
|
238
213
|
|
|
239
214
|
## Cost
|
|
240
215
|
|
|
241
|
-
|
|
216
|
+
All within the AWS permanent free tier:
|
|
242
217
|
|
|
243
218
|
| Service | Free tier |
|
|
244
219
|
|---|---|
|
|
245
|
-
| Lambda | 1M requests/month, 400K GB-seconds
|
|
246
|
-
| S3 | 5GB
|
|
247
|
-
| DynamoDB | 25 WCU/RCU, 25GB
|
|
248
|
-
| EventBridge | 14M scheduled invocations/month
|
|
220
|
+
| Lambda | 1M requests/month, 400K GB-seconds |
|
|
221
|
+
| S3 | 5GB, 20K GET requests/month |
|
|
222
|
+
| DynamoDB | 25 WCU/RCU, 25GB |
|
|
223
|
+
| EventBridge | 14M scheduled invocations/month |
|
|
249
224
|
|
|
250
225
|
---
|
|
251
226
|
|
|
252
227
|
## How it works
|
|
253
228
|
|
|
254
|
-
|
|
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
|
|
229
|
+
One Lambda function runs permanently. Every request:
|
|
261
230
|
|
|
262
|
-
|
|
231
|
+
1. Parses the path — `/proxy` → `proxy.zap`
|
|
232
|
+
2. Fetches that file from S3
|
|
233
|
+
3. Evaluates it in a sandboxed JS environment
|
|
234
|
+
4. Calls the exported function with the request
|
|
235
|
+
5. Returns the response
|
|
263
236
|
|
|
264
|
-
|
|
237
|
+
Updating a handler means updating a file in S3. The runtime always reads fresh.
|
|
265
238
|
|
|
266
239
|
---
|
|
267
240
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export default async (req) => {
|
|
2
|
+
const count = ((await kv.get('visitors')) ?? 0) + 1
|
|
3
|
+
await kv.set('visitors', count)
|
|
4
|
+
return {
|
|
5
|
+
headers: { 'content-type': 'text/html' },
|
|
6
|
+
body: `<!doctype html>
|
|
7
|
+
<html>
|
|
8
|
+
<head><meta charset="utf-8"><title>visitors</title>
|
|
9
|
+
<style>
|
|
10
|
+
body { font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #000; color: #fff; }
|
|
11
|
+
h1 { font-size: 20vw; font-weight: 900; letter-spacing: -0.05em; }
|
|
12
|
+
p { position: fixed; bottom: 2rem; font-size: 1rem; opacity: 0.4; }
|
|
13
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<h1>${count}</h1>
|
|
17
|
+
<p>visitors</p>
|
|
18
|
+
</body>
|
|
19
|
+
</html>`
|
|
20
|
+
}
|
|
21
|
+
}
|