sello 0.1.1 → 0.1.2
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/dist/cbor.js +337 -0
- package/dist/cli/bench.js +389 -0
- package/dist/cli/demo.js +113 -0
- package/dist/cli/sello.js +515 -0
- package/dist/cose/protected-header.js +210 -0
- package/dist/cose/sign1.js +124 -0
- package/dist/crypto/ed25519.js +117 -0
- package/dist/crypto/identifiers.js +64 -0
- package/dist/hpke/base.js +349 -0
- package/dist/hpke/receipt.js +79 -0
- package/dist/index.js +15 -0
- package/dist/log/canonical-url.js +168 -0
- package/dist/log/mock-log.js +147 -0
- package/dist/log/rekor.js +120 -0
- package/dist/log/types.js +0 -0
- package/dist/mcp/middleware.js +162 -0
- package/dist/owner/verify.js +271 -0
- package/dist/receipt/body.js +210 -0
- package/dist/registry/json-registry.js +233 -0
- package/dist/sdk/index.js +22 -0
- package/dist/sdk/keys.js +191 -0
- package/dist/sdk/logs.js +196 -0
- package/dist/sdk/publisher.js +106 -0
- package/dist/sdk/service.js +561 -0
- package/dist/service/create-receipt.js +174 -0
- package/dist/token/jws-profile.js +174 -0
- package/docs/release-checklist.md +1 -0
- package/package.json +9 -5
package/dist/cli/demo.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { encodeCbor } from "../cbor.js";
|
|
4
|
+
import { decodeReceiptEnvelope, generateEd25519KeyPair } from "../cose/sign1.js";
|
|
5
|
+
import { toHex } from "../crypto/identifiers.js";
|
|
6
|
+
import { generateHpkeKeyPair } from "../hpke/receipt.js";
|
|
7
|
+
import { MockTransparencyLog } from "../log/mock-log.js";
|
|
8
|
+
import { verifyReceipts } from "../owner/verify.js";
|
|
9
|
+
import {
|
|
10
|
+
loadSignedRegistry,
|
|
11
|
+
signRegistryJson,
|
|
12
|
+
} from "../registry/json-registry.js";
|
|
13
|
+
import { createReceiptFromJwsToken } from "../service/create-receipt.js";
|
|
14
|
+
import { base64urlEncode, signSelloJwsToken } from "../token/jws-profile.js";
|
|
15
|
+
|
|
16
|
+
const textEncoder = new TextEncoder();
|
|
17
|
+
const logUrl = "https://rekor.example.com/api" ;
|
|
18
|
+
const serviceIdentifier = "github.com/mcp/v1";
|
|
19
|
+
|
|
20
|
+
const owner = generateHpkeKeyPair();
|
|
21
|
+
const service = generateEd25519KeyPair();
|
|
22
|
+
const trustRoot = generateEd25519KeyPair();
|
|
23
|
+
const tokenIssuer = generateEd25519KeyPair();
|
|
24
|
+
const serviceKid = textEncoder.encode("github-mcp-v1-2026-q2");
|
|
25
|
+
const log = new MockTransparencyLog(logUrl);
|
|
26
|
+
const authorizationToken = signSelloJwsToken({
|
|
27
|
+
issuerPrivateKey: tokenIssuer.privateKey,
|
|
28
|
+
payload: {
|
|
29
|
+
sub: "demo-agent",
|
|
30
|
+
owner_hpke_pk: base64urlEncode(owner.publicKey),
|
|
31
|
+
sello_logs: [logUrl],
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const registryBytes = textEncoder.encode(
|
|
35
|
+
JSON.stringify({
|
|
36
|
+
[toHex(serviceKid)]: {
|
|
37
|
+
service_identifier: serviceIdentifier,
|
|
38
|
+
public_key_ed25519: Buffer.from(service.publicKey).toString("base64url"),
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
const registry = loadSignedRegistry({
|
|
43
|
+
registryBytes,
|
|
44
|
+
signatureBase64Url: signRegistryJson(registryBytes, trustRoot.privateKey),
|
|
45
|
+
trustRootPublicKey: trustRoot.publicKey,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const created = [
|
|
49
|
+
createDemoReceipt("success", "2026-05-28T10:00:00Z", "issue created"),
|
|
50
|
+
createDemoReceipt("error", "2026-05-28T10:00:01Z", "service error"),
|
|
51
|
+
createDemoReceipt("denied", "2026-05-28T10:00:02Z", "ignored"),
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
if (process.argv.includes("--tamper")) {
|
|
55
|
+
const decoded = decodeReceiptEnvelope(created[0].envelope);
|
|
56
|
+
log.append(
|
|
57
|
+
encodeCbor([
|
|
58
|
+
decoded.protectedBytes,
|
|
59
|
+
new Map(),
|
|
60
|
+
textEncoder.encode("tampered payload"),
|
|
61
|
+
decoded.signature,
|
|
62
|
+
]),
|
|
63
|
+
"2026-05-28T10:00:03Z",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const result = verifyReceipts({
|
|
68
|
+
authorizationTokenBytes: textEncoder.encode(authorizationToken),
|
|
69
|
+
trustedLogs: [log],
|
|
70
|
+
registry,
|
|
71
|
+
ownerPrivateKey: owner.privateKey,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
console.log(
|
|
75
|
+
JSON.stringify(
|
|
76
|
+
{
|
|
77
|
+
receipts: result.receipts.map((record) => ({
|
|
78
|
+
service: record.serviceIdentifier,
|
|
79
|
+
"action-type": record.receipt["action-type"],
|
|
80
|
+
"result-status": record.receipt["result-status"],
|
|
81
|
+
timestamp: record.receipt.timestamp,
|
|
82
|
+
verified: record.status === "valid",
|
|
83
|
+
status: record.status,
|
|
84
|
+
})),
|
|
85
|
+
rejected: result.rejected.map((record) => ({
|
|
86
|
+
code: record.code,
|
|
87
|
+
message: record.message,
|
|
88
|
+
})),
|
|
89
|
+
},
|
|
90
|
+
null,
|
|
91
|
+
2,
|
|
92
|
+
),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
function createDemoReceipt(
|
|
96
|
+
resultStatus ,
|
|
97
|
+
timestamp ,
|
|
98
|
+
outputText ,
|
|
99
|
+
) {
|
|
100
|
+
return createReceiptFromJwsToken({
|
|
101
|
+
authorizationToken,
|
|
102
|
+
tokenIssuerPublicKey: tokenIssuer.publicKey,
|
|
103
|
+
serviceKid,
|
|
104
|
+
servicePrivateKey: service.privateKey,
|
|
105
|
+
serviceIdentifier,
|
|
106
|
+
log,
|
|
107
|
+
actionType: "tools/call",
|
|
108
|
+
actionInputBytes: textEncoder.encode(`demo ${resultStatus} input`),
|
|
109
|
+
actionOutputBytes: textEncoder.encode(outputText),
|
|
110
|
+
resultStatus,
|
|
111
|
+
timestamp,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createServer, } from "node:http";
|
|
4
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
|
|
7
|
+
import { generateEd25519KeyPair } from "../cose/sign1.js";
|
|
8
|
+
import { deriveTokenIdentifiers, sha256, toHex } from "../crypto/identifiers.js";
|
|
9
|
+
import { generateHpkeKeyPair } from "../hpke/receipt.js";
|
|
10
|
+
import { MockTransparencyLog } from "../log/mock-log.js";
|
|
11
|
+
import { verifyReceipts } from "../owner/verify.js";
|
|
12
|
+
import {
|
|
13
|
+
loadSignedRegistry,
|
|
14
|
+
parseRegistry,
|
|
15
|
+
signRegistryJson,
|
|
16
|
+
} from "../registry/json-registry.js";
|
|
17
|
+
import { base64urlEncode as tokenBase64urlEncode, signSelloJwsToken } from "../token/jws-profile.js";
|
|
18
|
+
import {
|
|
19
|
+
base64urlEncode,
|
|
20
|
+
decodeBase64url,
|
|
21
|
+
encodeOwnerKey,
|
|
22
|
+
encodeServiceKey,
|
|
23
|
+
normalizeEd25519PublicKey,
|
|
24
|
+
normalizeHpkePrivateKey,
|
|
25
|
+
} from "../sdk/keys.js";
|
|
26
|
+
import {
|
|
27
|
+
deserializeEntry,
|
|
28
|
+
queryHttpLogByTokenRef,
|
|
29
|
+
serializeEntry,
|
|
30
|
+
toCanonicalLogUrl,
|
|
31
|
+
} from "../sdk/logs.js";
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
const textEncoder = new TextEncoder();
|
|
47
|
+
const textDecoder = new TextDecoder();
|
|
48
|
+
const command = process.argv[2] ?? "help";
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
enforceNodeVersion();
|
|
52
|
+
switch (command) {
|
|
53
|
+
case "actions":
|
|
54
|
+
await actionsCommand(process.argv.slice(3));
|
|
55
|
+
break;
|
|
56
|
+
case "dev":
|
|
57
|
+
await devCommand(process.argv.slice(3));
|
|
58
|
+
break;
|
|
59
|
+
case "keys":
|
|
60
|
+
keysCommand(process.argv.slice(3));
|
|
61
|
+
break;
|
|
62
|
+
case "inspect-env":
|
|
63
|
+
inspectEnvCommand();
|
|
64
|
+
break;
|
|
65
|
+
case "help":
|
|
66
|
+
case "--help":
|
|
67
|
+
case "-h":
|
|
68
|
+
printHelp();
|
|
69
|
+
break;
|
|
70
|
+
default:
|
|
71
|
+
throw new TypeError(`unknown command ${command}`);
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
75
|
+
console.error(`sello: ${message}`);
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function actionsCommand(args ) {
|
|
80
|
+
const devState = loadDevStateIfPresent();
|
|
81
|
+
const token = readFlag(args, "--token") ?? process.env.SELLO_ACTION_TOKEN ?? devState?.agentToken;
|
|
82
|
+
if (!token) {
|
|
83
|
+
throw new TypeError("missing token. Pass --token <agent-token> or run sello dev first.");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const ownerKeyInput = process.env.SELLO_OWNER_KEY ?? devState?.ownerKey;
|
|
87
|
+
if (!ownerKeyInput) {
|
|
88
|
+
throw new TypeError("missing SELLO_OWNER_KEY. Viewing actions requires the owner private key.");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const logUrl = process.env.SELLO_LOG_URL ?? devState?.logUrl;
|
|
92
|
+
const endpoint = process.env.SELLO_LOG_ENDPOINT ?? devState?.logEndpoint ?? logUrl;
|
|
93
|
+
if (!logUrl || !endpoint) {
|
|
94
|
+
throw new TypeError("missing SELLO_LOG_URL. Configure a trusted log before viewing actions.");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const registry = await loadViewerRegistry(devState);
|
|
98
|
+
const tokenBytes = textEncoder.encode(token);
|
|
99
|
+
const identifiers = deriveTokenIdentifiers(tokenBytes);
|
|
100
|
+
const query = await queryHttpLogByTokenRef({
|
|
101
|
+
endpoint,
|
|
102
|
+
tokenRef: identifiers.sello_token_ref,
|
|
103
|
+
});
|
|
104
|
+
const log = {
|
|
105
|
+
logUrl: toCanonicalLogUrl(logUrl),
|
|
106
|
+
queryByTokenRef: () => query,
|
|
107
|
+
verifyInclusionProof: verifyHttpProof,
|
|
108
|
+
};
|
|
109
|
+
const result = verifyReceipts({
|
|
110
|
+
authorizationTokenBytes: tokenBytes,
|
|
111
|
+
trustedLogs: [log],
|
|
112
|
+
registry,
|
|
113
|
+
ownerPrivateKey: normalizeHpkePrivateKey(ownerKeyInput, "SELLO_OWNER_KEY"),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
printActions(result);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function devCommand(args ) {
|
|
120
|
+
const port = Number(readFlag(args, "--port") ?? process.env.PORT ?? "8787");
|
|
121
|
+
const dryRun = args.includes("--dry-run");
|
|
122
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
|
|
123
|
+
throw new TypeError("port must be between 1 and 65535");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const serviceId =
|
|
127
|
+
readFlag(args, "--service") ??
|
|
128
|
+
process.env.SELLO_SERVICE_ID ??
|
|
129
|
+
"calendar.example.com/mcp/v1";
|
|
130
|
+
const logEndpoint = `http://localhost:${port}/api`;
|
|
131
|
+
const logUrl = toCanonicalLogUrl(`http://localhost:${port}/api`);
|
|
132
|
+
const state = createDevState({ serviceId, logUrl, logEndpoint });
|
|
133
|
+
saveDevState(state);
|
|
134
|
+
|
|
135
|
+
if (dryRun) {
|
|
136
|
+
printDevConfig(port, state);
|
|
137
|
+
console.log("");
|
|
138
|
+
console.log("Dry run: dev state written, server not started.");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const log = new MockTransparencyLog(logUrl);
|
|
143
|
+
const registry = parseRegistry(textEncoder.encode(state.registryJson));
|
|
144
|
+
const server = createServer(async (request, response) => {
|
|
145
|
+
try {
|
|
146
|
+
await handleDevRequest({ request, response, log, state, registry });
|
|
147
|
+
} catch (error) {
|
|
148
|
+
sendJson(response, 500, {
|
|
149
|
+
error: error instanceof Error ? error.message : String(error),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
server.listen(port, () => {
|
|
155
|
+
printDevConfig(port, state);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function keysCommand(args ) {
|
|
160
|
+
const subcommand = args[0] ?? "service";
|
|
161
|
+
if (subcommand !== "service") {
|
|
162
|
+
throw new TypeError("only `sello keys service` is supported");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const key = generateEd25519KeyPair();
|
|
166
|
+
const kid = textEncoder.encode(`svc-${Date.now().toString(36)}`);
|
|
167
|
+
console.log(`SELLO_SERVICE_KEY=${encodeServiceKey(kid, key.privateKey)}`);
|
|
168
|
+
console.log(`SELLO_SERVICE_PUBLIC_KEY=${base64urlEncode(key.publicKey)}`);
|
|
169
|
+
console.log(`SELLO_SERVICE_KID=${textDecoder.decode(kid)}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function inspectEnvCommand() {
|
|
173
|
+
const keys = [
|
|
174
|
+
"SELLO_SERVICE_ID",
|
|
175
|
+
"SELLO_SERVICE_KEY",
|
|
176
|
+
"SELLO_TOKEN_ISSUER_PUBLIC_KEY",
|
|
177
|
+
"SELLO_TOKEN_ISSUER_JWKS",
|
|
178
|
+
"SELLO_LOG_URL",
|
|
179
|
+
"SELLO_LOG_ENDPOINT",
|
|
180
|
+
"SELLO_SUBMIT_MODE",
|
|
181
|
+
"SELLO_SECRET_KEY",
|
|
182
|
+
"SELLO_OWNER_KEY",
|
|
183
|
+
"SELLO_REGISTRY_URL",
|
|
184
|
+
"SELLO_REGISTRY_PATH",
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
for (const key of keys) {
|
|
188
|
+
const value = process.env[key];
|
|
189
|
+
if (!value) {
|
|
190
|
+
console.log(`${key}=<unset>`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const sensitive = /KEY|SECRET/.test(key);
|
|
195
|
+
console.log(`${key}=${sensitive ? redact(value) : value}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function handleDevRequest(input
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
) {
|
|
206
|
+
const { request, response, log, state, registry } = input;
|
|
207
|
+
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
|
|
208
|
+
|
|
209
|
+
if (request.method === "GET" && (url.pathname === "/" || url.pathname === "/actions")) {
|
|
210
|
+
const result = verifyReceipts({
|
|
211
|
+
authorizationTokenBytes: textEncoder.encode(state.agentToken),
|
|
212
|
+
trustedLogs: [log],
|
|
213
|
+
registry,
|
|
214
|
+
ownerPrivateKey: normalizeHpkePrivateKey(state.ownerKey),
|
|
215
|
+
});
|
|
216
|
+
sendHtml(response, renderActionsHtml(result));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (request.method === "POST" && url.pathname === "/api/entries") {
|
|
221
|
+
const body = await readJson(request);
|
|
222
|
+
if (!isRecord(body) || typeof body.envelope !== "string") {
|
|
223
|
+
throw new TypeError("append body must contain envelope");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const entry = log.append(
|
|
227
|
+
decodeBase64url(body.envelope, "envelope"),
|
|
228
|
+
typeof body.integratedTime === "string" ? body.integratedTime : undefined,
|
|
229
|
+
);
|
|
230
|
+
sendJson(response, 200, serializeEntry(entry));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (request.method === "GET" && url.pathname === "/api/entries") {
|
|
235
|
+
const tokenRefHex = url.searchParams.get("sello_token_ref");
|
|
236
|
+
if (!tokenRefHex || !/^[0-9a-f]{64}$/.test(tokenRefHex)) {
|
|
237
|
+
throw new TypeError("sello_token_ref query must be 64 lowercase hex characters");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const query = log.queryByTokenRef(Uint8Array.from(Buffer.from(tokenRefHex, "hex")));
|
|
241
|
+
sendJson(response, 200, {
|
|
242
|
+
completeness: query.completeness,
|
|
243
|
+
entries: query.entries.map(serializeEntry),
|
|
244
|
+
});
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
sendJson(response, 404, { error: "not found" });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function loadViewerRegistry(devState ) {
|
|
252
|
+
if (process.env.SELLO_REGISTRY_PATH) {
|
|
253
|
+
return parseRegistry(readFileBytes(process.env.SELLO_REGISTRY_PATH));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (process.env.SELLO_REGISTRY_URL) {
|
|
257
|
+
const response = await fetch(process.env.SELLO_REGISTRY_URL);
|
|
258
|
+
if (!response.ok) {
|
|
259
|
+
throw new TypeError(`registry fetch failed with HTTP ${response.status}`);
|
|
260
|
+
}
|
|
261
|
+
const registryBytes = new Uint8Array(await response.arrayBuffer());
|
|
262
|
+
const signature = process.env.SELLO_REGISTRY_SIGNATURE;
|
|
263
|
+
const trustRoot = process.env.SELLO_REGISTRY_TRUST_ROOT_PUBLIC_KEY;
|
|
264
|
+
if (!signature || !trustRoot) {
|
|
265
|
+
throw new TypeError(
|
|
266
|
+
"SELLO_REGISTRY_URL requires SELLO_REGISTRY_SIGNATURE and SELLO_REGISTRY_TRUST_ROOT_PUBLIC_KEY",
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return loadSignedRegistry({
|
|
271
|
+
registryBytes,
|
|
272
|
+
signatureBase64Url: signature,
|
|
273
|
+
trustRootPublicKey: normalizeEd25519PublicKey(trustRoot, "SELLO_REGISTRY_TRUST_ROOT_PUBLIC_KEY"),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (devState) {
|
|
278
|
+
return parseRegistry(textEncoder.encode(devState.registryJson));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
throw new TypeError(
|
|
282
|
+
"missing registry. Set SELLO_REGISTRY_PATH or SELLO_REGISTRY_URL.",
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function createDevState(input
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
) {
|
|
291
|
+
const owner = generateHpkeKeyPair();
|
|
292
|
+
const service = generateEd25519KeyPair();
|
|
293
|
+
const tokenIssuer = generateEd25519KeyPair();
|
|
294
|
+
const trustRoot = generateEd25519KeyPair();
|
|
295
|
+
const kid = textEncoder.encode("dev-service-key");
|
|
296
|
+
const agentToken = signSelloJwsToken({
|
|
297
|
+
issuerPrivateKey: tokenIssuer.privateKey,
|
|
298
|
+
payload: {
|
|
299
|
+
sub: "sello-dev-agent",
|
|
300
|
+
owner_hpke_pk: tokenBase64urlEncode(owner.publicKey),
|
|
301
|
+
sello_logs: [input.logUrl],
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
const registryJson = JSON.stringify({
|
|
305
|
+
[toHex(kid)]: {
|
|
306
|
+
service_identifier: input.serviceId,
|
|
307
|
+
public_key_ed25519: base64urlEncode(service.publicKey),
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
signRegistryJson(textEncoder.encode(registryJson), trustRoot.privateKey);
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
serviceId: input.serviceId,
|
|
315
|
+
serviceKey: encodeServiceKey(kid, service.privateKey),
|
|
316
|
+
servicePublicKey: base64urlEncode(service.publicKey),
|
|
317
|
+
ownerKey: encodeOwnerKey(owner.privateKey),
|
|
318
|
+
ownerPublicKey: base64urlEncode(owner.publicKey),
|
|
319
|
+
tokenIssuerPublicKey: base64urlEncode(tokenIssuer.publicKey),
|
|
320
|
+
agentToken,
|
|
321
|
+
logUrl: input.logUrl,
|
|
322
|
+
logEndpoint: input.logEndpoint,
|
|
323
|
+
registryJson,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function saveDevState(state ) {
|
|
328
|
+
const path = devStatePath();
|
|
329
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
330
|
+
writeFileSync(path, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function loadDevStateIfPresent() {
|
|
334
|
+
try {
|
|
335
|
+
return JSON.parse(textDecoder.decode(readFileBytes(devStatePath()))) ;
|
|
336
|
+
} catch {
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function devStatePath() {
|
|
342
|
+
return join(process.cwd(), ".sello", "dev.json");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function readFileBytes(path ) {
|
|
346
|
+
return new Uint8Array(readFileSync(path));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function printActions(result ) {
|
|
350
|
+
console.log("Verified agent actions");
|
|
351
|
+
console.log("");
|
|
352
|
+
|
|
353
|
+
if (result.receipts.length === 0) {
|
|
354
|
+
console.log("No verified actions found.");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const record of result.receipts) {
|
|
358
|
+
console.log(
|
|
359
|
+
[
|
|
360
|
+
record.integratedTime.padEnd(21),
|
|
361
|
+
record.serviceIdentifier.padEnd(30),
|
|
362
|
+
record.receipt["action-type"].padEnd(28),
|
|
363
|
+
record.receipt["result-status"],
|
|
364
|
+
record.status === "duplicate" ? "(duplicate)" : "",
|
|
365
|
+
].filter(Boolean).join(" "),
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (result.rejected.length > 0) {
|
|
370
|
+
console.log("");
|
|
371
|
+
console.log("Rejected receipts");
|
|
372
|
+
for (const rejected of result.rejected) {
|
|
373
|
+
console.log(`${rejected.code}: ${rejected.message}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function renderActionsHtml(result ) {
|
|
379
|
+
const rows = result.receipts.map((record) => `
|
|
380
|
+
<tr>
|
|
381
|
+
<td>${escapeHtml(record.integratedTime)}</td>
|
|
382
|
+
<td>${escapeHtml(record.serviceIdentifier)}</td>
|
|
383
|
+
<td>${escapeHtml(record.receipt["action-type"])}</td>
|
|
384
|
+
<td>${escapeHtml(record.receipt["result-status"])}</td>
|
|
385
|
+
<td>${escapeHtml(record.status)}</td>
|
|
386
|
+
</tr>`).join("");
|
|
387
|
+
const rejected = result.rejected.map((record) => `
|
|
388
|
+
<li><strong>${escapeHtml(record.code)}</strong>: ${escapeHtml(record.message)}</li>`).join("");
|
|
389
|
+
|
|
390
|
+
return `<!doctype html>
|
|
391
|
+
<html lang="en">
|
|
392
|
+
<head>
|
|
393
|
+
<meta charset="utf-8">
|
|
394
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
395
|
+
<title>Sello Actions</title>
|
|
396
|
+
<style>
|
|
397
|
+
body { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 40px; color: #17201d; }
|
|
398
|
+
table { border-collapse: collapse; width: 100%; margin-top: 16px; }
|
|
399
|
+
th, td { border-bottom: 1px solid #d8dfdc; padding: 10px 8px; text-align: left; }
|
|
400
|
+
th { color: #52615b; font-weight: 600; }
|
|
401
|
+
.empty { color: #52615b; margin-top: 16px; }
|
|
402
|
+
</style>
|
|
403
|
+
</head>
|
|
404
|
+
<body>
|
|
405
|
+
<h1>Sello Actions</h1>
|
|
406
|
+
${rows ? `<table><thead><tr><th>Integrated time</th><th>Service</th><th>Action</th><th>Result</th><th>Status</th></tr></thead><tbody>${rows}</tbody></table>` : `<p class="empty">No verified actions found yet.</p>`}
|
|
407
|
+
${rejected ? `<h2>Rejected receipts</h2><ul>${rejected}</ul>` : ""}
|
|
408
|
+
</body>
|
|
409
|
+
</html>`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function verifyHttpProof(entry ) {
|
|
413
|
+
if (!isRecord(entry.proof)) {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const envelopeHash = toHex(sha256(entry.envelope));
|
|
418
|
+
const proofHash = toHex(
|
|
419
|
+
sha256(
|
|
420
|
+
textEncoder.encode(
|
|
421
|
+
`${entry.logUrl}\n${entry.index}\n${entry.integratedTime}\n${envelopeHash}`,
|
|
422
|
+
),
|
|
423
|
+
),
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
return (
|
|
427
|
+
entry.proof.logUrl === entry.logUrl &&
|
|
428
|
+
entry.proof.index === entry.index &&
|
|
429
|
+
entry.proof.integratedTime === entry.integratedTime &&
|
|
430
|
+
entry.proof.envelopeHash === envelopeHash &&
|
|
431
|
+
entry.proof.proofHash === proofHash
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function readFlag(args , name ) {
|
|
436
|
+
const index = args.indexOf(name);
|
|
437
|
+
return index === -1 ? undefined : args[index + 1];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function readJson(request ) {
|
|
441
|
+
const chunks = [];
|
|
442
|
+
for await (const chunk of request) {
|
|
443
|
+
chunks.push(typeof chunk === "string" ? textEncoder.encode(chunk) : chunk);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function sendJson(response , statusCode , body ) {
|
|
450
|
+
response.writeHead(statusCode, { "content-type": "application/json" });
|
|
451
|
+
response.end(`${JSON.stringify(body)}\n`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function sendHtml(response , body ) {
|
|
455
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
456
|
+
response.end(body);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function redact(value ) {
|
|
460
|
+
return value.length <= 8 ? "<set>" : `${value.slice(0, 6)}...${value.slice(-4)}`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function escapeHtml(value ) {
|
|
464
|
+
return value.replace(/[&<>"']/g, (char) => ({
|
|
465
|
+
"&": "&",
|
|
466
|
+
"<": "<",
|
|
467
|
+
">": ">",
|
|
468
|
+
"\"": """,
|
|
469
|
+
"'": "'",
|
|
470
|
+
}[char] ));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function isRecord(value ) {
|
|
474
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function printHelp() {
|
|
478
|
+
console.log(`Usage:
|
|
479
|
+
sello dev [--port 8787] [--service service-id] [--dry-run]
|
|
480
|
+
sello actions [--token agent-token]
|
|
481
|
+
sello keys service
|
|
482
|
+
sello inspect-env
|
|
483
|
+
`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function printDevConfig(port , state ) {
|
|
487
|
+
console.log(`Sello dev log running at http://localhost:${port}/actions`);
|
|
488
|
+
console.log("");
|
|
489
|
+
console.log("Service env:");
|
|
490
|
+
console.log(`SELLO_SERVICE_ID=${state.serviceId}`);
|
|
491
|
+
console.log(`SELLO_SERVICE_KEY=${state.serviceKey}`);
|
|
492
|
+
console.log(`SELLO_TOKEN_ISSUER_PUBLIC_KEY=${state.tokenIssuerPublicKey}`);
|
|
493
|
+
console.log(`SELLO_LOG_URL=${state.logUrl}`);
|
|
494
|
+
console.log(`SELLO_LOG_ENDPOINT=${state.logEndpoint}`);
|
|
495
|
+
console.log("SELLO_SUBMIT_MODE=background");
|
|
496
|
+
console.log("");
|
|
497
|
+
console.log("Viewer env:");
|
|
498
|
+
console.log(`SELLO_OWNER_KEY=${state.ownerKey}`);
|
|
499
|
+
console.log(`SELLO_LOG_URL=${state.logUrl}`);
|
|
500
|
+
console.log(`SELLO_LOG_ENDPOINT=${state.logEndpoint}`);
|
|
501
|
+
console.log("");
|
|
502
|
+
console.log("Dev token:");
|
|
503
|
+
console.log(`SELLO_ACTION_TOKEN=${state.agentToken}`);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function enforceNodeVersion() {
|
|
507
|
+
const [major = 0, minor = 0] = process.versions.node
|
|
508
|
+
.split(".")
|
|
509
|
+
.map((part) => Number(part));
|
|
510
|
+
if (major < 22 || (major === 22 && minor < 7)) {
|
|
511
|
+
throw new TypeError(
|
|
512
|
+
`Sello requires Node >=22.7.0; current Node is ${process.versions.node}`,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|