sello 0.1.7 → 0.1.9

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,13 @@ 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
+ npx --yes sello call-http-demo
50
+ ```
51
+
45
52
  ## Why Sello?
46
53
 
47
54
  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 +86,7 @@ The implementation includes a local end-to-end demo, compact JWS token verificat
79
86
  |------|------|
80
87
  | Add Sello in a few lines | [SDK Quickstart](docs/sdk-quickstart.md) |
81
88
  | Emit your first receipt | `npx --yes sello dev`, then `npx --yes sello emit-demo` |
89
+ | Wrap one HTTP route | `npx --yes sello init-http-demo`, then `npx --yes sello call-http-demo` |
82
90
  | Try a wrapped tool locally | `node --run dev`, then `node --run example:tool` |
83
91
  | Try an MCP-style tool call | `node --run dev`, then `node --run example:mcp` |
84
92
  | See a minimal MCP integration | [examples/mcp-minimal-server.ts](examples/mcp-minimal-server.ts) |
@@ -135,6 +143,8 @@ For an MCP-shaped `tools/call` boundary, run `node --run example:mcp` instead of
135
143
 
136
144
  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
145
 
146
+ 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. With the local log and route running, `npx sello call-http-demo` sends the demo request for you.
147
+
138
148
  ## The First 10 Minutes
139
149
 
140
150
  If you are implementing Sello, start with one local loop:
package/dist/cli/sello.js CHANGED
@@ -81,9 +81,15 @@ try {
81
81
  case "emit-demo":
82
82
  await emitDemoCommand(process.argv.slice(3));
83
83
  break;
84
+ case "call-http-demo":
85
+ await callHttpDemoCommand(process.argv.slice(3));
86
+ break;
84
87
  case "init-demo":
85
88
  initDemoCommand(process.argv.slice(3));
86
89
  break;
90
+ case "init-http-demo":
91
+ initHttpDemoCommand(process.argv.slice(3));
92
+ break;
87
93
  case "keys":
88
94
  keysCommand(process.argv.slice(3));
89
95
  break;
@@ -234,6 +240,38 @@ async function emitDemoCommand(args ) {
234
240
  console.log(` ${actionViewerUrl(state)}`);
235
241
  }
236
242
 
243
+ async function callHttpDemoCommand(args ) {
244
+ const state = loadDevStateOrThrow(
245
+ "missing local Sello dev state. Run `sello dev` first, then run `sello call-http-demo` in another terminal from the same directory.",
246
+ );
247
+ const url = readFlag(args, "--url") ?? "http://localhost:8790/calendar/events";
248
+ const title = readFlag(args, "--title") ?? "Ship Sello";
249
+ const response = await fetch(url, {
250
+ method: "POST",
251
+ headers: {
252
+ authorization: `Bearer ${state.agentToken}`,
253
+ "content-type": "application/json",
254
+ },
255
+ body: JSON.stringify({ title }),
256
+ });
257
+ const responseText = await response.text();
258
+
259
+ if (!response.ok) {
260
+ throw new TypeError(
261
+ `HTTP route demo failed with HTTP ${response.status}: ${responseText.trim()}`,
262
+ );
263
+ }
264
+
265
+ console.log("Called Sello HTTP route demo.");
266
+ console.log(formatHttpDemoResponse(responseText));
267
+ console.log("");
268
+ console.log("View verified actions with:");
269
+ console.log(" sello actions");
270
+ console.log("");
271
+ console.log("Or open:");
272
+ console.log(` ${actionViewerUrl(state)}`);
273
+ }
274
+
237
275
  function initDemoCommand(args ) {
238
276
  const output = readFlag(args, "--output") ?? "emit-receipt.mjs";
239
277
  const force = args.includes("--force");
@@ -256,6 +294,31 @@ function initDemoCommand(args ) {
256
294
  console.log("Then open http://localhost:8787/actions");
257
295
  }
258
296
 
297
+ function initHttpDemoCommand(args ) {
298
+ const output = readFlag(args, "--output") ?? "sello-http-route.mjs";
299
+ const force = args.includes("--force");
300
+
301
+ if (existsSync(output) && !force) {
302
+ throw new TypeError(`${output} already exists. Pass --force to overwrite it.`);
303
+ }
304
+
305
+ writeFileSync(output, httpRouteDemoSource(), { mode: 0o644 });
306
+
307
+ console.log(`Created ${output}`);
308
+ console.log("");
309
+ console.log("Terminal 1: keep the local dev log running");
310
+ console.log(" npx sello dev");
311
+ console.log("");
312
+ console.log("Terminal 2: start the route");
313
+ console.log(` node ${output}`);
314
+ console.log("");
315
+ console.log("Terminal 3: call the route and view the receipt");
316
+ console.log(" npx sello call-http-demo");
317
+ console.log(" npx sello actions");
318
+ console.log("");
319
+ console.log("Then open http://localhost:8787/actions");
320
+ }
321
+
259
322
  function keysCommand(args ) {
260
323
  const subcommand = args[0] ?? "service";
261
324
  if (subcommand !== "service") {
@@ -649,7 +712,9 @@ function printHelp() {
649
712
  console.log(`Usage:
650
713
  sello dev [--port 8787] [--service service-id] [--dry-run]
651
714
  sello emit-demo [--title title]
715
+ sello call-http-demo [--url http://localhost:8790/calendar/events] [--title title]
652
716
  sello init-demo [--output emit-receipt.mjs] [--force]
717
+ sello init-http-demo [--output sello-http-route.mjs] [--force]
653
718
  sello actions [--token agent-token]
654
719
  sello keys service
655
720
  sello inspect-env
@@ -708,6 +773,14 @@ function actionViewerUrl(state ) {
708
773
  return `${endpoint.origin}/actions`;
709
774
  }
710
775
 
776
+ function formatHttpDemoResponse(responseText ) {
777
+ try {
778
+ return JSON.stringify(JSON.parse(responseText), null, 2);
779
+ } catch {
780
+ return responseText;
781
+ }
782
+ }
783
+
711
784
  function slug(value ) {
712
785
  return value
713
786
  .toLowerCase()
@@ -762,3 +835,112 @@ await receipts.flush();
762
835
  console.log(result);
763
836
  `;
764
837
  }
838
+
839
+ function httpRouteDemoSource() {
840
+ return `import { createServer } from "node:http";
841
+ import { readFileSync } from "node:fs";
842
+ import { canonicalJsonBytes, sello } from "sello";
843
+
844
+ const state = JSON.parse(readFileSync(".sello/dev.json", "utf8"));
845
+ const port = Number(process.env.PORT ?? "8790");
846
+
847
+ const receipts = sello.service({
848
+ service: state.serviceId,
849
+ serviceKey: state.serviceKey,
850
+ tokenIssuer: state.tokenIssuerPublicKey,
851
+ log: sello.logs.http(state.logUrl, { endpoint: state.logEndpoint }),
852
+ submit: { mode: "await" },
853
+ });
854
+
855
+ const createEvent = receipts.tool(
856
+ "http.POST /calendar/events",
857
+ async (request) => {
858
+ const title = readString(request.body.title, "title");
859
+ return {
860
+ id: "evt_" + slug(title),
861
+ title,
862
+ status: "created",
863
+ createdAt: new Date().toISOString(),
864
+ };
865
+ },
866
+ {
867
+ authorizationToken: (request) => request.authorizationToken,
868
+ canonicalizeInput: (request) =>
869
+ canonicalJsonBytes({
870
+ method: "POST",
871
+ path: "/calendar/events",
872
+ body: request.body,
873
+ }),
874
+ canonicalizeOutput: (response) => canonicalJsonBytes(response),
875
+ },
876
+ );
877
+
878
+ const server = createServer(async (request, response) => {
879
+ try {
880
+ const path = new URL(request.url ?? "/", "http://localhost").pathname;
881
+ if (request.method !== "POST" || path !== "/calendar/events") {
882
+ sendJson(response, 404, { error: "not found" });
883
+ return;
884
+ }
885
+
886
+ const body = await readJson(request);
887
+ const result = await createEvent({
888
+ authorizationToken: bearerToken(request.headers.authorization),
889
+ body,
890
+ });
891
+ await receipts.flush();
892
+ sendJson(response, 200, result);
893
+ } catch (error) {
894
+ sendJson(response, 400, {
895
+ error: error instanceof Error ? error.message : String(error),
896
+ });
897
+ }
898
+ });
899
+
900
+ server.listen(port, () => {
901
+ console.log("Sello HTTP route demo running at http://localhost:" + port + "/calendar/events");
902
+ console.log("");
903
+ console.log("Call it from another terminal with:");
904
+ console.log(" npx sello call-http-demo");
905
+ });
906
+
907
+ async function readJson(request) {
908
+ const chunks = [];
909
+ for await (const chunk of request) {
910
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
911
+ }
912
+ if (chunks.length === 0) {
913
+ return {};
914
+ }
915
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
916
+ }
917
+
918
+ function bearerToken(header) {
919
+ const value = Array.isArray(header) ? header[0] : header;
920
+ if (typeof value !== "string" || !value.startsWith("Bearer ")) {
921
+ throw new TypeError("missing Authorization: Bearer <token> header");
922
+ }
923
+ return value.slice("Bearer ".length);
924
+ }
925
+
926
+ function sendJson(response, statusCode, body) {
927
+ response.writeHead(statusCode, { "content-type": "application/json" });
928
+ response.end(JSON.stringify(body, null, 2) + "\\n");
929
+ }
930
+
931
+ function readString(value, name) {
932
+ if (typeof value !== "string" || value.length === 0) {
933
+ throw new TypeError(name + " must be a non-empty string");
934
+ }
935
+ return value;
936
+ }
937
+
938
+ function slug(value) {
939
+ return value
940
+ .toLowerCase()
941
+ .replace(/[^a-z0-9]+/g, "_")
942
+ .replace(/^_+|_+$/g, "")
943
+ .slice(0, 40);
944
+ }
945
+ `;
946
+ }
@@ -35,6 +35,19 @@ 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. With `npx sello dev` and the generated route running, call it with:
45
+
46
+ ```bash
47
+ npx --yes sello call-http-demo
48
+ npx --yes sello actions
49
+ ```
50
+
38
51
  Inside this repo, start the local log and action viewer:
39
52
 
40
53
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sello",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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
@@ -82,9 +82,15 @@ try {
82
82
  case "emit-demo":
83
83
  await emitDemoCommand(process.argv.slice(3));
84
84
  break;
85
+ case "call-http-demo":
86
+ await callHttpDemoCommand(process.argv.slice(3));
87
+ break;
85
88
  case "init-demo":
86
89
  initDemoCommand(process.argv.slice(3));
87
90
  break;
91
+ case "init-http-demo":
92
+ initHttpDemoCommand(process.argv.slice(3));
93
+ break;
88
94
  case "keys":
89
95
  keysCommand(process.argv.slice(3));
90
96
  break;
@@ -235,6 +241,38 @@ async function emitDemoCommand(args: string[]): Promise<void> {
235
241
  console.log(` ${actionViewerUrl(state)}`);
236
242
  }
237
243
 
244
+ async function callHttpDemoCommand(args: string[]): Promise<void> {
245
+ const state = loadDevStateOrThrow(
246
+ "missing local Sello dev state. Run `sello dev` first, then run `sello call-http-demo` in another terminal from the same directory.",
247
+ );
248
+ const url = readFlag(args, "--url") ?? "http://localhost:8790/calendar/events";
249
+ const title = readFlag(args, "--title") ?? "Ship Sello";
250
+ const response = await fetch(url, {
251
+ method: "POST",
252
+ headers: {
253
+ authorization: `Bearer ${state.agentToken}`,
254
+ "content-type": "application/json",
255
+ },
256
+ body: JSON.stringify({ title }),
257
+ });
258
+ const responseText = await response.text();
259
+
260
+ if (!response.ok) {
261
+ throw new TypeError(
262
+ `HTTP route demo failed with HTTP ${response.status}: ${responseText.trim()}`,
263
+ );
264
+ }
265
+
266
+ console.log("Called Sello HTTP route demo.");
267
+ console.log(formatHttpDemoResponse(responseText));
268
+ console.log("");
269
+ console.log("View verified actions with:");
270
+ console.log(" sello actions");
271
+ console.log("");
272
+ console.log("Or open:");
273
+ console.log(` ${actionViewerUrl(state)}`);
274
+ }
275
+
238
276
  function initDemoCommand(args: string[]): void {
239
277
  const output = readFlag(args, "--output") ?? "emit-receipt.mjs";
240
278
  const force = args.includes("--force");
@@ -257,6 +295,31 @@ function initDemoCommand(args: string[]): void {
257
295
  console.log("Then open http://localhost:8787/actions");
258
296
  }
259
297
 
298
+ function initHttpDemoCommand(args: string[]): void {
299
+ const output = readFlag(args, "--output") ?? "sello-http-route.mjs";
300
+ const force = args.includes("--force");
301
+
302
+ if (existsSync(output) && !force) {
303
+ throw new TypeError(`${output} already exists. Pass --force to overwrite it.`);
304
+ }
305
+
306
+ writeFileSync(output, httpRouteDemoSource(), { mode: 0o644 });
307
+
308
+ console.log(`Created ${output}`);
309
+ console.log("");
310
+ console.log("Terminal 1: keep the local dev log running");
311
+ console.log(" npx sello dev");
312
+ console.log("");
313
+ console.log("Terminal 2: start the route");
314
+ console.log(` node ${output}`);
315
+ console.log("");
316
+ console.log("Terminal 3: call the route and view the receipt");
317
+ console.log(" npx sello call-http-demo");
318
+ console.log(" npx sello actions");
319
+ console.log("");
320
+ console.log("Then open http://localhost:8787/actions");
321
+ }
322
+
260
323
  function keysCommand(args: string[]): void {
261
324
  const subcommand = args[0] ?? "service";
262
325
  if (subcommand !== "service") {
@@ -650,7 +713,9 @@ function printHelp(): void {
650
713
  console.log(`Usage:
651
714
  sello dev [--port 8787] [--service service-id] [--dry-run]
652
715
  sello emit-demo [--title title]
716
+ sello call-http-demo [--url http://localhost:8790/calendar/events] [--title title]
653
717
  sello init-demo [--output emit-receipt.mjs] [--force]
718
+ sello init-http-demo [--output sello-http-route.mjs] [--force]
654
719
  sello actions [--token agent-token]
655
720
  sello keys service
656
721
  sello inspect-env
@@ -709,6 +774,14 @@ function actionViewerUrl(state: DevState): string {
709
774
  return `${endpoint.origin}/actions`;
710
775
  }
711
776
 
777
+ function formatHttpDemoResponse(responseText: string): string {
778
+ try {
779
+ return JSON.stringify(JSON.parse(responseText), null, 2);
780
+ } catch {
781
+ return responseText;
782
+ }
783
+ }
784
+
712
785
  function slug(value: string): string {
713
786
  return value
714
787
  .toLowerCase()
@@ -763,3 +836,112 @@ await receipts.flush();
763
836
  console.log(result);
764
837
  `;
765
838
  }
839
+
840
+ function httpRouteDemoSource(): string {
841
+ return `import { createServer } from "node:http";
842
+ import { readFileSync } from "node:fs";
843
+ import { canonicalJsonBytes, sello } from "sello";
844
+
845
+ const state = JSON.parse(readFileSync(".sello/dev.json", "utf8"));
846
+ const port = Number(process.env.PORT ?? "8790");
847
+
848
+ const receipts = sello.service({
849
+ service: state.serviceId,
850
+ serviceKey: state.serviceKey,
851
+ tokenIssuer: state.tokenIssuerPublicKey,
852
+ log: sello.logs.http(state.logUrl, { endpoint: state.logEndpoint }),
853
+ submit: { mode: "await" },
854
+ });
855
+
856
+ const createEvent = receipts.tool(
857
+ "http.POST /calendar/events",
858
+ async (request) => {
859
+ const title = readString(request.body.title, "title");
860
+ return {
861
+ id: "evt_" + slug(title),
862
+ title,
863
+ status: "created",
864
+ createdAt: new Date().toISOString(),
865
+ };
866
+ },
867
+ {
868
+ authorizationToken: (request) => request.authorizationToken,
869
+ canonicalizeInput: (request) =>
870
+ canonicalJsonBytes({
871
+ method: "POST",
872
+ path: "/calendar/events",
873
+ body: request.body,
874
+ }),
875
+ canonicalizeOutput: (response) => canonicalJsonBytes(response),
876
+ },
877
+ );
878
+
879
+ const server = createServer(async (request, response) => {
880
+ try {
881
+ const path = new URL(request.url ?? "/", "http://localhost").pathname;
882
+ if (request.method !== "POST" || path !== "/calendar/events") {
883
+ sendJson(response, 404, { error: "not found" });
884
+ return;
885
+ }
886
+
887
+ const body = await readJson(request);
888
+ const result = await createEvent({
889
+ authorizationToken: bearerToken(request.headers.authorization),
890
+ body,
891
+ });
892
+ await receipts.flush();
893
+ sendJson(response, 200, result);
894
+ } catch (error) {
895
+ sendJson(response, 400, {
896
+ error: error instanceof Error ? error.message : String(error),
897
+ });
898
+ }
899
+ });
900
+
901
+ server.listen(port, () => {
902
+ console.log("Sello HTTP route demo running at http://localhost:" + port + "/calendar/events");
903
+ console.log("");
904
+ console.log("Call it from another terminal with:");
905
+ console.log(" npx sello call-http-demo");
906
+ });
907
+
908
+ async function readJson(request) {
909
+ const chunks = [];
910
+ for await (const chunk of request) {
911
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
912
+ }
913
+ if (chunks.length === 0) {
914
+ return {};
915
+ }
916
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
917
+ }
918
+
919
+ function bearerToken(header) {
920
+ const value = Array.isArray(header) ? header[0] : header;
921
+ if (typeof value !== "string" || !value.startsWith("Bearer ")) {
922
+ throw new TypeError("missing Authorization: Bearer <token> header");
923
+ }
924
+ return value.slice("Bearer ".length);
925
+ }
926
+
927
+ function sendJson(response, statusCode, body) {
928
+ response.writeHead(statusCode, { "content-type": "application/json" });
929
+ response.end(JSON.stringify(body, null, 2) + "\\n");
930
+ }
931
+
932
+ function readString(value, name) {
933
+ if (typeof value !== "string" || value.length === 0) {
934
+ throw new TypeError(name + " must be a non-empty string");
935
+ }
936
+ return value;
937
+ }
938
+
939
+ function slug(value) {
940
+ return value
941
+ .toLowerCase()
942
+ .replace(/[^a-z0-9]+/g, "_")
943
+ .replace(/^_+|_+$/g, "")
944
+ .slice(0, 40);
945
+ }
946
+ `;
947
+ }