sello 0.1.3 → 0.1.5
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 +40 -29
- package/dist/cli/sello.js +65 -10
- package/docs/sdk-quickstart.md +14 -4
- package/examples/mcp-minimal-server.ts +95 -0
- package/package.json +2 -1
- package/src/cli/sello.ts +65 -10
package/README.md
CHANGED
|
@@ -15,6 +15,33 @@ agent calls service
|
|
|
15
15
|
-> owner fetches, verifies, decrypts
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
## Try It
|
|
19
|
+
|
|
20
|
+
Requires Node.js 22.7 or newer.
|
|
21
|
+
|
|
22
|
+
From a new project or temporary folder:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Terminal 1
|
|
26
|
+
npx --yes sello dev
|
|
27
|
+
|
|
28
|
+
# Terminal 2
|
|
29
|
+
npx --yes sello emit-demo
|
|
30
|
+
npx --yes sello actions
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then open:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
http://localhost:8787/actions
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
To see the tiny wrapped-tool source:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx --yes sello init-demo
|
|
43
|
+
```
|
|
44
|
+
|
|
18
45
|
## Why Sello?
|
|
19
46
|
|
|
20
47
|
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.
|
|
@@ -48,14 +75,13 @@ The implementation includes a local end-to-end demo, compact JWS token verificat
|
|
|
48
75
|
|
|
49
76
|
## Start Here
|
|
50
77
|
|
|
51
|
-
Sello currently requires Node.js 22.7 or newer.
|
|
52
|
-
|
|
53
78
|
| Goal | Read |
|
|
54
79
|
|------|------|
|
|
55
80
|
| 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` |
|
|
81
|
+
| Emit your first receipt | `npx --yes sello dev`, then `npx --yes sello emit-demo` |
|
|
57
82
|
| Try a wrapped tool locally | `node --run dev`, then `node --run example:tool` |
|
|
58
83
|
| Try an MCP-style tool call | `node --run dev`, then `node --run example:mcp` |
|
|
84
|
+
| See a minimal MCP integration | [examples/mcp-minimal-server.ts](examples/mcp-minimal-server.ts) |
|
|
59
85
|
| Understand the protocol | [SPEC.md](SPEC.md) Quick Start |
|
|
60
86
|
| Run the local demo | `node --run demo` |
|
|
61
87
|
| Run the test suite | `node --run test` |
|
|
@@ -82,6 +108,14 @@ Then inspect verified actions:
|
|
|
82
108
|
npx sello actions --token <agent-token>
|
|
83
109
|
```
|
|
84
110
|
|
|
111
|
+
In local dev, `sello dev` prints and saves a demo token:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
SELLO_ACTION_TOKEN=eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
You usually do not need to copy it. `npx sello actions` reads the latest dev token from `.sello/dev.json`. Use `--token` when you want to inspect receipts for a specific agent authorization token from your own system.
|
|
118
|
+
|
|
85
119
|
Sello works with your own log server. `sello.build` is optional convenience, not a protocol requirement.
|
|
86
120
|
|
|
87
121
|
To try the repo's local loop:
|
|
@@ -99,22 +133,7 @@ The example wraps a fake calendar tool, emits a service-signed encrypted receipt
|
|
|
99
133
|
|
|
100
134
|
For an MCP-shaped `tools/call` boundary, run `node --run example:mcp` instead of `node --run example:tool`.
|
|
101
135
|
|
|
102
|
-
|
|
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
|
-
```
|
|
136
|
+
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.
|
|
118
137
|
|
|
119
138
|
## The First 10 Minutes
|
|
120
139
|
|
|
@@ -198,18 +217,10 @@ Much of this prior work surfaced after Sello's design had already converged on s
|
|
|
198
217
|
- Use verifiable log integrated time for revocation decisions, not the receipt timestamp.
|
|
199
218
|
- Deduplicate only on the full spec key, including action type and input/output hashes.
|
|
200
219
|
|
|
201
|
-
## License
|
|
202
|
-
|
|
203
|
-
Apache 2.0. See [LICENSE](LICENSE).
|
|
204
|
-
|
|
205
220
|
## Feedback
|
|
206
221
|
|
|
207
222
|
Issues and pull requests are welcome. This is an early draft; adversarial review is the point.
|
|
208
223
|
|
|
209
|
-
##
|
|
210
|
-
|
|
211
|
-
The current dependency-free test command is:
|
|
224
|
+
## License
|
|
212
225
|
|
|
213
|
-
|
|
214
|
-
node --run test
|
|
215
|
-
```
|
|
226
|
+
Apache 2.0. See [LICENSE](LICENSE).
|
package/dist/cli/sello.js
CHANGED
|
@@ -44,6 +44,25 @@ import { createSelloService } from "../sdk/service.js";
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
47
66
|
|
|
48
67
|
|
|
49
68
|
const textEncoder = new TextEncoder();
|
|
@@ -285,16 +304,17 @@ async function handleDevRequest(input
|
|
|
285
304
|
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
|
|
286
305
|
|
|
287
306
|
if (request.method === "GET" && (url.pathname === "/" || url.pathname === "/actions")) {
|
|
288
|
-
const result =
|
|
289
|
-
authorizationTokenBytes: textEncoder.encode(state.agentToken),
|
|
290
|
-
trustedLogs: [log],
|
|
291
|
-
registry,
|
|
292
|
-
ownerPrivateKey: normalizeHpkePrivateKey(state.ownerKey),
|
|
293
|
-
});
|
|
307
|
+
const result = verifyDevActions({ log, state, registry });
|
|
294
308
|
sendHtml(response, renderActionsHtml(result));
|
|
295
309
|
return;
|
|
296
310
|
}
|
|
297
311
|
|
|
312
|
+
if (request.method === "GET" && url.pathname === "/api/actions") {
|
|
313
|
+
const result = verifyDevActions({ log, state, registry });
|
|
314
|
+
sendJson(response, 200, actionsViewModel(result));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
298
318
|
if (request.method === "POST" && url.pathname === "/api/entries") {
|
|
299
319
|
const body = await readJson(request);
|
|
300
320
|
if (!isRecord(body) || typeof body.envelope !== "string") {
|
|
@@ -326,6 +346,19 @@ async function handleDevRequest(input
|
|
|
326
346
|
sendJson(response, 404, { error: "not found" });
|
|
327
347
|
}
|
|
328
348
|
|
|
349
|
+
function verifyDevActions(input
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
) {
|
|
354
|
+
return verifyReceipts({
|
|
355
|
+
authorizationTokenBytes: textEncoder.encode(input.state.agentToken),
|
|
356
|
+
trustedLogs: [input.log],
|
|
357
|
+
registry: input.registry,
|
|
358
|
+
ownerPrivateKey: normalizeHpkePrivateKey(input.state.ownerKey),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
329
362
|
async function loadViewerRegistry(devState ) {
|
|
330
363
|
if (process.env.SELLO_REGISTRY_PATH) {
|
|
331
364
|
return parseRegistry(readFileBytes(process.env.SELLO_REGISTRY_PATH));
|
|
@@ -461,16 +494,38 @@ function printActions(result ) {
|
|
|
461
494
|
}
|
|
462
495
|
}
|
|
463
496
|
|
|
497
|
+
function actionsViewModel(result ) {
|
|
498
|
+
return {
|
|
499
|
+
receipts: result.receipts.map((record) => ({
|
|
500
|
+
integratedTime: record.integratedTime,
|
|
501
|
+
serviceIdentifier: record.serviceIdentifier,
|
|
502
|
+
actionType: record.receipt["action-type"],
|
|
503
|
+
resultStatus: record.receipt["result-status"],
|
|
504
|
+
status: record.status,
|
|
505
|
+
logUrl: record.logUrl,
|
|
506
|
+
logCompleteness: record.logCompleteness,
|
|
507
|
+
sameSecondActivity: record.sameSecondActivity,
|
|
508
|
+
})),
|
|
509
|
+
rejected: result.rejected.map((record) => ({
|
|
510
|
+
code: record.code,
|
|
511
|
+
message: record.message,
|
|
512
|
+
...(record.logUrl === undefined ? {} : { logUrl: record.logUrl }),
|
|
513
|
+
...(record.integratedTime === undefined ? {} : { integratedTime: record.integratedTime }),
|
|
514
|
+
})),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
464
518
|
function renderActionsHtml(result ) {
|
|
465
|
-
const
|
|
519
|
+
const view = actionsViewModel(result);
|
|
520
|
+
const rows = view.receipts.map((record) => `
|
|
466
521
|
<tr>
|
|
467
522
|
<td>${escapeHtml(record.integratedTime)}</td>
|
|
468
523
|
<td>${escapeHtml(record.serviceIdentifier)}</td>
|
|
469
|
-
<td>${escapeHtml(record.
|
|
470
|
-
<td>${escapeHtml(record.
|
|
524
|
+
<td>${escapeHtml(record.actionType)}</td>
|
|
525
|
+
<td>${escapeHtml(record.resultStatus)}</td>
|
|
471
526
|
<td>${escapeHtml(record.status)}</td>
|
|
472
527
|
</tr>`).join("");
|
|
473
|
-
const rejected =
|
|
528
|
+
const rejected = view.rejected.map((record) => `
|
|
474
529
|
<li><strong>${escapeHtml(record.code)}</strong>: ${escapeHtml(record.message)}</li>`).join("");
|
|
475
530
|
|
|
476
531
|
return `<!doctype html>
|
package/docs/sdk-quickstart.md
CHANGED
|
@@ -22,17 +22,17 @@ From a new project, the shortest loop is:
|
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
24
|
# Terminal 1
|
|
25
|
-
npx sello dev
|
|
25
|
+
npx --yes sello dev
|
|
26
26
|
|
|
27
27
|
# Terminal 2
|
|
28
|
-
npx sello emit-demo
|
|
29
|
-
npx sello actions
|
|
28
|
+
npx --yes sello emit-demo
|
|
29
|
+
npx --yes sello actions
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
To write the tiny emitter file into your project:
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
npx sello init-demo
|
|
35
|
+
npx --yes sello init-demo
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
Inside this repo, start the local log and action viewer:
|
|
@@ -63,6 +63,8 @@ http://localhost:8787/actions
|
|
|
63
63
|
|
|
64
64
|
Both examples read `.sello/dev.json`, wrap a fake calendar handler, submit one encrypted receipt, and let the owner verify it locally.
|
|
65
65
|
|
|
66
|
+
For the smallest production-shaped MCP boundary, 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.
|
|
67
|
+
|
|
66
68
|
For your own tool server, copy the printed service env:
|
|
67
69
|
|
|
68
70
|
```bash
|
|
@@ -80,6 +82,14 @@ Call your wrapped tool with the printed dev token. Then view actions:
|
|
|
80
82
|
npx sello actions
|
|
81
83
|
```
|
|
82
84
|
|
|
85
|
+
In local dev, `sello dev` prints and saves the token:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
SELLO_ACTION_TOKEN=eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`npx sello actions` reads that token from `.sello/dev.json`. Pass `--token` only when you want to inspect receipts for a specific agent authorization token.
|
|
92
|
+
|
|
83
93
|
## Self-Hosted Production
|
|
84
94
|
|
|
85
95
|
Use your own Sello-compatible log server:
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalJsonBytes,
|
|
3
|
+
sello,
|
|
4
|
+
type SelloReceipts,
|
|
5
|
+
} from "../src/index.ts";
|
|
6
|
+
|
|
7
|
+
export type MinimalMcpRequest = {
|
|
8
|
+
headers: {
|
|
9
|
+
authorization?: string;
|
|
10
|
+
Authorization?: string;
|
|
11
|
+
};
|
|
12
|
+
body: {
|
|
13
|
+
jsonrpc: "2.0";
|
|
14
|
+
id: string | number | null;
|
|
15
|
+
method: "tools/call";
|
|
16
|
+
params: {
|
|
17
|
+
name: string;
|
|
18
|
+
arguments: Record<string, unknown>;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type MinimalMcpResponse = {
|
|
24
|
+
jsonrpc: "2.0";
|
|
25
|
+
id: string | number | null;
|
|
26
|
+
result?: {
|
|
27
|
+
content: { type: "text"; text: string }[];
|
|
28
|
+
};
|
|
29
|
+
error?: {
|
|
30
|
+
code: number;
|
|
31
|
+
message: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function createCalendarMcpServer(
|
|
36
|
+
receipts: SelloReceipts = sello.service(),
|
|
37
|
+
) {
|
|
38
|
+
const createEvent = receipts.tool<MinimalMcpRequest, MinimalMcpResponse>(
|
|
39
|
+
"mcp.tools/call.calendar.create_event",
|
|
40
|
+
async (request) => {
|
|
41
|
+
const args = request.body.params.arguments;
|
|
42
|
+
const title = readString(args.title, "title");
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
jsonrpc: "2.0",
|
|
46
|
+
id: request.body.id,
|
|
47
|
+
result: {
|
|
48
|
+
content: [
|
|
49
|
+
{
|
|
50
|
+
type: "text",
|
|
51
|
+
text: `created ${title}`,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
canonicalizeInput: (request) => canonicalJsonBytes({
|
|
59
|
+
method: request.body.method,
|
|
60
|
+
params: request.body.params,
|
|
61
|
+
}),
|
|
62
|
+
canonicalizeOutput: (response) => canonicalJsonBytes(response),
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
async handle(request: MinimalMcpRequest): Promise<MinimalMcpResponse> {
|
|
68
|
+
if (
|
|
69
|
+
request.body.method !== "tools/call" ||
|
|
70
|
+
request.body.params.name !== "calendar.create_event"
|
|
71
|
+
) {
|
|
72
|
+
return {
|
|
73
|
+
jsonrpc: "2.0",
|
|
74
|
+
id: request.body.id,
|
|
75
|
+
error: {
|
|
76
|
+
code: -32601,
|
|
77
|
+
message: "tool not found",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return await createEvent(request);
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
flush: () => receipts.flush(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readString(value: unknown, name: string): string {
|
|
90
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
91
|
+
throw new TypeError(`${name} must be a non-empty string`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return value;
|
|
95
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sello",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"bench": "node --experimental-strip-types src/cli/bench.ts",
|
|
32
32
|
"demo": "node --experimental-strip-types src/cli/demo.ts",
|
|
33
33
|
"dev": "node --experimental-strip-types src/cli/sello.ts dev",
|
|
34
|
+
"example:mcp:minimal": "node --test --experimental-strip-types test/examples/mcp-minimal-server.test.ts",
|
|
34
35
|
"example:mcp": "node --experimental-strip-types examples/mcp-tool-server.ts",
|
|
35
36
|
"example:tool": "node --experimental-strip-types examples/quickstart-tool.ts",
|
|
36
37
|
"build": "node --disable-warning=ExperimentalWarning scripts/build-dist.mjs",
|
package/src/cli/sello.ts
CHANGED
|
@@ -47,6 +47,25 @@ type DevState = {
|
|
|
47
47
|
registryJson: string;
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
type ActionsViewModel = {
|
|
51
|
+
receipts: {
|
|
52
|
+
integratedTime: string;
|
|
53
|
+
serviceIdentifier: string;
|
|
54
|
+
actionType: string;
|
|
55
|
+
resultStatus: string;
|
|
56
|
+
status: string;
|
|
57
|
+
logUrl: string;
|
|
58
|
+
logCompleteness: string;
|
|
59
|
+
sameSecondActivity: boolean;
|
|
60
|
+
}[];
|
|
61
|
+
rejected: {
|
|
62
|
+
code: string;
|
|
63
|
+
message: string;
|
|
64
|
+
logUrl?: string;
|
|
65
|
+
integratedTime?: string;
|
|
66
|
+
}[];
|
|
67
|
+
};
|
|
68
|
+
|
|
50
69
|
const textEncoder = new TextEncoder();
|
|
51
70
|
const textDecoder = new TextDecoder();
|
|
52
71
|
const command = process.argv[2] ?? "help";
|
|
@@ -286,16 +305,17 @@ async function handleDevRequest(input: {
|
|
|
286
305
|
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
|
|
287
306
|
|
|
288
307
|
if (request.method === "GET" && (url.pathname === "/" || url.pathname === "/actions")) {
|
|
289
|
-
const result =
|
|
290
|
-
authorizationTokenBytes: textEncoder.encode(state.agentToken),
|
|
291
|
-
trustedLogs: [log],
|
|
292
|
-
registry,
|
|
293
|
-
ownerPrivateKey: normalizeHpkePrivateKey(state.ownerKey),
|
|
294
|
-
});
|
|
308
|
+
const result = verifyDevActions({ log, state, registry });
|
|
295
309
|
sendHtml(response, renderActionsHtml(result));
|
|
296
310
|
return;
|
|
297
311
|
}
|
|
298
312
|
|
|
313
|
+
if (request.method === "GET" && url.pathname === "/api/actions") {
|
|
314
|
+
const result = verifyDevActions({ log, state, registry });
|
|
315
|
+
sendJson(response, 200, actionsViewModel(result));
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
299
319
|
if (request.method === "POST" && url.pathname === "/api/entries") {
|
|
300
320
|
const body = await readJson(request);
|
|
301
321
|
if (!isRecord(body) || typeof body.envelope !== "string") {
|
|
@@ -327,6 +347,19 @@ async function handleDevRequest(input: {
|
|
|
327
347
|
sendJson(response, 404, { error: "not found" });
|
|
328
348
|
}
|
|
329
349
|
|
|
350
|
+
function verifyDevActions(input: {
|
|
351
|
+
log: MockTransparencyLog;
|
|
352
|
+
state: DevState;
|
|
353
|
+
registry: ReturnType<typeof parseRegistry>;
|
|
354
|
+
}): ReturnType<typeof verifyReceipts> {
|
|
355
|
+
return verifyReceipts({
|
|
356
|
+
authorizationTokenBytes: textEncoder.encode(input.state.agentToken),
|
|
357
|
+
trustedLogs: [input.log],
|
|
358
|
+
registry: input.registry,
|
|
359
|
+
ownerPrivateKey: normalizeHpkePrivateKey(input.state.ownerKey),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
330
363
|
async function loadViewerRegistry(devState: DevState | undefined) {
|
|
331
364
|
if (process.env.SELLO_REGISTRY_PATH) {
|
|
332
365
|
return parseRegistry(readFileBytes(process.env.SELLO_REGISTRY_PATH));
|
|
@@ -462,16 +495,38 @@ function printActions(result: ReturnType<typeof verifyReceipts>): void {
|
|
|
462
495
|
}
|
|
463
496
|
}
|
|
464
497
|
|
|
498
|
+
function actionsViewModel(result: ReturnType<typeof verifyReceipts>): ActionsViewModel {
|
|
499
|
+
return {
|
|
500
|
+
receipts: result.receipts.map((record) => ({
|
|
501
|
+
integratedTime: record.integratedTime,
|
|
502
|
+
serviceIdentifier: record.serviceIdentifier,
|
|
503
|
+
actionType: record.receipt["action-type"],
|
|
504
|
+
resultStatus: record.receipt["result-status"],
|
|
505
|
+
status: record.status,
|
|
506
|
+
logUrl: record.logUrl,
|
|
507
|
+
logCompleteness: record.logCompleteness,
|
|
508
|
+
sameSecondActivity: record.sameSecondActivity,
|
|
509
|
+
})),
|
|
510
|
+
rejected: result.rejected.map((record) => ({
|
|
511
|
+
code: record.code,
|
|
512
|
+
message: record.message,
|
|
513
|
+
...(record.logUrl === undefined ? {} : { logUrl: record.logUrl }),
|
|
514
|
+
...(record.integratedTime === undefined ? {} : { integratedTime: record.integratedTime }),
|
|
515
|
+
})),
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
465
519
|
function renderActionsHtml(result: ReturnType<typeof verifyReceipts>): string {
|
|
466
|
-
const
|
|
520
|
+
const view = actionsViewModel(result);
|
|
521
|
+
const rows = view.receipts.map((record) => `
|
|
467
522
|
<tr>
|
|
468
523
|
<td>${escapeHtml(record.integratedTime)}</td>
|
|
469
524
|
<td>${escapeHtml(record.serviceIdentifier)}</td>
|
|
470
|
-
<td>${escapeHtml(record.
|
|
471
|
-
<td>${escapeHtml(record.
|
|
525
|
+
<td>${escapeHtml(record.actionType)}</td>
|
|
526
|
+
<td>${escapeHtml(record.resultStatus)}</td>
|
|
472
527
|
<td>${escapeHtml(record.status)}</td>
|
|
473
528
|
</tr>`).join("");
|
|
474
|
-
const rejected =
|
|
529
|
+
const rejected = view.rejected.map((record) => `
|
|
475
530
|
<li><strong>${escapeHtml(record.code)}</strong>: ${escapeHtml(record.message)}</li>`).join("");
|
|
476
531
|
|
|
477
532
|
return `<!doctype html>
|