capability-worker 0.1.0 → 0.1.3

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 CHANGED
@@ -41,6 +41,12 @@ pnpm exec capability-build-assets --validate
41
41
  pnpm exec capability-validate
42
42
  ```
43
43
 
44
+ ## Publishing
45
+
46
+ Pushes to `main` are released by `.github/workflows/publish.yml`. The workflow prepares a new `v<version>` tag and publishes to npm through Trusted Publishing in the same GitHub Actions run, without an npm token.
47
+
48
+ Configure the npm package Trusted Publisher before relying on the workflow. The required npmjs.com settings are documented in [trusted-publishing.md](https://github.com/Iliasnolsson/capability-worker/blob/main/trusted-publishing.md).
49
+
44
50
  ## Mental Model
45
51
 
46
52
  A capability worker has two parts:
@@ -229,7 +235,7 @@ Use `requireCapabilitySignature` when route handlers prefer exception flow:
229
235
 
230
236
  ```ts
231
237
  import { requireCapabilitySignature } from "capability-worker/auth";
232
- import { errorResponse, jsonResponse } from "capability-worker/http";
238
+ import { errorResponse, okJson } from "capability-worker/http";
233
239
 
234
240
  export default {
235
241
  async fetch(request: Request, env: Env): Promise<Response> {
@@ -242,7 +248,7 @@ export default {
242
248
  .bind(accountId)
243
249
  .all();
244
250
 
245
- return jsonResponse({ ok: true, data: rows.results });
251
+ return okJson(rows.results);
246
252
  } catch (error) {
247
253
  return errorResponse(error);
248
254
  }
@@ -254,7 +260,7 @@ Use `verifyCapabilitySignature` when route handlers prefer explicit result handl
254
260
 
255
261
  ```ts
256
262
  import { verifyCapabilitySignature } from "capability-worker/auth";
257
- import { errorJson, jsonResponse } from "capability-worker/http";
263
+ import { errorJson, okJson } from "capability-worker/http";
258
264
 
259
265
  const auth = await verifyCapabilitySignature(request, env);
260
266
 
@@ -262,10 +268,7 @@ if (!auth.ok) {
262
268
  return errorJson(auth.code, auth.message, auth.status);
263
269
  }
264
270
 
265
- return jsonResponse({
266
- ok: true,
267
- accountId: auth.accountId
268
- });
271
+ return okJson({ accountId: auth.accountId });
269
272
  ```
270
273
 
271
274
  Rules for signed routes:
@@ -382,6 +385,7 @@ import {
382
385
  errorJson,
383
386
  errorResponse,
384
387
  jsonResponse,
388
+ okJson,
385
389
  textResponse
386
390
  } from "capability-worker/http";
387
391
 
@@ -392,7 +396,16 @@ export async function readMeeting(request: Request, env: Env) {
392
396
  throw new HttpError(404, "meeting_not_found", "Meeting not found.");
393
397
  }
394
398
 
395
- return jsonResponse({ ok: true, data: meeting });
399
+ return okJson(meeting);
400
+ }
401
+ ```
402
+
403
+ Success shape:
404
+
405
+ ```json
406
+ {
407
+ "ok": true,
408
+ "data": {}
396
409
  }
397
410
  ```
398
411
 
@@ -412,6 +425,7 @@ Useful helpers:
412
425
 
413
426
  ```ts
414
427
  jsonResponse(value, init);
428
+ okJson(data, init);
415
429
  textResponse(value, init);
416
430
  htmlResponse(value, init);
417
431
  redirectResponse(location, status);
@@ -915,6 +929,10 @@ CAPABILITY_PUBLIC_KEYS_URL = "https://host.example.com/capability/public-keys"
915
929
 
916
930
  ## New Capability Worker Recipe
917
931
 
932
+ The npm package includes a small starter at `templates/new-capability-worker`.
933
+ It uses `capability-build-assets`, `requireCapabilitySignature`, `okJson`,
934
+ `errorJson`, and `textResponse` from this package.
935
+
918
936
  1. Create `cli/definition.json`.
919
937
  2. Create `cli/mapping.json`.
920
938
  3. Create `skills/<capability-name>/SKILL.md`.
@@ -932,7 +950,7 @@ Minimal route skeleton:
932
950
 
933
951
  ```ts
934
952
  import { requireCapabilitySignature } from "capability-worker/auth";
935
- import { errorResponse, jsonResponse, textResponse } from "capability-worker/http";
953
+ import { errorJson, errorResponse, okJson, textResponse } from "capability-worker/http";
936
954
 
937
955
  export default {
938
956
  async fetch(request: Request, env: Env): Promise<Response> {
@@ -940,7 +958,7 @@ export default {
940
958
  const url = new URL(request.url);
941
959
 
942
960
  if (url.pathname === "/api/health") {
943
- return jsonResponse({ ok: true });
961
+ return okJson({ status: "ok" });
944
962
  }
945
963
 
946
964
  if (url.pathname === "/api/meetings" && request.method === "GET") {
@@ -954,22 +972,13 @@ export default {
954
972
  .all();
955
973
 
956
974
  if (wantsJson) {
957
- return jsonResponse({ ok: true, data: rows.results });
975
+ return okJson(rows.results);
958
976
  }
959
977
 
960
978
  return textResponse(formatMeetingsTable(rows.results));
961
979
  }
962
980
 
963
- return jsonResponse(
964
- {
965
- ok: false,
966
- error: {
967
- code: "not_found",
968
- message: "Not found."
969
- }
970
- },
971
- 404
972
- );
981
+ return errorJson("not_found", "Not found.", 404);
973
982
  } catch (error) {
974
983
  return errorResponse(error);
975
984
  }
File without changes
File without changes
@@ -7,6 +7,7 @@ declare class HttpError extends Error {
7
7
  });
8
8
  }
9
9
  declare function jsonResponse(value: unknown, init?: ResponseInit | number): Response;
10
+ declare function okJson(data: unknown, init?: ResponseInit | number): Response;
10
11
  declare function textResponse(value: string, init?: ResponseInit | number): Response;
11
12
  declare function htmlResponse(value: string, init?: ResponseInit | number): Response;
12
13
  declare function redirectResponse(location: string, status?: number): Response;
@@ -21,4 +22,4 @@ declare function booleanParam(value: unknown): boolean;
21
22
  declare function limitParam(value: unknown, defaultValue?: number, max?: number): number;
22
23
  declare function getRepeated(url: URL, key: string): string[];
23
24
 
24
- export { HttpError, booleanParam, errorJson, errorResponse, getRepeated, htmlResponse, isJsonRequested, jsonResponse, limitParam, parseJsonBody, readJsonObject, redirectResponse, stringArrayParam, stringParam, textResponse };
25
+ export { HttpError, booleanParam, errorJson, errorResponse, getRepeated, htmlResponse, isJsonRequested, jsonResponse, limitParam, okJson, parseJsonBody, readJsonObject, redirectResponse, stringArrayParam, stringParam, textResponse };
@@ -1 +1 @@
1
- class t extends Error{status;code;expose;constructor(t,e,n,r){super(n),this.name="HttpError",this.status=t,this.code=e,this.expose=r?.expose??t<500}}function e(t,e=200){const n=d(e);return new Response(JSON.stringify(t,null,2),{...n,headers:m(n.headers,"content-type","application/json; charset=utf-8")})}function n(t,e=200){return new Response(t.endsWith("\n")?t:`${t}\n`,{...d(e),headers:m(d(e).headers,"content-type","text/plain; charset=utf-8")})}function r(t,e=200){const n=d(e);return new Response(t,{...n,headers:m(n.headers,"content-type","text/html; charset=utf-8")})}function o(t,e=302){return new Response(null,{status:e,headers:{location:t,"cache-control":"no-store"}})}function s(t,n,r=400,o={}){return e({ok:!1,error:{code:t,message:n},...o},r)}function a(e){return e instanceof t?s(e.code,e.expose?e.message:"Internal error.",e.status):s("internal_error","Internal error.",500)}async function c(t){if(!t.body)return{};try{const e=await t.json();return!e||"object"!=typeof e||Array.isArray(e)?{}:e}catch{return{}}}async function u(e){const n=await e.json().catch(()=>null);if(!n||"object"!=typeof n||Array.isArray(n))throw new t(400,"invalid_json","Expected a JSON object.");return n}function i(t,e){const n=t.searchParams.get("json");return""===n||"1"===n||"true"===n||!0===e?.json}function f(t){if("string"!=typeof t)return;const e=t.trim();return e.length>0?e:void 0}function h(t){if(Array.isArray(t))return t.filter(t=>"string"==typeof t).map(t=>t.trim()).filter(Boolean);const e=f(t);return e?[e]:[]}function p(t){return!0===t||"true"===t||"1"===t||""===t}function l(t,e=20,n=100){const r="number"==typeof t?t:Number(t);return Number.isFinite(r)?Math.max(1,Math.min(n,Math.floor(r))):e}function y(t,e){return t.searchParams.getAll(e).map(t=>t.trim()).filter(Boolean)}function d(t){return"number"==typeof t?{status:t}:t}function m(t,e,n){const r=new Headers(t);return r.has(e)||r.set(e,n),r}export{t as HttpError,p as booleanParam,s as errorJson,a as errorResponse,y as getRepeated,r as htmlResponse,i as isJsonRequested,e as jsonResponse,l as limitParam,c as parseJsonBody,u as readJsonObject,o as redirectResponse,h as stringArrayParam,f as stringParam,n as textResponse};
1
+ class t extends Error{status;code;expose;constructor(t,e,n,r){super(n),this.name="HttpError",this.status=t,this.code=e,this.expose=r?.expose??t<500}}function e(t,e=200){const n=m(e);return new Response(JSON.stringify(t,null,2),{...n,headers:x(n.headers,"content-type","application/json; charset=utf-8")})}function n(t,n=200){return e({ok:!0,data:t},n)}function r(t,e=200){return new Response(t.endsWith("\n")?t:`${t}\n`,{...m(e),headers:x(m(e).headers,"content-type","text/plain; charset=utf-8")})}function o(t,e=200){const n=m(e);return new Response(t,{...n,headers:x(n.headers,"content-type","text/html; charset=utf-8")})}function s(t,e=302){return new Response(null,{status:e,headers:{location:t,"cache-control":"no-store"}})}function a(t,n,r=400,o={}){return e({ok:!1,error:{code:t,message:n},...o},r)}function c(e){return e instanceof t?a(e.code,e.expose?e.message:"Internal error.",e.status):a("internal_error","Internal error.",500)}async function u(t){if(!t.body)return{};try{const e=await t.json();return!e||"object"!=typeof e||Array.isArray(e)?{}:e}catch{return{}}}async function i(e){const n=await e.json().catch(()=>null);if(!n||"object"!=typeof n||Array.isArray(n))throw new t(400,"invalid_json","Expected a JSON object.");return n}function f(t,e){const n=t.searchParams.get("json");return""===n||"1"===n||"true"===n||!0===e?.json}function h(t){if("string"!=typeof t)return;const e=t.trim();return e.length>0?e:void 0}function p(t){if(Array.isArray(t))return t.filter(t=>"string"==typeof t).map(t=>t.trim()).filter(Boolean);const e=h(t);return e?[e]:[]}function l(t){return!0===t||"true"===t||"1"===t||""===t}function y(t,e=20,n=100){const r="number"==typeof t?t:Number(t);return Number.isFinite(r)?Math.max(1,Math.min(n,Math.floor(r))):e}function d(t,e){return t.searchParams.getAll(e).map(t=>t.trim()).filter(Boolean)}function m(t){return"number"==typeof t?{status:t}:t}function x(t,e,n){const r=new Headers(t);return r.has(e)||r.set(e,n),r}export{t as HttpError,l as booleanParam,a as errorJson,c as errorResponse,d as getRepeated,o as htmlResponse,f as isJsonRequested,e as jsonResponse,y as limitParam,n as okJson,u as parseJsonBody,i as readJsonObject,s as redirectResponse,p as stringArrayParam,h as stringParam,r as textResponse};
package/package.json CHANGED
@@ -1,14 +1,29 @@
1
1
  {
2
2
  "name": "capability-worker",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Shared toolkit for Cloudflare Workers that expose CLI-like capabilities over HTTP.",
5
5
  "type": "module",
6
+ "packageManager": "pnpm@11.9.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Iliasnolsson/capability-worker.git"
10
+ },
6
11
  "sideEffects": false,
7
12
  "files": [
8
13
  "dist",
14
+ "templates",
9
15
  "README.md",
10
16
  "LICENSE"
11
17
  ],
18
+ "scripts": {
19
+ "clean": "rm -rf dist",
20
+ "build": "pnpm clean && rollup -c",
21
+ "typecheck": "tsc --noEmit",
22
+ "test": "vitest run",
23
+ "prepack": "pnpm build",
24
+ "pack:check": "pnpm build && pnpm pack --dry-run",
25
+ "publish:npm": "pnpm typecheck && pnpm test && pnpm pack:check && pnpm publish --access public"
26
+ },
12
27
  "bin": {
13
28
  "capability-build-assets": "dist/node/build-capability-assets-cli.js",
14
29
  "capability-validate": "dist/node/validate-cli.js"
@@ -65,13 +80,5 @@
65
80
  "rollup-plugin-esbuild": "^6.0.0",
66
81
  "typescript": "^5.9.0",
67
82
  "vitest": "^4.1.0"
68
- },
69
- "scripts": {
70
- "clean": "rm -rf dist",
71
- "build": "pnpm clean && rollup -c",
72
- "typecheck": "tsc --noEmit",
73
- "test": "vitest run",
74
- "pack:check": "pnpm build && pnpm pack --dry-run",
75
- "publish:npm": "pnpm typecheck && pnpm test && pnpm pack:check && pnpm publish --access public"
76
83
  }
77
- }
84
+ }
@@ -0,0 +1,15 @@
1
+ # New Capability Worker
2
+
3
+ Small starter for a Cloudflare Worker capability that uses the shared `capability-worker` package.
4
+
5
+ ## Setup
6
+
7
+ ```sh
8
+ pnpm install
9
+ CAPABILITY_BASE_URL=https://starter.example.com/api pnpm -C worker run build:capability-assets
10
+ pnpm -C worker run typecheck
11
+ pnpm -C worker run dev
12
+ ```
13
+
14
+ Replace `starter` names, command definitions, mapping routes, and domain behavior before deploying.
15
+
@@ -0,0 +1,48 @@
1
+ {
2
+ "schemaVersion": "cli.definition.v1",
3
+ "cli": {
4
+ "name": "starter",
5
+ "summary": "Run starter capability commands.",
6
+ "description": "A minimal capability definition to replace with product-specific commands."
7
+ },
8
+ "commandGroups": [
9
+ {
10
+ "id": "starter",
11
+ "title": "Starter",
12
+ "description": "Starter commands."
13
+ }
14
+ ],
15
+ "commands": [
16
+ {
17
+ "id": "hello",
18
+ "group": "starter",
19
+ "command": "starter hello",
20
+ "summary": "Return a signed hello response.",
21
+ "description": "Verifies the capability signature and returns a small account-scoped response.",
22
+ "usage": "starter hello [--json]",
23
+ "risk": "read",
24
+ "requiresConfirmation": false,
25
+ "arguments": [],
26
+ "options": [
27
+ {
28
+ "name": "--json",
29
+ "key": "json",
30
+ "type": "boolean",
31
+ "description": "Return structured JSON instead of text."
32
+ }
33
+ ],
34
+ "examples": [
35
+ {
36
+ "description": "Return a text response.",
37
+ "command": "starter hello"
38
+ },
39
+ {
40
+ "description": "Return a JSON response.",
41
+ "command": "starter hello --json"
42
+ }
43
+ ],
44
+ "output": "Without --json, prints a short text response. With --json, returns a standard { ok, data } JSON response."
45
+ }
46
+ ]
47
+ }
48
+
@@ -0,0 +1,8 @@
1
+ {
2
+ "hello": {
3
+ "method": "GET",
4
+ "path": "/hello",
5
+ "query": ["json"]
6
+ }
7
+ }
8
+
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "new-capability-worker",
3
+ "private": true,
4
+ "type": "module",
5
+ "packageManager": "pnpm@11.9.0",
6
+ "scripts": {
7
+ "build:capability-assets": "pnpm -C worker run build:capability-assets",
8
+ "typecheck": "pnpm -C worker run typecheck",
9
+ "dev": "pnpm -C worker run dev",
10
+ "deploy": "pnpm -C worker run deploy"
11
+ }
12
+ }
13
+
@@ -0,0 +1,6 @@
1
+ packages:
2
+ - worker
3
+
4
+ minimumReleaseAgeExclude:
5
+ - capability-worker@0.1.1
6
+
@@ -0,0 +1,9 @@
1
+ # Starter
2
+
3
+ Use the `starter` command to verify that the capability is installed and signed requests are working.
4
+
5
+ ## Commands
6
+
7
+ - `starter hello`: returns a short text response.
8
+ - `starter hello --json`: returns the same response as structured JSON.
9
+
@@ -0,0 +1,36 @@
1
+ {
2
+ "schemaVersion": "capability.info.v1",
3
+ "id": "starter",
4
+ "name": "starter",
5
+ "displayName": "Starter",
6
+ "summary": "Run starter capability commands.",
7
+ "description": "A minimal capability definition to replace with product-specific commands.",
8
+ "homepage": "${CAPABILITY_BASE_URL}",
9
+ "apiBaseUrl": "${CAPABILITY_BASE_URL}",
10
+ "endpoints": {
11
+ "definition": "definition",
12
+ "mapping": "mapping"
13
+ },
14
+ "skill": {
15
+ "name": "starter",
16
+ "entrypoint": {
17
+ "path": "SKILL.md",
18
+ "url": "skill/SKILL.md",
19
+ "contentType": "text/markdown"
20
+ },
21
+ "resources": []
22
+ },
23
+ "auth": {
24
+ "scheme": "capability-signature-v1",
25
+ "accountHeader": "x-capability-account-id",
26
+ "timestampHeader": "x-capability-timestamp",
27
+ "keyIdHeader": "x-capability-key-id",
28
+ "signatureHeader": "x-capability-signature"
29
+ },
30
+ "version": "${APP_ASSET_VERSION}",
31
+ "updatedAt": "2026-07-06T00:00:00Z",
32
+ "cache": {
33
+ "maxAgeSeconds": 86400
34
+ }
35
+ }
36
+
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "starter-capability-worker",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "build:capability-assets": "capability-build-assets --worker-root . --repo-root .. --validate",
7
+ "typecheck": "tsc --noEmit",
8
+ "dev": "wrangler dev",
9
+ "deploy": "pnpm run build:capability-assets && wrangler deploy"
10
+ },
11
+ "dependencies": {
12
+ "capability-worker": "^0.1.1"
13
+ },
14
+ "devDependencies": {
15
+ "@cloudflare/workers-types": "^4.20260627.0",
16
+ "typescript": "^5.9.0",
17
+ "wrangler": "^4.51.0"
18
+ }
19
+ }
20
+
@@ -0,0 +1,51 @@
1
+ import { isCapabilityAssetPath } from "capability-worker";
2
+ import { requireCapabilitySignature } from "capability-worker/auth";
3
+ import {
4
+ errorJson,
5
+ errorResponse,
6
+ isJsonRequested,
7
+ okJson,
8
+ textResponse
9
+ } from "capability-worker/http";
10
+
11
+ export interface Env {
12
+ ASSETS: Fetcher;
13
+ CAPABILITY_PUBLIC_KEYS_JSON?: string;
14
+ CAPABILITY_PUBLIC_KEYS_URL?: string;
15
+ CAPABILITY_PUBLIC_KEYS_SERVICE?: Fetcher;
16
+ }
17
+
18
+ export default {
19
+ async fetch(request: Request, env: Env): Promise<Response> {
20
+ try {
21
+ const url = new URL(request.url);
22
+
23
+ if (isCapabilityAssetPath(url.pathname, { basePath: "/api" })) {
24
+ return env.ASSETS.fetch(request);
25
+ }
26
+
27
+ if (url.pathname === "/api/health" && request.method === "GET") {
28
+ return okJson({ status: "ok" });
29
+ }
30
+
31
+ if (url.pathname === "/api/hello" && request.method === "GET") {
32
+ const { accountId } = await requireCapabilitySignature(request, env);
33
+ const data = {
34
+ accountId,
35
+ message: "Hello from the starter capability."
36
+ };
37
+
38
+ if (isJsonRequested(url)) {
39
+ return okJson(data);
40
+ }
41
+
42
+ return textResponse(`Hello ${accountId}.`);
43
+ }
44
+
45
+ return errorJson("not_found", "Not found.", 404);
46
+ } catch (error) {
47
+ return errorResponse(error);
48
+ }
49
+ }
50
+ };
51
+
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "types": ["@cloudflare/workers-types"]
9
+ },
10
+ "include": ["src/**/*.ts"]
11
+ }
12
+
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "starter-capability-worker",
3
+ "main": "src/worker.ts",
4
+ "compatibility_date": "2026-07-06",
5
+ "assets": {
6
+ "directory": "./public",
7
+ "binding": "ASSETS",
8
+ "html_handling": "none",
9
+ "not_found_handling": "none"
10
+ },
11
+ "vars": {
12
+ "CAPABILITY_BASE_URL": "https://starter.example.com/api",
13
+ "APP_ASSET_VERSION": "0.1.1",
14
+ "CAPABILITY_PUBLIC_KEYS_URL": "https://example.com/capability/public-keys"
15
+ }
16
+ }
17
+