sello 0.1.7 → 0.1.8

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
@@ -42,6 +42,12 @@ To see the tiny wrapped-tool source:
42
42
  npx --yes sello init-demo
43
43
  ```
44
44
 
45
+ To scaffold a small HTTP route that emits receipts:
46
+
47
+ ```bash
48
+ npx --yes sello init-http-demo
49
+ ```
50
+
45
51
  ## Why Sello?
46
52
 
47
53
  Most agent logs are written by the same system whose behavior they describe. If the agent, runtime, or operator is compromised, those logs can be incomplete or false.
@@ -79,6 +85,7 @@ The implementation includes a local end-to-end demo, compact JWS token verificat
79
85
  |------|------|
80
86
  | Add Sello in a few lines | [SDK Quickstart](docs/sdk-quickstart.md) |
81
87
  | Emit your first receipt | `npx --yes sello dev`, then `npx --yes sello emit-demo` |
88
+ | Wrap one HTTP route | `npx --yes sello init-http-demo` |
82
89
  | Try a wrapped tool locally | `node --run dev`, then `node --run example:tool` |
83
90
  | Try an MCP-style tool call | `node --run dev`, then `node --run example:mcp` |
84
91
  | See a minimal MCP integration | [examples/mcp-minimal-server.ts](examples/mcp-minimal-server.ts) |
@@ -135,6 +142,8 @@ For an MCP-shaped `tools/call` boundary, run `node --run example:mcp` instead of
135
142
 
136
143
  For a smaller production-shaped MCP example, see [examples/mcp-minimal-server.ts](examples/mcp-minimal-server.ts). It wraps one `tools/call` handler with `sello.service()` and leaves unknown tools unreceipted.
137
144
 
145
+ For an installed-project bridge from demo to app, run `npx sello init-http-demo`. It writes a small dependency-free HTTP route that imports `sello`, reads the local dev config, verifies a bearer token, runs a handler, and emits a receipt.
146
+
138
147
  ## The First 10 Minutes
139
148
 
140
149
  If you are implementing Sello, start with one local loop:
package/dist/cli/sello.js CHANGED
@@ -84,6 +84,9 @@ try {
84
84
  case "init-demo":
85
85
  initDemoCommand(process.argv.slice(3));
86
86
  break;
87
+ case "init-http-demo":
88
+ initHttpDemoCommand(process.argv.slice(3));
89
+ break;
87
90
  case "keys":
88
91
  keysCommand(process.argv.slice(3));
89
92
  break;
@@ -256,6 +259,32 @@ function initDemoCommand(args ) {
256
259
  console.log("Then open http://localhost:8787/actions");
257
260
  }
258
261
 
262
+ function initHttpDemoCommand(args ) {
263
+ const output = readFlag(args, "--output") ?? "sello-http-route.mjs";
264
+ const force = args.includes("--force");
265
+
266
+ if (existsSync(output) && !force) {
267
+ throw new TypeError(`${output} already exists. Pass --force to overwrite it.`);
268
+ }
269
+
270
+ writeFileSync(output, httpRouteDemoSource(), { mode: 0o644 });
271
+
272
+ console.log(`Created ${output}`);
273
+ console.log("");
274
+ console.log("Terminal 1: keep the local dev log running");
275
+ console.log(" npx sello dev");
276
+ console.log("");
277
+ console.log("Terminal 2: start the route");
278
+ console.log(` node ${output}`);
279
+ console.log("");
280
+ console.log("Terminal 3: call the route and view the receipt");
281
+ console.log(" TOKEN=$(node -e 'console.log(JSON.parse(require(\"fs\").readFileSync(\".sello/dev.json\", \"utf8\")).agentToken)')");
282
+ console.log(" curl -sS -X POST http://localhost:8790/calendar/events -H \"authorization: Bearer $TOKEN\" -H \"content-type: application/json\" -d '{\"title\":\"Ship Sello\"}'");
283
+ console.log(" npx sello actions");
284
+ console.log("");
285
+ console.log("Then open http://localhost:8787/actions");
286
+ }
287
+
259
288
  function keysCommand(args ) {
260
289
  const subcommand = args[0] ?? "service";
261
290
  if (subcommand !== "service") {
@@ -650,6 +679,7 @@ function printHelp() {
650
679
  sello dev [--port 8787] [--service service-id] [--dry-run]
651
680
  sello emit-demo [--title title]
652
681
  sello init-demo [--output emit-receipt.mjs] [--force]
682
+ sello init-http-demo [--output sello-http-route.mjs] [--force]
653
683
  sello actions [--token agent-token]
654
684
  sello keys service
655
685
  sello inspect-env
@@ -762,3 +792,113 @@ await receipts.flush();
762
792
  console.log(result);
763
793
  `;
764
794
  }
795
+
796
+ function httpRouteDemoSource() {
797
+ return `import { createServer } from "node:http";
798
+ import { readFileSync } from "node:fs";
799
+ import { canonicalJsonBytes, sello } from "sello";
800
+
801
+ const state = JSON.parse(readFileSync(".sello/dev.json", "utf8"));
802
+ const port = Number(process.env.PORT ?? "8790");
803
+
804
+ const receipts = sello.service({
805
+ service: state.serviceId,
806
+ serviceKey: state.serviceKey,
807
+ tokenIssuer: state.tokenIssuerPublicKey,
808
+ log: sello.logs.http(state.logUrl, { endpoint: state.logEndpoint }),
809
+ submit: { mode: "await" },
810
+ });
811
+
812
+ const createEvent = receipts.tool(
813
+ "http.POST /calendar/events",
814
+ async (request) => {
815
+ const title = readString(request.body.title, "title");
816
+ return {
817
+ id: "evt_" + slug(title),
818
+ title,
819
+ status: "created",
820
+ createdAt: new Date().toISOString(),
821
+ };
822
+ },
823
+ {
824
+ authorizationToken: (request) => request.authorizationToken,
825
+ canonicalizeInput: (request) =>
826
+ canonicalJsonBytes({
827
+ method: "POST",
828
+ path: "/calendar/events",
829
+ body: request.body,
830
+ }),
831
+ canonicalizeOutput: (response) => canonicalJsonBytes(response),
832
+ },
833
+ );
834
+
835
+ const server = createServer(async (request, response) => {
836
+ try {
837
+ const path = new URL(request.url ?? "/", "http://localhost").pathname;
838
+ if (request.method !== "POST" || path !== "/calendar/events") {
839
+ sendJson(response, 404, { error: "not found" });
840
+ return;
841
+ }
842
+
843
+ const body = await readJson(request);
844
+ const result = await createEvent({
845
+ authorizationToken: bearerToken(request.headers.authorization),
846
+ body,
847
+ });
848
+ await receipts.flush();
849
+ sendJson(response, 200, result);
850
+ } catch (error) {
851
+ sendJson(response, 400, {
852
+ error: error instanceof Error ? error.message : String(error),
853
+ });
854
+ }
855
+ });
856
+
857
+ server.listen(port, () => {
858
+ console.log("Sello HTTP route demo running at http://localhost:" + port + "/calendar/events");
859
+ console.log("");
860
+ console.log("Call it with:");
861
+ console.log(" TOKEN=$(node -e 'console.log(JSON.parse(require(\\\"fs\\\").readFileSync(\\\".sello/dev.json\\\", \\\"utf8\\\")).agentToken)')");
862
+ console.log(" curl -sS -X POST http://localhost:" + port + "/calendar/events -H \\\"authorization: Bearer $TOKEN\\\" -H \\\"content-type: application/json\\\" -d '{\\\"title\\\":\\\"Ship Sello\\\"}'");
863
+ });
864
+
865
+ async function readJson(request) {
866
+ const chunks = [];
867
+ for await (const chunk of request) {
868
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
869
+ }
870
+ if (chunks.length === 0) {
871
+ return {};
872
+ }
873
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
874
+ }
875
+
876
+ function bearerToken(header) {
877
+ const value = Array.isArray(header) ? header[0] : header;
878
+ if (typeof value !== "string" || !value.startsWith("Bearer ")) {
879
+ throw new TypeError("missing Authorization: Bearer <token> header");
880
+ }
881
+ return value.slice("Bearer ".length);
882
+ }
883
+
884
+ function sendJson(response, statusCode, body) {
885
+ response.writeHead(statusCode, { "content-type": "application/json" });
886
+ response.end(JSON.stringify(body, null, 2) + "\\n");
887
+ }
888
+
889
+ function readString(value, name) {
890
+ if (typeof value !== "string" || value.length === 0) {
891
+ throw new TypeError(name + " must be a non-empty string");
892
+ }
893
+ return value;
894
+ }
895
+
896
+ function slug(value) {
897
+ return value
898
+ .toLowerCase()
899
+ .replace(/[^a-z0-9]+/g, "_")
900
+ .replace(/^_+|_+$/g, "")
901
+ .slice(0, 40);
902
+ }
903
+ `;
904
+ }
@@ -35,6 +35,14 @@ To write the tiny emitter file into your project:
35
35
  npx --yes sello init-demo
36
36
  ```
37
37
 
38
+ To write a dependency-free HTTP route example into your project:
39
+
40
+ ```bash
41
+ npx --yes sello init-http-demo
42
+ ```
43
+
44
+ The route example imports `sello`, reads the local dev config, verifies a bearer token, runs one `POST /calendar/events` handler, and emits a receipt.
45
+
38
46
  Inside this repo, start the local log and action viewer:
39
47
 
40
48
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sello",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Reference implementation of the Sello protocol for service-signed AI agent receipts.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/src/cli/sello.ts CHANGED
@@ -85,6 +85,9 @@ try {
85
85
  case "init-demo":
86
86
  initDemoCommand(process.argv.slice(3));
87
87
  break;
88
+ case "init-http-demo":
89
+ initHttpDemoCommand(process.argv.slice(3));
90
+ break;
88
91
  case "keys":
89
92
  keysCommand(process.argv.slice(3));
90
93
  break;
@@ -257,6 +260,32 @@ function initDemoCommand(args: string[]): void {
257
260
  console.log("Then open http://localhost:8787/actions");
258
261
  }
259
262
 
263
+ function initHttpDemoCommand(args: string[]): void {
264
+ const output = readFlag(args, "--output") ?? "sello-http-route.mjs";
265
+ const force = args.includes("--force");
266
+
267
+ if (existsSync(output) && !force) {
268
+ throw new TypeError(`${output} already exists. Pass --force to overwrite it.`);
269
+ }
270
+
271
+ writeFileSync(output, httpRouteDemoSource(), { mode: 0o644 });
272
+
273
+ console.log(`Created ${output}`);
274
+ console.log("");
275
+ console.log("Terminal 1: keep the local dev log running");
276
+ console.log(" npx sello dev");
277
+ console.log("");
278
+ console.log("Terminal 2: start the route");
279
+ console.log(` node ${output}`);
280
+ console.log("");
281
+ console.log("Terminal 3: call the route and view the receipt");
282
+ console.log(" TOKEN=$(node -e 'console.log(JSON.parse(require(\"fs\").readFileSync(\".sello/dev.json\", \"utf8\")).agentToken)')");
283
+ console.log(" curl -sS -X POST http://localhost:8790/calendar/events -H \"authorization: Bearer $TOKEN\" -H \"content-type: application/json\" -d '{\"title\":\"Ship Sello\"}'");
284
+ console.log(" npx sello actions");
285
+ console.log("");
286
+ console.log("Then open http://localhost:8787/actions");
287
+ }
288
+
260
289
  function keysCommand(args: string[]): void {
261
290
  const subcommand = args[0] ?? "service";
262
291
  if (subcommand !== "service") {
@@ -651,6 +680,7 @@ function printHelp(): void {
651
680
  sello dev [--port 8787] [--service service-id] [--dry-run]
652
681
  sello emit-demo [--title title]
653
682
  sello init-demo [--output emit-receipt.mjs] [--force]
683
+ sello init-http-demo [--output sello-http-route.mjs] [--force]
654
684
  sello actions [--token agent-token]
655
685
  sello keys service
656
686
  sello inspect-env
@@ -763,3 +793,113 @@ await receipts.flush();
763
793
  console.log(result);
764
794
  `;
765
795
  }
796
+
797
+ function httpRouteDemoSource(): string {
798
+ return `import { createServer } from "node:http";
799
+ import { readFileSync } from "node:fs";
800
+ import { canonicalJsonBytes, sello } from "sello";
801
+
802
+ const state = JSON.parse(readFileSync(".sello/dev.json", "utf8"));
803
+ const port = Number(process.env.PORT ?? "8790");
804
+
805
+ const receipts = sello.service({
806
+ service: state.serviceId,
807
+ serviceKey: state.serviceKey,
808
+ tokenIssuer: state.tokenIssuerPublicKey,
809
+ log: sello.logs.http(state.logUrl, { endpoint: state.logEndpoint }),
810
+ submit: { mode: "await" },
811
+ });
812
+
813
+ const createEvent = receipts.tool(
814
+ "http.POST /calendar/events",
815
+ async (request) => {
816
+ const title = readString(request.body.title, "title");
817
+ return {
818
+ id: "evt_" + slug(title),
819
+ title,
820
+ status: "created",
821
+ createdAt: new Date().toISOString(),
822
+ };
823
+ },
824
+ {
825
+ authorizationToken: (request) => request.authorizationToken,
826
+ canonicalizeInput: (request) =>
827
+ canonicalJsonBytes({
828
+ method: "POST",
829
+ path: "/calendar/events",
830
+ body: request.body,
831
+ }),
832
+ canonicalizeOutput: (response) => canonicalJsonBytes(response),
833
+ },
834
+ );
835
+
836
+ const server = createServer(async (request, response) => {
837
+ try {
838
+ const path = new URL(request.url ?? "/", "http://localhost").pathname;
839
+ if (request.method !== "POST" || path !== "/calendar/events") {
840
+ sendJson(response, 404, { error: "not found" });
841
+ return;
842
+ }
843
+
844
+ const body = await readJson(request);
845
+ const result = await createEvent({
846
+ authorizationToken: bearerToken(request.headers.authorization),
847
+ body,
848
+ });
849
+ await receipts.flush();
850
+ sendJson(response, 200, result);
851
+ } catch (error) {
852
+ sendJson(response, 400, {
853
+ error: error instanceof Error ? error.message : String(error),
854
+ });
855
+ }
856
+ });
857
+
858
+ server.listen(port, () => {
859
+ console.log("Sello HTTP route demo running at http://localhost:" + port + "/calendar/events");
860
+ console.log("");
861
+ console.log("Call it with:");
862
+ console.log(" TOKEN=$(node -e 'console.log(JSON.parse(require(\\\"fs\\\").readFileSync(\\\".sello/dev.json\\\", \\\"utf8\\\")).agentToken)')");
863
+ console.log(" curl -sS -X POST http://localhost:" + port + "/calendar/events -H \\\"authorization: Bearer $TOKEN\\\" -H \\\"content-type: application/json\\\" -d '{\\\"title\\\":\\\"Ship Sello\\\"}'");
864
+ });
865
+
866
+ async function readJson(request) {
867
+ const chunks = [];
868
+ for await (const chunk of request) {
869
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
870
+ }
871
+ if (chunks.length === 0) {
872
+ return {};
873
+ }
874
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
875
+ }
876
+
877
+ function bearerToken(header) {
878
+ const value = Array.isArray(header) ? header[0] : header;
879
+ if (typeof value !== "string" || !value.startsWith("Bearer ")) {
880
+ throw new TypeError("missing Authorization: Bearer <token> header");
881
+ }
882
+ return value.slice("Bearer ".length);
883
+ }
884
+
885
+ function sendJson(response, statusCode, body) {
886
+ response.writeHead(statusCode, { "content-type": "application/json" });
887
+ response.end(JSON.stringify(body, null, 2) + "\\n");
888
+ }
889
+
890
+ function readString(value, name) {
891
+ if (typeof value !== "string" || value.length === 0) {
892
+ throw new TypeError(name + " must be a non-empty string");
893
+ }
894
+ return value;
895
+ }
896
+
897
+ function slug(value) {
898
+ return value
899
+ .toLowerCase()
900
+ .replace(/[^a-z0-9]+/g, "_")
901
+ .replace(/^_+|_+$/g, "")
902
+ .slice(0, 40);
903
+ }
904
+ `;
905
+ }