sello 0.1.8 → 0.1.10
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 +9 -2
- package/dist/cli/sello.js +130 -9
- package/docs/sdk-quickstart.md +12 -1
- package/package.json +1 -1
- package/src/cli/sello.ts +130 -9
package/README.md
CHANGED
|
@@ -46,8 +46,15 @@ To scaffold a small HTTP route that emits receipts:
|
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
48
|
npx --yes sello init-http-demo
|
|
49
|
+
npx --yes sello call-http-demo
|
|
49
50
|
```
|
|
50
51
|
|
|
52
|
+
## What Just Happened?
|
|
53
|
+
|
|
54
|
+
`sello dev` created a local owner key, service key, token, registry, and transparency log. The demo tool or route verified the token before running the handler. After the handler ran, the service signed an encrypted receipt for the action it observed. The local log stored the encrypted receipt, not plaintext action details. `sello actions` fetched the receipt, verified the log entry and service signature, decrypted it with the owner key, and printed the owner's view.
|
|
55
|
+
|
|
56
|
+
Local dev state lives under `.sello/`. The dev log is stored as encrypted receipt entries in `.sello/dev-log.jsonl`, so receipts survive restarting `sello dev` while staying out of git.
|
|
57
|
+
|
|
51
58
|
## Why Sello?
|
|
52
59
|
|
|
53
60
|
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.
|
|
@@ -85,7 +92,7 @@ The implementation includes a local end-to-end demo, compact JWS token verificat
|
|
|
85
92
|
|------|------|
|
|
86
93
|
| Add Sello in a few lines | [SDK Quickstart](docs/sdk-quickstart.md) |
|
|
87
94
|
| 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` |
|
|
95
|
+
| Wrap one HTTP route | `npx --yes sello init-http-demo`, then `npx --yes sello call-http-demo` |
|
|
89
96
|
| Try a wrapped tool locally | `node --run dev`, then `node --run example:tool` |
|
|
90
97
|
| Try an MCP-style tool call | `node --run dev`, then `node --run example:mcp` |
|
|
91
98
|
| See a minimal MCP integration | [examples/mcp-minimal-server.ts](examples/mcp-minimal-server.ts) |
|
|
@@ -142,7 +149,7 @@ For an MCP-shaped `tools/call` boundary, run `node --run example:mcp` instead of
|
|
|
142
149
|
|
|
143
150
|
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.
|
|
144
151
|
|
|
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.
|
|
152
|
+
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.
|
|
146
153
|
|
|
147
154
|
## The First 10 Minutes
|
|
148
155
|
|
package/dist/cli/sello.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { createServer, } from "node:http";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
appendFileSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
5
11
|
import { dirname, join } from "node:path";
|
|
6
12
|
|
|
7
13
|
import { generateEd25519KeyPair } from "../cose/sign1.js";
|
|
@@ -81,6 +87,9 @@ try {
|
|
|
81
87
|
case "emit-demo":
|
|
82
88
|
await emitDemoCommand(process.argv.slice(3));
|
|
83
89
|
break;
|
|
90
|
+
case "call-http-demo":
|
|
91
|
+
await callHttpDemoCommand(process.argv.slice(3));
|
|
92
|
+
break;
|
|
84
93
|
case "init-demo":
|
|
85
94
|
initDemoCommand(process.argv.slice(3));
|
|
86
95
|
break;
|
|
@@ -160,7 +169,7 @@ async function devCommand(args ) {
|
|
|
160
169
|
"calendar.example.com/mcp/v1";
|
|
161
170
|
const logEndpoint = `http://localhost:${port}/api`;
|
|
162
171
|
const logUrl = toCanonicalLogUrl(`http://localhost:${port}/api`);
|
|
163
|
-
const state =
|
|
172
|
+
const state = loadOrCreateDevState({ serviceId, logUrl, logEndpoint });
|
|
164
173
|
saveDevState(state);
|
|
165
174
|
|
|
166
175
|
if (dryRun) {
|
|
@@ -171,10 +180,12 @@ async function devCommand(args ) {
|
|
|
171
180
|
}
|
|
172
181
|
|
|
173
182
|
const log = new MockTransparencyLog(logUrl);
|
|
183
|
+
const logPath = devLogPath();
|
|
184
|
+
const loadedEntries = loadDevLogEntries(log, logPath);
|
|
174
185
|
const registry = parseRegistry(textEncoder.encode(state.registryJson));
|
|
175
186
|
const server = createServer(async (request, response) => {
|
|
176
187
|
try {
|
|
177
|
-
await handleDevRequest({ request, response, log, state, registry });
|
|
188
|
+
await handleDevRequest({ request, response, log, logPath, state, registry });
|
|
178
189
|
} catch (error) {
|
|
179
190
|
sendJson(response, 500, {
|
|
180
191
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -184,6 +195,9 @@ async function devCommand(args ) {
|
|
|
184
195
|
|
|
185
196
|
server.listen(port, () => {
|
|
186
197
|
printDevConfig(port, state);
|
|
198
|
+
console.log("");
|
|
199
|
+
console.log(`Local dev log: ${logPath}`);
|
|
200
|
+
console.log(`Loaded ${devLogEntryCountLabel(loadedEntries)}.`);
|
|
187
201
|
});
|
|
188
202
|
}
|
|
189
203
|
|
|
@@ -237,6 +251,38 @@ async function emitDemoCommand(args ) {
|
|
|
237
251
|
console.log(` ${actionViewerUrl(state)}`);
|
|
238
252
|
}
|
|
239
253
|
|
|
254
|
+
async function callHttpDemoCommand(args ) {
|
|
255
|
+
const state = loadDevStateOrThrow(
|
|
256
|
+
"missing local Sello dev state. Run `sello dev` first, then run `sello call-http-demo` in another terminal from the same directory.",
|
|
257
|
+
);
|
|
258
|
+
const url = readFlag(args, "--url") ?? "http://localhost:8790/calendar/events";
|
|
259
|
+
const title = readFlag(args, "--title") ?? "Ship Sello";
|
|
260
|
+
const response = await fetch(url, {
|
|
261
|
+
method: "POST",
|
|
262
|
+
headers: {
|
|
263
|
+
authorization: `Bearer ${state.agentToken}`,
|
|
264
|
+
"content-type": "application/json",
|
|
265
|
+
},
|
|
266
|
+
body: JSON.stringify({ title }),
|
|
267
|
+
});
|
|
268
|
+
const responseText = await response.text();
|
|
269
|
+
|
|
270
|
+
if (!response.ok) {
|
|
271
|
+
throw new TypeError(
|
|
272
|
+
`HTTP route demo failed with HTTP ${response.status}: ${responseText.trim()}`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.log("Called Sello HTTP route demo.");
|
|
277
|
+
console.log(formatHttpDemoResponse(responseText));
|
|
278
|
+
console.log("");
|
|
279
|
+
console.log("View verified actions with:");
|
|
280
|
+
console.log(" sello actions");
|
|
281
|
+
console.log("");
|
|
282
|
+
console.log("Or open:");
|
|
283
|
+
console.log(` ${actionViewerUrl(state)}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
240
286
|
function initDemoCommand(args ) {
|
|
241
287
|
const output = readFlag(args, "--output") ?? "emit-receipt.mjs";
|
|
242
288
|
const force = args.includes("--force");
|
|
@@ -278,8 +324,7 @@ function initHttpDemoCommand(args ) {
|
|
|
278
324
|
console.log(` node ${output}`);
|
|
279
325
|
console.log("");
|
|
280
326
|
console.log("Terminal 3: call the route and view the receipt");
|
|
281
|
-
console.log("
|
|
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\"}'");
|
|
327
|
+
console.log(" npx sello call-http-demo");
|
|
283
328
|
console.log(" npx sello actions");
|
|
284
329
|
console.log("");
|
|
285
330
|
console.log("Then open http://localhost:8787/actions");
|
|
@@ -330,9 +375,10 @@ async function handleDevRequest(input
|
|
|
330
375
|
|
|
331
376
|
|
|
332
377
|
|
|
378
|
+
|
|
333
379
|
|
|
334
380
|
) {
|
|
335
|
-
const { request, response, log, state, registry } = input;
|
|
381
|
+
const { request, response, log, logPath, state, registry } = input;
|
|
336
382
|
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
|
|
337
383
|
|
|
338
384
|
if (request.method === "GET" && (url.pathname === "/" || url.pathname === "/actions")) {
|
|
@@ -357,6 +403,7 @@ async function handleDevRequest(input
|
|
|
357
403
|
decodeBase64url(body.envelope, "envelope"),
|
|
358
404
|
typeof body.integratedTime === "string" ? body.integratedTime : undefined,
|
|
359
405
|
);
|
|
406
|
+
appendDevLogEntry(logPath, entry);
|
|
360
407
|
sendJson(response, 200, serializeEntry(entry));
|
|
361
408
|
return;
|
|
362
409
|
}
|
|
@@ -378,6 +425,24 @@ async function handleDevRequest(input
|
|
|
378
425
|
sendJson(response, 404, { error: "not found" });
|
|
379
426
|
}
|
|
380
427
|
|
|
428
|
+
function loadOrCreateDevState(input
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
) {
|
|
433
|
+
const existing = loadDevStateIfPresent();
|
|
434
|
+
if (
|
|
435
|
+
existing &&
|
|
436
|
+
existing.serviceId === input.serviceId &&
|
|
437
|
+
existing.logUrl === input.logUrl &&
|
|
438
|
+
existing.logEndpoint === input.logEndpoint
|
|
439
|
+
) {
|
|
440
|
+
return existing;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return createDevState(input);
|
|
444
|
+
}
|
|
445
|
+
|
|
381
446
|
function verifyDevActions(input
|
|
382
447
|
|
|
383
448
|
|
|
@@ -493,6 +558,54 @@ function devStatePath() {
|
|
|
493
558
|
return join(process.cwd(), ".sello", "dev.json");
|
|
494
559
|
}
|
|
495
560
|
|
|
561
|
+
function devLogPath() {
|
|
562
|
+
return join(process.cwd(), ".sello", "dev-log.jsonl");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function loadDevLogEntries(log , path ) {
|
|
566
|
+
if (!existsSync(path)) {
|
|
567
|
+
return 0;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const lines = textDecoder
|
|
571
|
+
.decode(readFileBytes(path))
|
|
572
|
+
.split(/\r?\n/)
|
|
573
|
+
.filter((line) => line.trim().length > 0);
|
|
574
|
+
|
|
575
|
+
let loaded = 0;
|
|
576
|
+
for (const [index, line] of lines.entries()) {
|
|
577
|
+
let entry;
|
|
578
|
+
try {
|
|
579
|
+
entry = deserializeEntry(JSON.parse(line));
|
|
580
|
+
if (entry.logUrl !== log.logUrl) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
log.append(entry.envelope, entry.integratedTime);
|
|
584
|
+
loaded += 1;
|
|
585
|
+
} catch (error) {
|
|
586
|
+
throw new TypeError(
|
|
587
|
+
`invalid local dev log entry ${index + 1} in ${path}: ${
|
|
588
|
+
error instanceof Error ? error.message : String(error)
|
|
589
|
+
}`,
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return loaded;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function appendDevLogEntry(
|
|
598
|
+
path ,
|
|
599
|
+
entry ,
|
|
600
|
+
) {
|
|
601
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
602
|
+
appendFileSync(path, `${JSON.stringify(serializeEntry(entry))}\n`, { mode: 0o600 });
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function devLogEntryCountLabel(count ) {
|
|
606
|
+
return count === 1 ? "1 encrypted receipt" : `${count} encrypted receipts`;
|
|
607
|
+
}
|
|
608
|
+
|
|
496
609
|
function readFileBytes(path ) {
|
|
497
610
|
return new Uint8Array(readFileSync(path));
|
|
498
611
|
}
|
|
@@ -678,6 +791,7 @@ function printHelp() {
|
|
|
678
791
|
console.log(`Usage:
|
|
679
792
|
sello dev [--port 8787] [--service service-id] [--dry-run]
|
|
680
793
|
sello emit-demo [--title title]
|
|
794
|
+
sello call-http-demo [--url http://localhost:8790/calendar/events] [--title title]
|
|
681
795
|
sello init-demo [--output emit-receipt.mjs] [--force]
|
|
682
796
|
sello init-http-demo [--output sello-http-route.mjs] [--force]
|
|
683
797
|
sello actions [--token agent-token]
|
|
@@ -738,6 +852,14 @@ function actionViewerUrl(state ) {
|
|
|
738
852
|
return `${endpoint.origin}/actions`;
|
|
739
853
|
}
|
|
740
854
|
|
|
855
|
+
function formatHttpDemoResponse(responseText ) {
|
|
856
|
+
try {
|
|
857
|
+
return JSON.stringify(JSON.parse(responseText), null, 2);
|
|
858
|
+
} catch {
|
|
859
|
+
return responseText;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
741
863
|
function slug(value ) {
|
|
742
864
|
return value
|
|
743
865
|
.toLowerCase()
|
|
@@ -857,9 +979,8 @@ const server = createServer(async (request, response) => {
|
|
|
857
979
|
server.listen(port, () => {
|
|
858
980
|
console.log("Sello HTTP route demo running at http://localhost:" + port + "/calendar/events");
|
|
859
981
|
console.log("");
|
|
860
|
-
console.log("Call it with:");
|
|
861
|
-
console.log("
|
|
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\\\"}'");
|
|
982
|
+
console.log("Call it from another terminal with:");
|
|
983
|
+
console.log(" npx sello call-http-demo");
|
|
863
984
|
});
|
|
864
985
|
|
|
865
986
|
async function readJson(request) {
|
package/docs/sdk-quickstart.md
CHANGED
|
@@ -41,7 +41,18 @@ To write a dependency-free HTTP route example into your project:
|
|
|
41
41
|
npx --yes sello init-http-demo
|
|
42
42
|
```
|
|
43
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.
|
|
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
|
+
|
|
51
|
+
## What Just Happened?
|
|
52
|
+
|
|
53
|
+
`sello dev` created local development keys, a demo authorization token, a service registry, and a local transparency log. The wrapped tool or route verified the token before running your handler. After the handler returned, Sello signed an encrypted receipt for the observed action and submitted it to the local log. The log stored encrypted receipt data, not plaintext action details. `sello actions` used the owner key from local dev state to fetch, verify, decrypt, and print the action.
|
|
54
|
+
|
|
55
|
+
Local dev state lives under `.sello/`. The encrypted dev log is stored in `.sello/dev-log.jsonl`, so receipts survive restarting `sello dev` without being committed to git.
|
|
45
56
|
|
|
46
57
|
Inside this repo, start the local log and action viewer:
|
|
47
58
|
|
package/package.json
CHANGED
package/src/cli/sello.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
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 {
|
|
4
|
+
import {
|
|
5
|
+
appendFileSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
5
11
|
import { dirname, join } from "node:path";
|
|
6
12
|
|
|
7
13
|
import { generateEd25519KeyPair } from "../cose/sign1.ts";
|
|
@@ -82,6 +88,9 @@ try {
|
|
|
82
88
|
case "emit-demo":
|
|
83
89
|
await emitDemoCommand(process.argv.slice(3));
|
|
84
90
|
break;
|
|
91
|
+
case "call-http-demo":
|
|
92
|
+
await callHttpDemoCommand(process.argv.slice(3));
|
|
93
|
+
break;
|
|
85
94
|
case "init-demo":
|
|
86
95
|
initDemoCommand(process.argv.slice(3));
|
|
87
96
|
break;
|
|
@@ -161,7 +170,7 @@ async function devCommand(args: string[]): Promise<void> {
|
|
|
161
170
|
"calendar.example.com/mcp/v1";
|
|
162
171
|
const logEndpoint = `http://localhost:${port}/api`;
|
|
163
172
|
const logUrl = toCanonicalLogUrl(`http://localhost:${port}/api`);
|
|
164
|
-
const state =
|
|
173
|
+
const state = loadOrCreateDevState({ serviceId, logUrl, logEndpoint });
|
|
165
174
|
saveDevState(state);
|
|
166
175
|
|
|
167
176
|
if (dryRun) {
|
|
@@ -172,10 +181,12 @@ async function devCommand(args: string[]): Promise<void> {
|
|
|
172
181
|
}
|
|
173
182
|
|
|
174
183
|
const log = new MockTransparencyLog(logUrl);
|
|
184
|
+
const logPath = devLogPath();
|
|
185
|
+
const loadedEntries = loadDevLogEntries(log, logPath);
|
|
175
186
|
const registry = parseRegistry(textEncoder.encode(state.registryJson));
|
|
176
187
|
const server = createServer(async (request, response) => {
|
|
177
188
|
try {
|
|
178
|
-
await handleDevRequest({ request, response, log, state, registry });
|
|
189
|
+
await handleDevRequest({ request, response, log, logPath, state, registry });
|
|
179
190
|
} catch (error) {
|
|
180
191
|
sendJson(response, 500, {
|
|
181
192
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -185,6 +196,9 @@ async function devCommand(args: string[]): Promise<void> {
|
|
|
185
196
|
|
|
186
197
|
server.listen(port, () => {
|
|
187
198
|
printDevConfig(port, state);
|
|
199
|
+
console.log("");
|
|
200
|
+
console.log(`Local dev log: ${logPath}`);
|
|
201
|
+
console.log(`Loaded ${devLogEntryCountLabel(loadedEntries)}.`);
|
|
188
202
|
});
|
|
189
203
|
}
|
|
190
204
|
|
|
@@ -238,6 +252,38 @@ async function emitDemoCommand(args: string[]): Promise<void> {
|
|
|
238
252
|
console.log(` ${actionViewerUrl(state)}`);
|
|
239
253
|
}
|
|
240
254
|
|
|
255
|
+
async function callHttpDemoCommand(args: string[]): Promise<void> {
|
|
256
|
+
const state = loadDevStateOrThrow(
|
|
257
|
+
"missing local Sello dev state. Run `sello dev` first, then run `sello call-http-demo` in another terminal from the same directory.",
|
|
258
|
+
);
|
|
259
|
+
const url = readFlag(args, "--url") ?? "http://localhost:8790/calendar/events";
|
|
260
|
+
const title = readFlag(args, "--title") ?? "Ship Sello";
|
|
261
|
+
const response = await fetch(url, {
|
|
262
|
+
method: "POST",
|
|
263
|
+
headers: {
|
|
264
|
+
authorization: `Bearer ${state.agentToken}`,
|
|
265
|
+
"content-type": "application/json",
|
|
266
|
+
},
|
|
267
|
+
body: JSON.stringify({ title }),
|
|
268
|
+
});
|
|
269
|
+
const responseText = await response.text();
|
|
270
|
+
|
|
271
|
+
if (!response.ok) {
|
|
272
|
+
throw new TypeError(
|
|
273
|
+
`HTTP route demo failed with HTTP ${response.status}: ${responseText.trim()}`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
console.log("Called Sello HTTP route demo.");
|
|
278
|
+
console.log(formatHttpDemoResponse(responseText));
|
|
279
|
+
console.log("");
|
|
280
|
+
console.log("View verified actions with:");
|
|
281
|
+
console.log(" sello actions");
|
|
282
|
+
console.log("");
|
|
283
|
+
console.log("Or open:");
|
|
284
|
+
console.log(` ${actionViewerUrl(state)}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
241
287
|
function initDemoCommand(args: string[]): void {
|
|
242
288
|
const output = readFlag(args, "--output") ?? "emit-receipt.mjs";
|
|
243
289
|
const force = args.includes("--force");
|
|
@@ -279,8 +325,7 @@ function initHttpDemoCommand(args: string[]): void {
|
|
|
279
325
|
console.log(` node ${output}`);
|
|
280
326
|
console.log("");
|
|
281
327
|
console.log("Terminal 3: call the route and view the receipt");
|
|
282
|
-
console.log("
|
|
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\"}'");
|
|
328
|
+
console.log(" npx sello call-http-demo");
|
|
284
329
|
console.log(" npx sello actions");
|
|
285
330
|
console.log("");
|
|
286
331
|
console.log("Then open http://localhost:8787/actions");
|
|
@@ -330,10 +375,11 @@ async function handleDevRequest(input: {
|
|
|
330
375
|
request: IncomingMessage;
|
|
331
376
|
response: ServerResponse;
|
|
332
377
|
log: MockTransparencyLog;
|
|
378
|
+
logPath: string;
|
|
333
379
|
state: DevState;
|
|
334
380
|
registry: ReturnType<typeof parseRegistry>;
|
|
335
381
|
}): Promise<void> {
|
|
336
|
-
const { request, response, log, state, registry } = input;
|
|
382
|
+
const { request, response, log, logPath, state, registry } = input;
|
|
337
383
|
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
|
|
338
384
|
|
|
339
385
|
if (request.method === "GET" && (url.pathname === "/" || url.pathname === "/actions")) {
|
|
@@ -358,6 +404,7 @@ async function handleDevRequest(input: {
|
|
|
358
404
|
decodeBase64url(body.envelope, "envelope"),
|
|
359
405
|
typeof body.integratedTime === "string" ? body.integratedTime : undefined,
|
|
360
406
|
);
|
|
407
|
+
appendDevLogEntry(logPath, entry);
|
|
361
408
|
sendJson(response, 200, serializeEntry(entry));
|
|
362
409
|
return;
|
|
363
410
|
}
|
|
@@ -379,6 +426,24 @@ async function handleDevRequest(input: {
|
|
|
379
426
|
sendJson(response, 404, { error: "not found" });
|
|
380
427
|
}
|
|
381
428
|
|
|
429
|
+
function loadOrCreateDevState(input: {
|
|
430
|
+
serviceId: string;
|
|
431
|
+
logUrl: CanonicalLogUrl;
|
|
432
|
+
logEndpoint: string;
|
|
433
|
+
}): DevState {
|
|
434
|
+
const existing = loadDevStateIfPresent();
|
|
435
|
+
if (
|
|
436
|
+
existing &&
|
|
437
|
+
existing.serviceId === input.serviceId &&
|
|
438
|
+
existing.logUrl === input.logUrl &&
|
|
439
|
+
existing.logEndpoint === input.logEndpoint
|
|
440
|
+
) {
|
|
441
|
+
return existing;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return createDevState(input);
|
|
445
|
+
}
|
|
446
|
+
|
|
382
447
|
function verifyDevActions(input: {
|
|
383
448
|
log: MockTransparencyLog;
|
|
384
449
|
state: DevState;
|
|
@@ -494,6 +559,54 @@ function devStatePath(): string {
|
|
|
494
559
|
return join(process.cwd(), ".sello", "dev.json");
|
|
495
560
|
}
|
|
496
561
|
|
|
562
|
+
function devLogPath(): string {
|
|
563
|
+
return join(process.cwd(), ".sello", "dev-log.jsonl");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function loadDevLogEntries(log: MockTransparencyLog, path: string): number {
|
|
567
|
+
if (!existsSync(path)) {
|
|
568
|
+
return 0;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const lines = textDecoder
|
|
572
|
+
.decode(readFileBytes(path))
|
|
573
|
+
.split(/\r?\n/)
|
|
574
|
+
.filter((line) => line.trim().length > 0);
|
|
575
|
+
|
|
576
|
+
let loaded = 0;
|
|
577
|
+
for (const [index, line] of lines.entries()) {
|
|
578
|
+
let entry;
|
|
579
|
+
try {
|
|
580
|
+
entry = deserializeEntry(JSON.parse(line));
|
|
581
|
+
if (entry.logUrl !== log.logUrl) {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
log.append(entry.envelope, entry.integratedTime);
|
|
585
|
+
loaded += 1;
|
|
586
|
+
} catch (error) {
|
|
587
|
+
throw new TypeError(
|
|
588
|
+
`invalid local dev log entry ${index + 1} in ${path}: ${
|
|
589
|
+
error instanceof Error ? error.message : String(error)
|
|
590
|
+
}`,
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return loaded;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function appendDevLogEntry(
|
|
599
|
+
path: string,
|
|
600
|
+
entry: ReturnType<MockTransparencyLog["append"]>,
|
|
601
|
+
): void {
|
|
602
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
603
|
+
appendFileSync(path, `${JSON.stringify(serializeEntry(entry))}\n`, { mode: 0o600 });
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function devLogEntryCountLabel(count: number): string {
|
|
607
|
+
return count === 1 ? "1 encrypted receipt" : `${count} encrypted receipts`;
|
|
608
|
+
}
|
|
609
|
+
|
|
497
610
|
function readFileBytes(path: string): Uint8Array {
|
|
498
611
|
return new Uint8Array(readFileSync(path));
|
|
499
612
|
}
|
|
@@ -679,6 +792,7 @@ function printHelp(): void {
|
|
|
679
792
|
console.log(`Usage:
|
|
680
793
|
sello dev [--port 8787] [--service service-id] [--dry-run]
|
|
681
794
|
sello emit-demo [--title title]
|
|
795
|
+
sello call-http-demo [--url http://localhost:8790/calendar/events] [--title title]
|
|
682
796
|
sello init-demo [--output emit-receipt.mjs] [--force]
|
|
683
797
|
sello init-http-demo [--output sello-http-route.mjs] [--force]
|
|
684
798
|
sello actions [--token agent-token]
|
|
@@ -739,6 +853,14 @@ function actionViewerUrl(state: DevState): string {
|
|
|
739
853
|
return `${endpoint.origin}/actions`;
|
|
740
854
|
}
|
|
741
855
|
|
|
856
|
+
function formatHttpDemoResponse(responseText: string): string {
|
|
857
|
+
try {
|
|
858
|
+
return JSON.stringify(JSON.parse(responseText), null, 2);
|
|
859
|
+
} catch {
|
|
860
|
+
return responseText;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
742
864
|
function slug(value: string): string {
|
|
743
865
|
return value
|
|
744
866
|
.toLowerCase()
|
|
@@ -858,9 +980,8 @@ const server = createServer(async (request, response) => {
|
|
|
858
980
|
server.listen(port, () => {
|
|
859
981
|
console.log("Sello HTTP route demo running at http://localhost:" + port + "/calendar/events");
|
|
860
982
|
console.log("");
|
|
861
|
-
console.log("Call it with:");
|
|
862
|
-
console.log("
|
|
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\\\"}'");
|
|
983
|
+
console.log("Call it from another terminal with:");
|
|
984
|
+
console.log(" npx sello call-http-demo");
|
|
864
985
|
});
|
|
865
986
|
|
|
866
987
|
async function readJson(request) {
|