sello 0.1.2 → 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
@@ -53,6 +53,7 @@ Sello currently requires Node.js 22.7 or newer.
53
53
  | Goal | Read |
54
54
  |------|------|
55
55
  | Add Sello in a few lines | [SDK Quickstart](docs/sdk-quickstart.md) |
56
+ | Emit your first receipt | `npx sello dev`, then `npx sello emit-demo` |
56
57
  | Try a wrapped tool locally | `node --run dev`, then `node --run example:tool` |
57
58
  | Try an MCP-style tool call | `node --run dev`, then `node --run example:mcp` |
58
59
  | Understand the protocol | [SPEC.md](SPEC.md) Quick Start |
@@ -98,6 +99,23 @@ The example wraps a fake calendar tool, emits a service-signed encrypted receipt
98
99
 
99
100
  For an MCP-shaped `tools/call` boundary, run `node --run example:mcp` instead of `node --run example:tool`.
100
101
 
102
+ To try the published package from a fresh project:
103
+
104
+ ```bash
105
+ # Terminal 1
106
+ npx sello dev
107
+
108
+ # Terminal 2
109
+ npx sello emit-demo
110
+ npx sello actions
111
+ ```
112
+
113
+ To see the tiny emitter code:
114
+
115
+ ```bash
116
+ npx sello init-demo
117
+ ```
118
+
101
119
  ## The First 10 Minutes
102
120
 
103
121
  If you are implementing Sello, start with one local loop:
package/dist/cli/sello.js CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { createServer, } from "node:http";
4
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
 
7
7
  import { generateEd25519KeyPair } from "../cose/sign1.js";
8
8
  import { deriveTokenIdentifiers, sha256, toHex } from "../crypto/identifiers.js";
9
9
  import { generateHpkeKeyPair } from "../hpke/receipt.js";
10
10
  import { MockTransparencyLog } from "../log/mock-log.js";
11
+ import { canonicalJsonBytes } from "../mcp/middleware.js";
11
12
  import { verifyReceipts } from "../owner/verify.js";
12
13
  import {
13
14
  loadSignedRegistry,
@@ -25,10 +26,12 @@ import {
25
26
  } from "../sdk/keys.js";
26
27
  import {
27
28
  deserializeEntry,
29
+ http as httpLog,
28
30
  queryHttpLogByTokenRef,
29
31
  serializeEntry,
30
32
  toCanonicalLogUrl,
31
33
  } from "../sdk/logs.js";
34
+ import { createSelloService } from "../sdk/service.js";
32
35
 
33
36
 
34
37
 
@@ -56,6 +59,12 @@ try {
56
59
  case "dev":
57
60
  await devCommand(process.argv.slice(3));
58
61
  break;
62
+ case "emit-demo":
63
+ await emitDemoCommand(process.argv.slice(3));
64
+ break;
65
+ case "init-demo":
66
+ initDemoCommand(process.argv.slice(3));
67
+ break;
59
68
  case "keys":
60
69
  keysCommand(process.argv.slice(3));
61
70
  break;
@@ -156,6 +165,75 @@ async function devCommand(args ) {
156
165
  });
157
166
  }
158
167
 
168
+ async function emitDemoCommand(args ) {
169
+ const state = loadDevStateOrThrow(
170
+ "missing local Sello dev state. Run `sello dev` first, then run `sello emit-demo` in another terminal from the same directory.",
171
+ );
172
+ const title = readFlag(args, "--title") ?? "Test Sello receipt";
173
+ const receipts = createSelloService({
174
+ service: state.serviceId,
175
+ serviceKey: state.serviceKey,
176
+ tokenIssuer: state.tokenIssuerPublicKey,
177
+ log: httpLog(state.logUrl, { endpoint: state.logEndpoint }),
178
+ submit: { mode: "await" },
179
+ });
180
+ const createEvent = receipts.tool (
181
+ "calendar.create_event",
182
+ async (request) => ({
183
+ id: `evt_${slug(request.title)}`,
184
+ calendarId: request.calendarId,
185
+ title: request.title,
186
+ status: "created",
187
+ createdAt: new Date().toISOString(),
188
+ }),
189
+ {
190
+ canonicalizeInput: (request) => canonicalJsonBytes({
191
+ calendarId: request.calendarId,
192
+ title: request.title,
193
+ start: request.start,
194
+ attendees: request.attendees,
195
+ }),
196
+ },
197
+ );
198
+ const response = await createEvent({
199
+ authorizationToken: state.agentToken,
200
+ calendarId: "demo-calendar",
201
+ title,
202
+ start: "2026-06-05T17:00:00Z",
203
+ attendees: ["ada@example.com", "grace@example.com"],
204
+ });
205
+
206
+ await receipts.flush();
207
+
208
+ console.log("Emitted demo Sello receipt.");
209
+ console.log(JSON.stringify(response, null, 2));
210
+ console.log("");
211
+ console.log("View verified actions with:");
212
+ console.log(" sello actions");
213
+ console.log("");
214
+ console.log("Or open:");
215
+ console.log(` ${actionViewerUrl(state)}`);
216
+ }
217
+
218
+ function initDemoCommand(args ) {
219
+ const output = readFlag(args, "--output") ?? "emit-receipt.mjs";
220
+ const force = args.includes("--force");
221
+
222
+ if (existsSync(output) && !force) {
223
+ throw new TypeError(`${output} already exists. Pass --force to overwrite it.`);
224
+ }
225
+
226
+ writeFileSync(output, demoEmitterSource(), { mode: 0o644 });
227
+
228
+ console.log(`Created ${output}`);
229
+ console.log("");
230
+ console.log("Run:");
231
+ console.log(" npm install sello");
232
+ console.log(" npx sello dev");
233
+ console.log(` node ${output}`);
234
+ console.log(" npx sello actions");
235
+ }
236
+
159
237
  function keysCommand(args ) {
160
238
  const subcommand = args[0] ?? "service";
161
239
  if (subcommand !== "service") {
@@ -338,6 +416,14 @@ function loadDevStateIfPresent() {
338
416
  }
339
417
  }
340
418
 
419
+ function loadDevStateOrThrow(message ) {
420
+ const state = loadDevStateIfPresent();
421
+ if (!state) {
422
+ throw new TypeError(message);
423
+ }
424
+ return state;
425
+ }
426
+
341
427
  function devStatePath() {
342
428
  return join(process.cwd(), ".sello", "dev.json");
343
429
  }
@@ -477,6 +563,8 @@ function isRecord(value ) {
477
563
  function printHelp() {
478
564
  console.log(`Usage:
479
565
  sello dev [--port 8787] [--service service-id] [--dry-run]
566
+ sello emit-demo [--title title]
567
+ sello init-demo [--output emit-receipt.mjs] [--force]
480
568
  sello actions [--token agent-token]
481
569
  sello keys service
482
570
  sello inspect-env
@@ -513,3 +601,79 @@ function enforceNodeVersion() {
513
601
  );
514
602
  }
515
603
  }
604
+
605
+
606
+
607
+
608
+
609
+
610
+
611
+
612
+
613
+
614
+
615
+
616
+
617
+
618
+
619
+
620
+
621
+ function actionViewerUrl(state ) {
622
+ const endpoint = new URL(state.logEndpoint);
623
+ return `${endpoint.origin}/actions`;
624
+ }
625
+
626
+ function slug(value ) {
627
+ return value
628
+ .toLowerCase()
629
+ .replace(/[^a-z0-9]+/g, "_")
630
+ .replace(/^_+|_+$/g, "")
631
+ .slice(0, 40);
632
+ }
633
+
634
+ function demoEmitterSource() {
635
+ return `import { readFileSync } from "node:fs";
636
+ import { canonicalJsonBytes, sello } from "sello";
637
+
638
+ const state = JSON.parse(readFileSync(".sello/dev.json", "utf8"));
639
+
640
+ const receipts = sello.service({
641
+ service: state.serviceId,
642
+ serviceKey: state.serviceKey,
643
+ tokenIssuer: state.tokenIssuerPublicKey,
644
+ log: sello.logs.http(state.logUrl, { endpoint: state.logEndpoint }),
645
+ submit: { mode: "await" },
646
+ });
647
+
648
+ const createEvent = receipts.tool(
649
+ "calendar.create_event",
650
+ async (request) => ({
651
+ id: "evt_test_sello_receipt",
652
+ calendarId: request.calendarId,
653
+ title: request.title,
654
+ status: "created",
655
+ createdAt: new Date().toISOString(),
656
+ }),
657
+ {
658
+ canonicalizeInput: (request) =>
659
+ canonicalJsonBytes({
660
+ calendarId: request.calendarId,
661
+ title: request.title,
662
+ start: request.start,
663
+ attendees: request.attendees,
664
+ }),
665
+ },
666
+ );
667
+
668
+ const result = await createEvent({
669
+ authorizationToken: state.agentToken,
670
+ calendarId: "demo-calendar",
671
+ title: "Test Sello receipt",
672
+ start: "2026-06-05T17:00:00Z",
673
+ attendees: ["ada@example.com", "grace@example.com"],
674
+ });
675
+
676
+ await receipts.flush();
677
+ console.log(result);
678
+ `;
679
+ }
@@ -18,6 +18,23 @@ The service process emits receipts. It does not need the owner private key.
18
18
 
19
19
  ## Local Development
20
20
 
21
+ From a new project, the shortest loop is:
22
+
23
+ ```bash
24
+ # Terminal 1
25
+ npx sello dev
26
+
27
+ # Terminal 2
28
+ npx sello emit-demo
29
+ npx sello actions
30
+ ```
31
+
32
+ To write the tiny emitter file into your project:
33
+
34
+ ```bash
35
+ npx sello init-demo
36
+ ```
37
+
21
38
  Inside this repo, start the local log and action viewer:
22
39
 
23
40
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sello",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env -S node --experimental-strip-types
2
2
 
3
3
  import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
4
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
 
7
7
  import { generateEd25519KeyPair } from "../cose/sign1.ts";
@@ -9,6 +9,7 @@ import { deriveTokenIdentifiers, sha256, toHex } from "../crypto/identifiers.ts"
9
9
  import { generateHpkeKeyPair } from "../hpke/receipt.ts";
10
10
  import { type CanonicalLogUrl } from "../log/canonical-url.ts";
11
11
  import { MockTransparencyLog } from "../log/mock-log.ts";
12
+ import { canonicalJsonBytes } from "../mcp/middleware.ts";
12
13
  import { verifyReceipts } from "../owner/verify.ts";
13
14
  import {
14
15
  loadSignedRegistry,
@@ -26,10 +27,12 @@ import {
26
27
  } from "../sdk/keys.ts";
27
28
  import {
28
29
  deserializeEntry,
30
+ http as httpLog,
29
31
  queryHttpLogByTokenRef,
30
32
  serializeEntry,
31
33
  toCanonicalLogUrl,
32
34
  } from "../sdk/logs.ts";
35
+ import { createSelloService } from "../sdk/service.ts";
33
36
 
34
37
  type DevState = {
35
38
  serviceId: string;
@@ -57,6 +60,12 @@ try {
57
60
  case "dev":
58
61
  await devCommand(process.argv.slice(3));
59
62
  break;
63
+ case "emit-demo":
64
+ await emitDemoCommand(process.argv.slice(3));
65
+ break;
66
+ case "init-demo":
67
+ initDemoCommand(process.argv.slice(3));
68
+ break;
60
69
  case "keys":
61
70
  keysCommand(process.argv.slice(3));
62
71
  break;
@@ -157,6 +166,75 @@ async function devCommand(args: string[]): Promise<void> {
157
166
  });
158
167
  }
159
168
 
169
+ async function emitDemoCommand(args: string[]): Promise<void> {
170
+ const state = loadDevStateOrThrow(
171
+ "missing local Sello dev state. Run `sello dev` first, then run `sello emit-demo` in another terminal from the same directory.",
172
+ );
173
+ const title = readFlag(args, "--title") ?? "Test Sello receipt";
174
+ const receipts = createSelloService({
175
+ service: state.serviceId,
176
+ serviceKey: state.serviceKey,
177
+ tokenIssuer: state.tokenIssuerPublicKey,
178
+ log: httpLog(state.logUrl, { endpoint: state.logEndpoint }),
179
+ submit: { mode: "await" },
180
+ });
181
+ const createEvent = receipts.tool<DemoEventRequest, DemoEventResponse>(
182
+ "calendar.create_event",
183
+ async (request) => ({
184
+ id: `evt_${slug(request.title)}`,
185
+ calendarId: request.calendarId,
186
+ title: request.title,
187
+ status: "created",
188
+ createdAt: new Date().toISOString(),
189
+ }),
190
+ {
191
+ canonicalizeInput: (request) => canonicalJsonBytes({
192
+ calendarId: request.calendarId,
193
+ title: request.title,
194
+ start: request.start,
195
+ attendees: request.attendees,
196
+ }),
197
+ },
198
+ );
199
+ const response = await createEvent({
200
+ authorizationToken: state.agentToken,
201
+ calendarId: "demo-calendar",
202
+ title,
203
+ start: "2026-06-05T17:00:00Z",
204
+ attendees: ["ada@example.com", "grace@example.com"],
205
+ });
206
+
207
+ await receipts.flush();
208
+
209
+ console.log("Emitted demo Sello receipt.");
210
+ console.log(JSON.stringify(response, null, 2));
211
+ console.log("");
212
+ console.log("View verified actions with:");
213
+ console.log(" sello actions");
214
+ console.log("");
215
+ console.log("Or open:");
216
+ console.log(` ${actionViewerUrl(state)}`);
217
+ }
218
+
219
+ function initDemoCommand(args: string[]): void {
220
+ const output = readFlag(args, "--output") ?? "emit-receipt.mjs";
221
+ const force = args.includes("--force");
222
+
223
+ if (existsSync(output) && !force) {
224
+ throw new TypeError(`${output} already exists. Pass --force to overwrite it.`);
225
+ }
226
+
227
+ writeFileSync(output, demoEmitterSource(), { mode: 0o644 });
228
+
229
+ console.log(`Created ${output}`);
230
+ console.log("");
231
+ console.log("Run:");
232
+ console.log(" npm install sello");
233
+ console.log(" npx sello dev");
234
+ console.log(` node ${output}`);
235
+ console.log(" npx sello actions");
236
+ }
237
+
160
238
  function keysCommand(args: string[]): void {
161
239
  const subcommand = args[0] ?? "service";
162
240
  if (subcommand !== "service") {
@@ -339,6 +417,14 @@ function loadDevStateIfPresent(): DevState | undefined {
339
417
  }
340
418
  }
341
419
 
420
+ function loadDevStateOrThrow(message: string): DevState {
421
+ const state = loadDevStateIfPresent();
422
+ if (!state) {
423
+ throw new TypeError(message);
424
+ }
425
+ return state;
426
+ }
427
+
342
428
  function devStatePath(): string {
343
429
  return join(process.cwd(), ".sello", "dev.json");
344
430
  }
@@ -478,6 +564,8 @@ function isRecord(value: unknown): value is Record<string, unknown> {
478
564
  function printHelp(): void {
479
565
  console.log(`Usage:
480
566
  sello dev [--port 8787] [--service service-id] [--dry-run]
567
+ sello emit-demo [--title title]
568
+ sello init-demo [--output emit-receipt.mjs] [--force]
481
569
  sello actions [--token agent-token]
482
570
  sello keys service
483
571
  sello inspect-env
@@ -514,3 +602,79 @@ function enforceNodeVersion(): void {
514
602
  );
515
603
  }
516
604
  }
605
+
606
+ type DemoEventRequest = {
607
+ authorizationToken: string;
608
+ calendarId: string;
609
+ title: string;
610
+ start: string;
611
+ attendees: string[];
612
+ };
613
+
614
+ type DemoEventResponse = {
615
+ id: string;
616
+ calendarId: string;
617
+ title: string;
618
+ status: "created";
619
+ createdAt: string;
620
+ };
621
+
622
+ function actionViewerUrl(state: DevState): string {
623
+ const endpoint = new URL(state.logEndpoint);
624
+ return `${endpoint.origin}/actions`;
625
+ }
626
+
627
+ function slug(value: string): string {
628
+ return value
629
+ .toLowerCase()
630
+ .replace(/[^a-z0-9]+/g, "_")
631
+ .replace(/^_+|_+$/g, "")
632
+ .slice(0, 40);
633
+ }
634
+
635
+ function demoEmitterSource(): string {
636
+ return `import { readFileSync } from "node:fs";
637
+ import { canonicalJsonBytes, sello } from "sello";
638
+
639
+ const state = JSON.parse(readFileSync(".sello/dev.json", "utf8"));
640
+
641
+ const receipts = sello.service({
642
+ service: state.serviceId,
643
+ serviceKey: state.serviceKey,
644
+ tokenIssuer: state.tokenIssuerPublicKey,
645
+ log: sello.logs.http(state.logUrl, { endpoint: state.logEndpoint }),
646
+ submit: { mode: "await" },
647
+ });
648
+
649
+ const createEvent = receipts.tool(
650
+ "calendar.create_event",
651
+ async (request) => ({
652
+ id: "evt_test_sello_receipt",
653
+ calendarId: request.calendarId,
654
+ title: request.title,
655
+ status: "created",
656
+ createdAt: new Date().toISOString(),
657
+ }),
658
+ {
659
+ canonicalizeInput: (request) =>
660
+ canonicalJsonBytes({
661
+ calendarId: request.calendarId,
662
+ title: request.title,
663
+ start: request.start,
664
+ attendees: request.attendees,
665
+ }),
666
+ },
667
+ );
668
+
669
+ const result = await createEvent({
670
+ authorizationToken: state.agentToken,
671
+ calendarId: "demo-calendar",
672
+ title: "Test Sello receipt",
673
+ start: "2026-06-05T17:00:00Z",
674
+ attendees: ["ada@example.com", "grace@example.com"],
675
+ });
676
+
677
+ await receipts.flush();
678
+ console.log(result);
679
+ `;
680
+ }