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 +10 -0
- package/dist/cli/sello.js +182 -0
- package/docs/sdk-quickstart.md +13 -0
- package/package.json +1 -1
- package/src/cli/sello.ts +182 -0
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
|
+
}
|
package/docs/sdk-quickstart.md
CHANGED
|
@@ -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
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
|
+
}
|