capability-worker 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 +21 -0
- package/README.md +1012 -0
- package/dist/chunks/yaIxL224.js +1 -0
- package/dist/node/build-capability-assets-cli.d.ts +2 -0
- package/dist/node/build-capability-assets-cli.js +2 -0
- package/dist/node/index.d.ts +44 -0
- package/dist/node/index.js +1 -0
- package/dist/node/validate-cli.d.ts +2 -0
- package/dist/node/validate-cli.js +2 -0
- package/dist/runtime/auth/index.d.ts +55 -0
- package/dist/runtime/auth/index.js +1 -0
- package/dist/runtime/credentials/index.d.ts +33 -0
- package/dist/runtime/credentials/index.js +1 -0
- package/dist/runtime/crypto/index.d.ts +16 -0
- package/dist/runtime/crypto/index.js +1 -0
- package/dist/runtime/files/index.d.ts +39 -0
- package/dist/runtime/files/index.js +1 -0
- package/dist/runtime/http/index.d.ts +24 -0
- package/dist/runtime/http/index.js +1 -0
- package/dist/runtime/index.d.ts +10 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/storage/index.d.ts +86 -0
- package/dist/runtime/storage/index.js +1 -0
- package/dist/runtime/validation/index.d.ts +22 -0
- package/dist/runtime/validation/index.js +1 -0
- package/dist/testing/index.d.ts +28 -0
- package/dist/testing/index.js +1 -0
- package/dist/types-BCUAqVuU.d.ts +44 -0
- package/package.json +77 -0
package/README.md
ADDED
|
@@ -0,0 +1,1012 @@
|
|
|
1
|
+
# capability-worker
|
|
2
|
+
|
|
3
|
+
`capability-worker` is the shared toolkit for building Cloudflare Workers that expose a CLI-like capability over HTTP.
|
|
4
|
+
|
|
5
|
+
Use this package when you are building a capability worker and want the common platform mechanics handled in one place:
|
|
6
|
+
|
|
7
|
+
- public capability contract assets
|
|
8
|
+
- `capability-signature-v1` verification
|
|
9
|
+
- public key lookup and caching
|
|
10
|
+
- first-party browser auth-cookie verification
|
|
11
|
+
- JSON, text, redirect, and error responses
|
|
12
|
+
- crypto helpers for Workers
|
|
13
|
+
- encrypted credential fields
|
|
14
|
+
- multipart file parsing and file validation
|
|
15
|
+
- R2/S3-compatible presigned URLs
|
|
16
|
+
- Worker-proxied signed download URLs
|
|
17
|
+
- contract validation
|
|
18
|
+
- test helpers for signed requests
|
|
19
|
+
|
|
20
|
+
The package lets each worker focus on provider or product behavior: mail, calendar, meetings, invoices, files, or another domain.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
pnpm add capability-worker
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The package is ESM-only and usable from Cloudflare Workers.
|
|
29
|
+
|
|
30
|
+
Runtime imports use subpaths so the worker only imports the modules it needs:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { requireCapabilitySignature } from "capability-worker/auth";
|
|
34
|
+
import { jsonResponse, errorResponse } from "capability-worker/http";
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Node-only tools, such as the asset builder CLI, are exposed through package binaries:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
pnpm exec capability-build-assets --validate
|
|
41
|
+
pnpm exec capability-validate
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Mental Model
|
|
45
|
+
|
|
46
|
+
A capability worker has two parts:
|
|
47
|
+
|
|
48
|
+
1. Static public contract assets that describe the CLI and HTTP mapping.
|
|
49
|
+
2. Dynamic API routes that execute account-scoped operations.
|
|
50
|
+
|
|
51
|
+
The static assets live at the installed capability URL:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
<capability-url>/info
|
|
55
|
+
<capability-url>/definition
|
|
56
|
+
<capability-url>/mapping
|
|
57
|
+
<capability-url>/skill/SKILL.md
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
For a capability installed at `https://meeting.example.com/api`, the files are:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
https://meeting.example.com/api/info
|
|
64
|
+
https://meeting.example.com/api/definition
|
|
65
|
+
https://meeting.example.com/api/mapping
|
|
66
|
+
https://meeting.example.com/api/skill/SKILL.md
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The dynamic API also lives under `apiBaseUrl`. Account-scoped routes must verify the capability signature before reading or writing account data.
|
|
70
|
+
|
|
71
|
+
## Recommended Worker Layout
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
worker/
|
|
75
|
+
capability/
|
|
76
|
+
info.template.json
|
|
77
|
+
public/
|
|
78
|
+
api/
|
|
79
|
+
info
|
|
80
|
+
definition
|
|
81
|
+
mapping
|
|
82
|
+
skill/
|
|
83
|
+
SKILL.md
|
|
84
|
+
_headers
|
|
85
|
+
scripts/
|
|
86
|
+
src/
|
|
87
|
+
worker.ts
|
|
88
|
+
cli/
|
|
89
|
+
definition.json
|
|
90
|
+
mapping.json
|
|
91
|
+
skills/
|
|
92
|
+
meeting/
|
|
93
|
+
SKILL.md
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The package helps generate `worker/public/...` from the source files in `capability/`, `cli/`, and `skills/`.
|
|
97
|
+
|
|
98
|
+
## Exports
|
|
99
|
+
|
|
100
|
+
Available exports:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import {} from "capability-worker";
|
|
104
|
+
import {} from "capability-worker/auth";
|
|
105
|
+
import {} from "capability-worker/crypto";
|
|
106
|
+
import {} from "capability-worker/credentials";
|
|
107
|
+
import {} from "capability-worker/files";
|
|
108
|
+
import {} from "capability-worker/http";
|
|
109
|
+
import {} from "capability-worker/storage";
|
|
110
|
+
import {} from "capability-worker/testing";
|
|
111
|
+
import {} from "capability-worker/validation";
|
|
112
|
+
import {} from "capability-worker/node";
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Use runtime exports from Worker code. Use `testing` from tests. Use `node` or the package CLIs from Node scripts only.
|
|
116
|
+
|
|
117
|
+
## 1. Generate Capability Assets
|
|
118
|
+
|
|
119
|
+
Use `capability-build-assets` to create the public files that a capability client installs and reads.
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
CAPABILITY_BASE_URL=https://meeting.example.com/api \
|
|
123
|
+
APP_ASSET_VERSION=0.1.0 \
|
|
124
|
+
pnpm exec capability-build-assets --validate
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Common package script:
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"scripts": {
|
|
132
|
+
"build:capability-assets": "capability-build-assets --validate"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Worker-specific script:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"scripts": {
|
|
142
|
+
"build:capability-assets": "CAPABILITY_BASE_URL=https://meeting.example.com/api APP_ASSET_VERSION=0.1.0 capability-build-assets --validate"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Useful flags:
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
capability-build-assets \
|
|
151
|
+
--worker-root worker \
|
|
152
|
+
--repo-root . \
|
|
153
|
+
--public-root worker/public \
|
|
154
|
+
--validate
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Useful environment variables:
|
|
158
|
+
|
|
159
|
+
```text
|
|
160
|
+
CAPABILITY_BASE_URL
|
|
161
|
+
APP_ASSET_VERSION
|
|
162
|
+
CAPABILITY_INFO_TEMPLATE_PATH
|
|
163
|
+
CAPABILITY_DEFINITION_PATH
|
|
164
|
+
CAPABILITY_MAPPING_PATH
|
|
165
|
+
CAPABILITY_SKILL_ENTRYPOINT
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The generated layout looks like this:
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
worker/public/
|
|
172
|
+
api/
|
|
173
|
+
info
|
|
174
|
+
definition
|
|
175
|
+
mapping
|
|
176
|
+
skill/
|
|
177
|
+
SKILL.md
|
|
178
|
+
_headers
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Configure Wrangler static assets:
|
|
182
|
+
|
|
183
|
+
```jsonc
|
|
184
|
+
{
|
|
185
|
+
"assets": {
|
|
186
|
+
"directory": "./public",
|
|
187
|
+
"binding": "ASSETS",
|
|
188
|
+
"html_handling": "none",
|
|
189
|
+
"not_found_handling": "none"
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The builder:
|
|
195
|
+
|
|
196
|
+
- normalize `CAPABILITY_BASE_URL`
|
|
197
|
+
- generate files under the URL path prefix, such as `/api`
|
|
198
|
+
- replace template variables such as `${CAPABILITY_BASE_URL}` and `${APP_ASSET_VERSION}`
|
|
199
|
+
- copy the skill entrypoint and skill resources
|
|
200
|
+
- reject unsafe paths such as `../secret`
|
|
201
|
+
- write `_headers` with JSON and Markdown content types
|
|
202
|
+
- optionally validate generated assets
|
|
203
|
+
|
|
204
|
+
## 2. Verify Signed Capability Requests
|
|
205
|
+
|
|
206
|
+
Use `capability-signature-v1` for agent or server-to-server account-scoped routes.
|
|
207
|
+
|
|
208
|
+
Required headers:
|
|
209
|
+
|
|
210
|
+
```text
|
|
211
|
+
x-capability-account-id: <account-id>
|
|
212
|
+
x-capability-timestamp: <unix-seconds>
|
|
213
|
+
x-capability-key-id: <key-id>
|
|
214
|
+
x-capability-signature: v1=<base64url-signature>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Canonical string:
|
|
218
|
+
|
|
219
|
+
```text
|
|
220
|
+
CAPABILITY-SIGNATURE-V1
|
|
221
|
+
<METHOD>
|
|
222
|
+
<PATH_WITH_QUERY>
|
|
223
|
+
<TIMESTAMP>
|
|
224
|
+
<ACCOUNT_ID>
|
|
225
|
+
<SHA256_HEX_BODY>
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Use `requireCapabilitySignature` when route handlers prefer exception flow:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
import { requireCapabilitySignature } from "capability-worker/auth";
|
|
232
|
+
import { errorResponse, jsonResponse } from "capability-worker/http";
|
|
233
|
+
|
|
234
|
+
export default {
|
|
235
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
236
|
+
try {
|
|
237
|
+
const { accountId } = await requireCapabilitySignature(request, env);
|
|
238
|
+
|
|
239
|
+
const rows = await env.DB.prepare(
|
|
240
|
+
"SELECT * FROM meetings WHERE account_id = ? ORDER BY starts_at DESC"
|
|
241
|
+
)
|
|
242
|
+
.bind(accountId)
|
|
243
|
+
.all();
|
|
244
|
+
|
|
245
|
+
return jsonResponse({ ok: true, data: rows.results });
|
|
246
|
+
} catch (error) {
|
|
247
|
+
return errorResponse(error);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Use `verifyCapabilitySignature` when route handlers prefer explicit result handling:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { verifyCapabilitySignature } from "capability-worker/auth";
|
|
257
|
+
import { errorJson, jsonResponse } from "capability-worker/http";
|
|
258
|
+
|
|
259
|
+
const auth = await verifyCapabilitySignature(request, env);
|
|
260
|
+
|
|
261
|
+
if (!auth.ok) {
|
|
262
|
+
return errorJson(auth.code, auth.message, auth.status);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return jsonResponse({
|
|
266
|
+
ok: true,
|
|
267
|
+
accountId: auth.accountId
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Rules for signed routes:
|
|
272
|
+
|
|
273
|
+
- verify before account-scoped reads or writes
|
|
274
|
+
- use the verified account id as the only tenant scope
|
|
275
|
+
- include query strings in the canonical path
|
|
276
|
+
- hash the exact raw request body bytes
|
|
277
|
+
- never accept account id from the body or query string
|
|
278
|
+
- return `404` when a resource does not exist inside the verified account scope
|
|
279
|
+
|
|
280
|
+
## 3. Fetch And Cache Public Keys
|
|
281
|
+
|
|
282
|
+
Use the public key store behind signature and auth-cookie verification.
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import { PublicKeyStore } from "capability-worker/auth";
|
|
286
|
+
|
|
287
|
+
const keyStore = new PublicKeyStore(env, {
|
|
288
|
+
cacheTtlMs: 300_000
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const key = await keyStore.getKey("key_123");
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Supported key sources, in priority order:
|
|
295
|
+
|
|
296
|
+
1. `CAPABILITY_PUBLIC_KEYS_JSON`
|
|
297
|
+
2. `CAPABILITY_PUBLIC_KEYS`
|
|
298
|
+
3. `CAPABILITY_PUBLIC_KEYS_SERVICE.fetch(CAPABILITY_PUBLIC_KEYS_URL)`
|
|
299
|
+
4. `fetch(CAPABILITY_PUBLIC_KEYS_URL)`
|
|
300
|
+
|
|
301
|
+
Recommended Wrangler variables:
|
|
302
|
+
|
|
303
|
+
```toml
|
|
304
|
+
[vars]
|
|
305
|
+
CAPABILITY_PUBLIC_KEYS_URL = "https://host.example.com/capability/public-keys"
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
When the public-key endpoint is another Worker on the same Cloudflare zone, also use a service binding in production:
|
|
309
|
+
|
|
310
|
+
```toml
|
|
311
|
+
[[services]]
|
|
312
|
+
binding = "CAPABILITY_PUBLIC_KEYS_SERVICE"
|
|
313
|
+
service = "public-keys-worker"
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Public key rules:
|
|
317
|
+
|
|
318
|
+
- accept `{ "keys": [...] }` and raw `[...]` payloads
|
|
319
|
+
- reject private key material such as `d`
|
|
320
|
+
- refetch keys once when a key id is unknown
|
|
321
|
+
- clear caches in tests with `clearGlobalPublicKeyCache`
|
|
322
|
+
|
|
323
|
+
## 4. Verify First-Party Browser Auth Cookies
|
|
324
|
+
|
|
325
|
+
Use browser auth cookie helpers when the capability also has a first-party site.
|
|
326
|
+
|
|
327
|
+
Example:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
import { verifyAuthCookie } from "capability-worker/auth";
|
|
331
|
+
import { errorJson, jsonResponse } from "capability-worker/http";
|
|
332
|
+
|
|
333
|
+
const auth = await verifyAuthCookie(request, env, {
|
|
334
|
+
cookieName: "auth"
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
if (!auth.ok) {
|
|
338
|
+
return errorJson(auth.code, auth.message, auth.status);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const meeting = await env.DB.prepare(
|
|
342
|
+
"SELECT * FROM meetings WHERE id = ? AND account_id = ?"
|
|
343
|
+
)
|
|
344
|
+
.bind(meetingId, auth.accountId)
|
|
345
|
+
.first();
|
|
346
|
+
|
|
347
|
+
if (!meeting) {
|
|
348
|
+
return errorJson("meeting_not_found", "Meeting not found.", 404);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return jsonResponse({ ok: true, data: meeting });
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Use browser auth for site-origin requests such as creator/admin views. Use capability signatures for agent/server-to-server management calls.
|
|
355
|
+
|
|
356
|
+
Browser auth rules:
|
|
357
|
+
|
|
358
|
+
- verify the JWT with keys from `CAPABILITY_PUBLIC_KEYS_URL`
|
|
359
|
+
- accept only configured asymmetric algorithms, normally `ES256`
|
|
360
|
+
- use `sub` as the verified account id
|
|
361
|
+
- treat `email` as trusted only when `email_verified === true`
|
|
362
|
+
- protect unsafe methods with an `Origin` allowlist
|
|
363
|
+
- never accept admin status, creator id, or account id from the browser body, query string, local storage, or unsigned headers
|
|
364
|
+
|
|
365
|
+
For routes that can accept either a capability signature or browser cookie, use `accountAuthFromRequest`:
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
import { accountAuthFromRequest } from "capability-worker/auth";
|
|
369
|
+
|
|
370
|
+
const auth = await accountAuthFromRequest(request, env, {
|
|
371
|
+
cookieName: "auth"
|
|
372
|
+
});
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## 5. Return Consistent HTTP Responses
|
|
376
|
+
|
|
377
|
+
Use the HTTP helpers to keep response shape consistent without adopting a router.
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
import {
|
|
381
|
+
HttpError,
|
|
382
|
+
errorJson,
|
|
383
|
+
errorResponse,
|
|
384
|
+
jsonResponse,
|
|
385
|
+
textResponse
|
|
386
|
+
} from "capability-worker/http";
|
|
387
|
+
|
|
388
|
+
export async function readMeeting(request: Request, env: Env) {
|
|
389
|
+
const meeting = await loadMeeting(request, env);
|
|
390
|
+
|
|
391
|
+
if (!meeting) {
|
|
392
|
+
throw new HttpError(404, "meeting_not_found", "Meeting not found.");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return jsonResponse({ ok: true, data: meeting });
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Error shape:
|
|
400
|
+
|
|
401
|
+
```json
|
|
402
|
+
{
|
|
403
|
+
"ok": false,
|
|
404
|
+
"error": {
|
|
405
|
+
"code": "invalid_request",
|
|
406
|
+
"message": "Invalid request."
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Useful helpers:
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
jsonResponse(value, init);
|
|
415
|
+
textResponse(value, init);
|
|
416
|
+
htmlResponse(value, init);
|
|
417
|
+
redirectResponse(location, status);
|
|
418
|
+
errorJson(code, message, status, extra);
|
|
419
|
+
errorResponse(error);
|
|
420
|
+
parseJsonBody(request);
|
|
421
|
+
readJsonObject(request);
|
|
422
|
+
isJsonRequested(url, body);
|
|
423
|
+
stringParam(value);
|
|
424
|
+
stringArrayParam(value);
|
|
425
|
+
booleanParam(value);
|
|
426
|
+
limitParam(value, defaultValue, max);
|
|
427
|
+
getRepeated(url, key);
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Return text command output exactly as the agent or user sees it:
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
return textResponse(
|
|
434
|
+
[
|
|
435
|
+
"ID Starts Title",
|
|
436
|
+
"mtg_1 2026-07-06 10:00 Intro call"
|
|
437
|
+
].join("\n")
|
|
438
|
+
);
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
If a command supports JSON output, expose a normal `--json` option in the definition asset and return `application/json` only when requested.
|
|
442
|
+
|
|
443
|
+
## 6. Use Worker-Safe Crypto Helpers
|
|
444
|
+
|
|
445
|
+
Use `capability-worker/crypto` for common Web Crypto and encoding work.
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
import {
|
|
449
|
+
base64UrlDecode,
|
|
450
|
+
base64UrlEncode,
|
|
451
|
+
randomBase64Url,
|
|
452
|
+
sha256Hex,
|
|
453
|
+
timingSafeEqual
|
|
454
|
+
} from "capability-worker/crypto";
|
|
455
|
+
|
|
456
|
+
const id = `file_${randomBase64Url(12)}`;
|
|
457
|
+
const bodyHash = await sha256Hex(await request.arrayBuffer());
|
|
458
|
+
const token = base64UrlEncode(new TextEncoder().encode("hello"));
|
|
459
|
+
const bytes = base64UrlDecode(token);
|
|
460
|
+
const matches = timingSafeEqual("a", "a");
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Available helpers:
|
|
464
|
+
|
|
465
|
+
```ts
|
|
466
|
+
utf8(value);
|
|
467
|
+
decodeUtf8(value);
|
|
468
|
+
toArrayBuffer(bytes);
|
|
469
|
+
base64UrlEncode(value);
|
|
470
|
+
base64UrlDecode(value);
|
|
471
|
+
base64Encode(value);
|
|
472
|
+
base64Decode(value);
|
|
473
|
+
sha256Hex(value);
|
|
474
|
+
hmacSha256Base64Url(secret, payload);
|
|
475
|
+
randomBytes(byteLength);
|
|
476
|
+
randomBase64Url(byteLength);
|
|
477
|
+
timingSafeEqual(a, b);
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
Use these instead of copying base64url, hashing, HMAC, random token, and timing-safe comparison code across workers.
|
|
481
|
+
|
|
482
|
+
## 7. Encrypt Credential Fields
|
|
483
|
+
|
|
484
|
+
Use credential helpers for account-owned secrets such as refresh tokens, app passwords, SMTP credentials, API keys, or provider tokens.
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
import {
|
|
488
|
+
decryptCredentialField,
|
|
489
|
+
encryptCredentialField
|
|
490
|
+
} from "capability-worker/credentials";
|
|
491
|
+
|
|
492
|
+
const encrypted = await encryptCredentialField({
|
|
493
|
+
env,
|
|
494
|
+
accountId,
|
|
495
|
+
rowId: connectionId,
|
|
496
|
+
fieldName: "refresh_token",
|
|
497
|
+
plaintext: refreshToken
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
await env.DB.prepare(
|
|
501
|
+
"UPDATE google_connections SET refresh_token_encrypted = ? WHERE id = ? AND account_id = ?"
|
|
502
|
+
)
|
|
503
|
+
.bind(JSON.stringify(encrypted), connectionId, accountId)
|
|
504
|
+
.run();
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
Decrypt later using the same account id, row id, and field name:
|
|
508
|
+
|
|
509
|
+
```ts
|
|
510
|
+
const refreshToken = await decryptCredentialField({
|
|
511
|
+
env,
|
|
512
|
+
accountId,
|
|
513
|
+
rowId: connectionId,
|
|
514
|
+
fieldName: "refresh_token",
|
|
515
|
+
encrypted: JSON.parse(row.refresh_token_encrypted)
|
|
516
|
+
});
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
Environment convention:
|
|
520
|
+
|
|
521
|
+
```text
|
|
522
|
+
CREDENTIAL_ENCRYPTION_KEYS={"v1":"<base64url-32-byte-key>"}
|
|
523
|
+
CREDENTIAL_ACTIVE_KEY_ID=v1
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Credential rules:
|
|
527
|
+
|
|
528
|
+
- store one encrypted field as `{ keyId, nonce, ciphertext }`
|
|
529
|
+
- use AES-GCM with a 12-byte random nonce
|
|
530
|
+
- use AAD that includes account id, row id, field name, and key id
|
|
531
|
+
- do not log plaintext, ciphertext, nonces, or full secret values
|
|
532
|
+
- fail in production when no keyring is configured
|
|
533
|
+
- rotate keys by changing `CREDENTIAL_ACTIVE_KEY_ID`; old fields continue to decrypt while their key remains in the keyring
|
|
534
|
+
|
|
535
|
+
The package does not own your database schema. It only encrypts and decrypts values.
|
|
536
|
+
|
|
537
|
+
## 8. Parse Multipart Requests And Validate Files
|
|
538
|
+
|
|
539
|
+
Use file helpers when commands accept local files from the host.
|
|
540
|
+
|
|
541
|
+
Expected multipart shape:
|
|
542
|
+
|
|
543
|
+
```text
|
|
544
|
+
payload=<JSON object string>
|
|
545
|
+
attachments=<raw bytes>
|
|
546
|
+
attachments=<raw bytes>
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Route example:
|
|
550
|
+
|
|
551
|
+
```ts
|
|
552
|
+
import {
|
|
553
|
+
parseMultipartRequest,
|
|
554
|
+
validateCapabilityFiles
|
|
555
|
+
} from "capability-worker/files";
|
|
556
|
+
import { requireCapabilitySignature } from "capability-worker/auth";
|
|
557
|
+
import { errorJson, jsonResponse } from "capability-worker/http";
|
|
558
|
+
|
|
559
|
+
const rawBody = await request.arrayBuffer();
|
|
560
|
+
|
|
561
|
+
const { accountId } = await requireCapabilitySignature(request, env, {
|
|
562
|
+
rawBody
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
const parsed = await parseMultipartRequest(
|
|
566
|
+
new Request(request, { body: rawBody }),
|
|
567
|
+
{
|
|
568
|
+
payloadFieldName: "payload",
|
|
569
|
+
fileFieldNames: ["attachments"],
|
|
570
|
+
maxTotalBytes: 20 * 1024 * 1024
|
|
571
|
+
}
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
const fileValidation = validateCapabilityFiles(parsed.files, {
|
|
575
|
+
allowedExtensions: [".pdf", ".png", ".jpg"],
|
|
576
|
+
allowedMimeTypes: ["application/pdf", "image/png", "image/jpeg"],
|
|
577
|
+
maxBytes: 10 * 1024 * 1024,
|
|
578
|
+
maxCount: 5,
|
|
579
|
+
maxTotalBytes: 20 * 1024 * 1024
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
if (!fileValidation.ok) {
|
|
583
|
+
return errorJson(
|
|
584
|
+
fileValidation.code,
|
|
585
|
+
fileValidation.message,
|
|
586
|
+
fileValidation.status
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return jsonResponse({
|
|
591
|
+
ok: true,
|
|
592
|
+
accountId,
|
|
593
|
+
payload: parsed.payload,
|
|
594
|
+
fileCount: parsed.files.length
|
|
595
|
+
});
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
Important: verify the signature before parsing multipart, but use the same raw bytes for both verification and parsing.
|
|
599
|
+
|
|
600
|
+
File rules:
|
|
601
|
+
|
|
602
|
+
- normalize and sanitize filenames
|
|
603
|
+
- reject slashes, nulls, carriage returns, and line feeds in filenames
|
|
604
|
+
- validate extension and MIME type server-side
|
|
605
|
+
- enforce per-file byte limit, file count, and total byte limit
|
|
606
|
+
- make file fields match the capability mapping
|
|
607
|
+
|
|
608
|
+
## 9. Create R2 Or S3-Compatible Presigned URLs
|
|
609
|
+
|
|
610
|
+
Use storage helpers when a worker writes generated files to a private bucket and returns a temporary download URL.
|
|
611
|
+
|
|
612
|
+
For Cloudflare R2:
|
|
613
|
+
|
|
614
|
+
```ts
|
|
615
|
+
import { presignR2ObjectUrl } from "capability-worker/storage";
|
|
616
|
+
|
|
617
|
+
const downloadUrl = await presignR2ObjectUrl(env, {
|
|
618
|
+
method: "GET",
|
|
619
|
+
key: `accounts/${accountId}/exports/${exportId}.pdf`,
|
|
620
|
+
expiresInSeconds: 3600,
|
|
621
|
+
query: {
|
|
622
|
+
"response-content-disposition": `attachment; filename="${filename}"`,
|
|
623
|
+
"response-content-type": "application/pdf"
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
R2 environment:
|
|
629
|
+
|
|
630
|
+
```text
|
|
631
|
+
R2_PUBLIC_BASE_URL
|
|
632
|
+
R2_ACCOUNT_ID
|
|
633
|
+
R2_BUCKET_NAME
|
|
634
|
+
R2_ACCESS_KEY_ID
|
|
635
|
+
R2_SECRET_ACCESS_KEY
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
For generic S3-compatible storage:
|
|
639
|
+
|
|
640
|
+
```ts
|
|
641
|
+
import { presignS3ObjectUrl } from "capability-worker/storage";
|
|
642
|
+
|
|
643
|
+
const url = await presignS3ObjectUrl({
|
|
644
|
+
method: "GET",
|
|
645
|
+
key: `accounts/${accountId}/exports/${exportId}.zip`,
|
|
646
|
+
expiresInSeconds: 3600,
|
|
647
|
+
config: {
|
|
648
|
+
baseUrl: "https://storage.example.com/bucket",
|
|
649
|
+
region: "auto",
|
|
650
|
+
service: "s3",
|
|
651
|
+
credentials: {
|
|
652
|
+
accessKeyId: env.S3_ACCESS_KEY_ID,
|
|
653
|
+
secretAccessKey: env.S3_SECRET_ACCESS_KEY
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
Presigned URL rules:
|
|
660
|
+
|
|
661
|
+
- default to read-only `GET` URLs
|
|
662
|
+
- cap expiry in worker code, usually at 24 hours or less
|
|
663
|
+
- keep buckets private
|
|
664
|
+
- include the verified account id in object keys
|
|
665
|
+
- never return access keys, secret keys, or signing keys
|
|
666
|
+
- only use response override query params for presentation metadata
|
|
667
|
+
- do not accept arbitrary object keys from request bodies without account-scope checks
|
|
668
|
+
|
|
669
|
+
## 10. Create Worker-Proxied Signed Download URLs
|
|
670
|
+
|
|
671
|
+
Use Worker-proxied signed download URLs for local development or when the Worker streams private R2 objects itself.
|
|
672
|
+
|
|
673
|
+
```ts
|
|
674
|
+
import {
|
|
675
|
+
createSignedDownloadUrl,
|
|
676
|
+
r2DownloadResponse,
|
|
677
|
+
verifySignedDownloadToken
|
|
678
|
+
} from "capability-worker/storage";
|
|
679
|
+
|
|
680
|
+
const url = await createSignedDownloadUrl({
|
|
681
|
+
env,
|
|
682
|
+
payload: {
|
|
683
|
+
accountId,
|
|
684
|
+
storageKey: `accounts/${accountId}/exports/${exportId}.pdf`,
|
|
685
|
+
filename: "invoice.pdf",
|
|
686
|
+
contentType: "application/pdf",
|
|
687
|
+
expiresAt: new Date(Date.now() + 15 * 60_000).toISOString(),
|
|
688
|
+
disposition: "attachment"
|
|
689
|
+
},
|
|
690
|
+
routePrefix: "/__dev/r2-download"
|
|
691
|
+
});
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
Handle the download route:
|
|
695
|
+
|
|
696
|
+
```ts
|
|
697
|
+
const url = new URL(request.url);
|
|
698
|
+
|
|
699
|
+
if (url.pathname === "/__dev/r2-download") {
|
|
700
|
+
const token = url.searchParams.get("token");
|
|
701
|
+
if (!token) {
|
|
702
|
+
return new Response("Missing token.", { status: 400 });
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const payload = await verifySignedDownloadToken(env, token);
|
|
706
|
+
return r2DownloadResponse({
|
|
707
|
+
bucket: env.PDFS,
|
|
708
|
+
payload
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
Signed download rules:
|
|
714
|
+
|
|
715
|
+
- sign payloads with HMAC-SHA256
|
|
716
|
+
- use timing-safe comparison
|
|
717
|
+
- return `403` for expired tokens
|
|
718
|
+
- return `404` for missing R2 objects
|
|
719
|
+
- include account id in the signed payload
|
|
720
|
+
- use `cache-control: no-store` for private proxy downloads
|
|
721
|
+
|
|
722
|
+
## 11. Validate Capability Contracts
|
|
723
|
+
|
|
724
|
+
Use validation before deploy and in tests.
|
|
725
|
+
|
|
726
|
+
```ts
|
|
727
|
+
import { validateCapabilityAssets } from "capability-worker/validation";
|
|
728
|
+
|
|
729
|
+
const issues = validateCapabilityAssets({
|
|
730
|
+
info,
|
|
731
|
+
definition,
|
|
732
|
+
mapping,
|
|
733
|
+
skillFiles: [
|
|
734
|
+
{
|
|
735
|
+
path: "SKILL.md",
|
|
736
|
+
content: skillMarkdown
|
|
737
|
+
}
|
|
738
|
+
]
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
const errors = issues.filter((issue) => issue.severity === "error");
|
|
742
|
+
|
|
743
|
+
if (errors.length > 0) {
|
|
744
|
+
throw new Error(errors.map((issue) => issue.message).join("\n"));
|
|
745
|
+
}
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
The validator checks:
|
|
749
|
+
|
|
750
|
+
- `info.schemaVersion`
|
|
751
|
+
- required `info` fields
|
|
752
|
+
- safe command names
|
|
753
|
+
- `definition.schemaVersion`
|
|
754
|
+
- `definition.cli.name === info.name`
|
|
755
|
+
- stable and unique command ids
|
|
756
|
+
- mapping keys exist in the definition
|
|
757
|
+
- mapping paths are relative
|
|
758
|
+
- path params exist in route paths
|
|
759
|
+
- mapped fields exist in command arguments or options
|
|
760
|
+
- file fields have constraints
|
|
761
|
+
- skill paths are safe relative paths
|
|
762
|
+
- public assets do not contain obvious secrets
|
|
763
|
+
- production `apiBaseUrl` uses HTTPS
|
|
764
|
+
|
|
765
|
+
CLI use:
|
|
766
|
+
|
|
767
|
+
```sh
|
|
768
|
+
pnpm exec capability-validate
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
Or validate during asset generation:
|
|
772
|
+
|
|
773
|
+
```sh
|
|
774
|
+
pnpm exec capability-build-assets --validate
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
## 12. Route Static Capability Assets
|
|
778
|
+
|
|
779
|
+
Most workers use Cloudflare Workers Static Assets to serve contract files directly.
|
|
780
|
+
|
|
781
|
+
If the worker must route assets itself, use the routing helper to detect capability asset paths and delegate to `env.ASSETS`.
|
|
782
|
+
|
|
783
|
+
```ts
|
|
784
|
+
import { isCapabilityAssetPath } from "capability-worker";
|
|
785
|
+
|
|
786
|
+
export default {
|
|
787
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
788
|
+
const url = new URL(request.url);
|
|
789
|
+
|
|
790
|
+
if (isCapabilityAssetPath(url.pathname, { basePath: "/api" })) {
|
|
791
|
+
return env.ASSETS.fetch(request);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
return handleApi(request, env);
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
The helper recognizes paths like:
|
|
800
|
+
|
|
801
|
+
```text
|
|
802
|
+
/info
|
|
803
|
+
/definition
|
|
804
|
+
/mapping
|
|
805
|
+
/skill/SKILL.md
|
|
806
|
+
/api/info
|
|
807
|
+
/api/definition
|
|
808
|
+
/api/mapping
|
|
809
|
+
/api/skill/SKILL.md
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
This helper is only for identifying contract assets. It is not a full router.
|
|
813
|
+
|
|
814
|
+
## 13. Test Signed Routes
|
|
815
|
+
|
|
816
|
+
Use `capability-worker/testing` to remove repeated signing boilerplate while still testing the real Worker entrypoint.
|
|
817
|
+
|
|
818
|
+
```ts
|
|
819
|
+
import {
|
|
820
|
+
createCapabilityTestKeyPair,
|
|
821
|
+
publicKeysResponse,
|
|
822
|
+
withCapabilitySignature
|
|
823
|
+
} from "capability-worker/testing";
|
|
824
|
+
import worker from "../src/worker";
|
|
825
|
+
|
|
826
|
+
test("lists meetings for the signed account", async () => {
|
|
827
|
+
const key = await createCapabilityTestKeyPair({ alg: "ES256" });
|
|
828
|
+
|
|
829
|
+
const env = {
|
|
830
|
+
...testEnv,
|
|
831
|
+
CAPABILITY_PUBLIC_KEYS_JSON: JSON.stringify({
|
|
832
|
+
keys: [key.publicJwk]
|
|
833
|
+
})
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
const request = await withCapabilitySignature({
|
|
837
|
+
request: new Request("https://meeting.example.com/api/meetings"),
|
|
838
|
+
accountId: "acct_123",
|
|
839
|
+
keyId: key.keyId,
|
|
840
|
+
privateJwk: key.privateJwk
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
const response = await worker.fetch(request, env);
|
|
844
|
+
|
|
845
|
+
expect(response.status).toBe(200);
|
|
846
|
+
});
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
Available helpers:
|
|
850
|
+
|
|
851
|
+
```ts
|
|
852
|
+
createCapabilityTestKeyPair(options);
|
|
853
|
+
signCapabilityRequest(input);
|
|
854
|
+
withCapabilitySignature(input);
|
|
855
|
+
publicKeysResponse(keys);
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
Test cases every worker covers:
|
|
859
|
+
|
|
860
|
+
- valid signatures
|
|
861
|
+
- missing signatures
|
|
862
|
+
- stale timestamps
|
|
863
|
+
- wrong body hashes
|
|
864
|
+
- unknown key ids and public-key refresh
|
|
865
|
+
- account isolation
|
|
866
|
+
- multipart body signatures
|
|
867
|
+
- generated contract assets
|
|
868
|
+
- text output mode
|
|
869
|
+
- JSON output mode when a command supports `--json`
|
|
870
|
+
|
|
871
|
+
Use Cloudflare's Workers Vitest integration for Worker tests. Do not replace D1, R2, KV, Durable Objects, assets, or bindings with hand-written mocks as proof that the Worker works.
|
|
872
|
+
|
|
873
|
+
## Environment Variables
|
|
874
|
+
|
|
875
|
+
Common worker variables:
|
|
876
|
+
|
|
877
|
+
```text
|
|
878
|
+
CAPABILITY_BASE_URL
|
|
879
|
+
APP_ASSET_VERSION
|
|
880
|
+
CAPABILITY_PUBLIC_KEYS_URL
|
|
881
|
+
CAPABILITY_PUBLIC_KEYS_JSON
|
|
882
|
+
CAPABILITY_PUBLIC_KEYS
|
|
883
|
+
CREDENTIAL_ENCRYPTION_KEYS
|
|
884
|
+
CREDENTIAL_ACTIVE_KEY_ID
|
|
885
|
+
R2_PUBLIC_BASE_URL
|
|
886
|
+
R2_ACCOUNT_ID
|
|
887
|
+
R2_BUCKET_NAME
|
|
888
|
+
R2_ACCESS_KEY_ID
|
|
889
|
+
R2_SECRET_ACCESS_KEY
|
|
890
|
+
S3_PUBLIC_BASE_URL
|
|
891
|
+
S3_ENDPOINT
|
|
892
|
+
S3_REGION
|
|
893
|
+
S3_BUCKET_NAME
|
|
894
|
+
S3_ACCESS_KEY_ID
|
|
895
|
+
S3_SECRET_ACCESS_KEY
|
|
896
|
+
DEV_DOWNLOAD_SECRET
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
Use Wrangler secrets for secret values:
|
|
900
|
+
|
|
901
|
+
```sh
|
|
902
|
+
wrangler secret put CREDENTIAL_ENCRYPTION_KEYS
|
|
903
|
+
wrangler secret put R2_ACCESS_KEY_ID
|
|
904
|
+
wrangler secret put R2_SECRET_ACCESS_KEY
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
Use Wrangler vars for public or non-secret values:
|
|
908
|
+
|
|
909
|
+
```toml
|
|
910
|
+
[vars]
|
|
911
|
+
CAPABILITY_BASE_URL = "https://meeting.example.com/api"
|
|
912
|
+
APP_ASSET_VERSION = "0.1.0"
|
|
913
|
+
CAPABILITY_PUBLIC_KEYS_URL = "https://host.example.com/capability/public-keys"
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
## New Capability Worker Recipe
|
|
917
|
+
|
|
918
|
+
1. Create `cli/definition.json`.
|
|
919
|
+
2. Create `cli/mapping.json`.
|
|
920
|
+
3. Create `skills/<capability-name>/SKILL.md`.
|
|
921
|
+
4. Create `worker/capability/info.template.json`.
|
|
922
|
+
5. Configure Wrangler static assets.
|
|
923
|
+
6. Add `build:capability-assets`.
|
|
924
|
+
7. Implement dynamic API routes.
|
|
925
|
+
8. Verify signed account-scoped routes with `requireCapabilitySignature` or `verifyCapabilitySignature`.
|
|
926
|
+
9. Scope every account-owned query and object key by verified account id.
|
|
927
|
+
10. Return final command output from the worker as text or JSON.
|
|
928
|
+
11. Validate assets with `capability-build-assets --validate`.
|
|
929
|
+
12. Test through Cloudflare Workers Vitest or Wrangler-backed tooling.
|
|
930
|
+
|
|
931
|
+
Minimal route skeleton:
|
|
932
|
+
|
|
933
|
+
```ts
|
|
934
|
+
import { requireCapabilitySignature } from "capability-worker/auth";
|
|
935
|
+
import { errorResponse, jsonResponse, textResponse } from "capability-worker/http";
|
|
936
|
+
|
|
937
|
+
export default {
|
|
938
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
939
|
+
try {
|
|
940
|
+
const url = new URL(request.url);
|
|
941
|
+
|
|
942
|
+
if (url.pathname === "/api/health") {
|
|
943
|
+
return jsonResponse({ ok: true });
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (url.pathname === "/api/meetings" && request.method === "GET") {
|
|
947
|
+
const { accountId } = await requireCapabilitySignature(request, env);
|
|
948
|
+
const wantsJson = url.searchParams.get("json") === "true";
|
|
949
|
+
|
|
950
|
+
const rows = await env.DB.prepare(
|
|
951
|
+
"SELECT id, title, starts_at FROM meetings WHERE account_id = ?"
|
|
952
|
+
)
|
|
953
|
+
.bind(accountId)
|
|
954
|
+
.all();
|
|
955
|
+
|
|
956
|
+
if (wantsJson) {
|
|
957
|
+
return jsonResponse({ ok: true, data: rows.results });
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
return textResponse(formatMeetingsTable(rows.results));
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
return jsonResponse(
|
|
964
|
+
{
|
|
965
|
+
ok: false,
|
|
966
|
+
error: {
|
|
967
|
+
code: "not_found",
|
|
968
|
+
message: "Not found."
|
|
969
|
+
}
|
|
970
|
+
},
|
|
971
|
+
404
|
|
972
|
+
);
|
|
973
|
+
} catch (error) {
|
|
974
|
+
return errorResponse(error);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
## What Stays Local To Each Worker
|
|
981
|
+
|
|
982
|
+
Keep these in the worker, not in `capability-worker`:
|
|
983
|
+
|
|
984
|
+
- provider API clients
|
|
985
|
+
- OAuth flows and provider-specific token refresh
|
|
986
|
+
- product database schema
|
|
987
|
+
- product domain validation
|
|
988
|
+
- command-specific output formatting
|
|
989
|
+
- email, calendar, invoice, or meeting business logic
|
|
990
|
+
- public site UI
|
|
991
|
+
- webhooks and provider-specific verification
|
|
992
|
+
- deployment-specific route ownership
|
|
993
|
+
|
|
994
|
+
The shared package owns repeated mechanics. The worker owns the product.
|
|
995
|
+
|
|
996
|
+
## Compatibility Checklist
|
|
997
|
+
|
|
998
|
+
Before shipping a worker that uses this package:
|
|
999
|
+
|
|
1000
|
+
- `/info`, `/definition`, `/mapping`, and `/skill/SKILL.md` are static assets
|
|
1001
|
+
- `info.name` matches `definition.cli.name`
|
|
1002
|
+
- command ids are stable and unique
|
|
1003
|
+
- every mapping key exists in the definition
|
|
1004
|
+
- mapping paths are relative under `apiBaseUrl`
|
|
1005
|
+
- public contract and skill assets contain no secrets or account data
|
|
1006
|
+
- production `apiBaseUrl` uses HTTPS
|
|
1007
|
+
- account-scoped routes verify `capability-signature-v1`
|
|
1008
|
+
- browser creator/admin routes verify the auth cookie and compare verified account id to stored ownership
|
|
1009
|
+
- every account-owned database query is scoped by verified account id
|
|
1010
|
+
- every account-owned object key includes verified account id
|
|
1011
|
+
- text and JSON output modes are documented and tested when both exist
|
|
1012
|
+
- tests use Wrangler-backed Cloudflare runtime tooling and configured bindings
|