sello 0.1.0
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/LICENSE +200 -0
- package/README.md +195 -0
- package/SPEC.md +738 -0
- package/docs/assets/sello-banner.png +0 -0
- package/docs/assets/sello-social-preview.png +0 -0
- package/docs/decisions.md +79 -0
- package/docs/paper/notarized-agents.md +523 -0
- package/docs/paper/notarized-agents.pdf +0 -0
- package/docs/paper/notarized-agents.tex +1387 -0
- package/docs/paper/refs.bib +245 -0
- package/docs/performance.md +24 -0
- package/docs/release-checklist.md +56 -0
- package/docs/sdk-build-plan.md +214 -0
- package/docs/sdk-quickstart.md +115 -0
- package/docs/sdk-security-audit.md +53 -0
- package/docs/security-review.md +54 -0
- package/examples/mcp-tool-server.ts +250 -0
- package/examples/quickstart-tool.ts +178 -0
- package/fixtures/vectors/.gitkeep +1 -0
- package/fixtures/vectors/sello-v0.1.json +101 -0
- package/package.json +52 -0
- package/src/cbor.ts +337 -0
- package/src/cli/bench.ts +390 -0
- package/src/cli/demo.ts +114 -0
- package/src/cli/sello.ts +514 -0
- package/src/cose/protected-header.ts +210 -0
- package/src/cose/sign1.ts +124 -0
- package/src/crypto/ed25519.ts +117 -0
- package/src/crypto/identifiers.ts +64 -0
- package/src/hpke/base.ts +349 -0
- package/src/hpke/receipt.ts +79 -0
- package/src/index.ts +15 -0
- package/src/log/canonical-url.ts +168 -0
- package/src/log/mock-log.ts +170 -0
- package/src/log/rekor.ts +147 -0
- package/src/log/types.ts +27 -0
- package/src/mcp/middleware.ts +198 -0
- package/src/owner/verify.ts +276 -0
- package/src/receipt/body.ts +210 -0
- package/src/registry/json-registry.ts +233 -0
- package/src/sdk/index.ts +22 -0
- package/src/sdk/keys.ts +191 -0
- package/src/sdk/logs.ts +200 -0
- package/src/sdk/publisher.ts +145 -0
- package/src/sdk/service.ts +562 -0
- package/src/service/create-receipt.ts +178 -0
- package/src/token/jws-profile.ts +174 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { sha256, assertTokenRef, toHex } from "../crypto/identifiers.ts";
|
|
2
|
+
import { decodeProtectedHeader } from "../cose/protected-header.ts";
|
|
3
|
+
import { decodeReceiptEnvelope } from "../cose/sign1.ts";
|
|
4
|
+
import {
|
|
5
|
+
type CanonicalLogUrl,
|
|
6
|
+
assertCanonicalLogUrl,
|
|
7
|
+
logUrlsEqual,
|
|
8
|
+
} from "./canonical-url.ts";
|
|
9
|
+
import { type TransparencyLogQueryResult } from "./types.ts";
|
|
10
|
+
|
|
11
|
+
export type MockInclusionProof = {
|
|
12
|
+
logUrl: CanonicalLogUrl;
|
|
13
|
+
index: number;
|
|
14
|
+
integratedTime: string;
|
|
15
|
+
envelopeHash: string;
|
|
16
|
+
proofHash: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type MockLogEntry = {
|
|
20
|
+
logUrl: CanonicalLogUrl;
|
|
21
|
+
index: number;
|
|
22
|
+
integratedTime: string;
|
|
23
|
+
envelope: Uint8Array;
|
|
24
|
+
proof: MockInclusionProof;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type MockLogQueryResult = TransparencyLogQueryResult & {
|
|
28
|
+
completeness: "complete";
|
|
29
|
+
entries: MockLogEntry[];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export class MockTransparencyLog {
|
|
33
|
+
readonly logUrl: CanonicalLogUrl;
|
|
34
|
+
#entries: MockLogEntry[] = [];
|
|
35
|
+
#tokenIndex = new Map<string, number[]>();
|
|
36
|
+
|
|
37
|
+
constructor(logUrl: CanonicalLogUrl) {
|
|
38
|
+
assertCanonicalLogUrl(logUrl, "logUrl");
|
|
39
|
+
this.logUrl = logUrl;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
append(envelope: Uint8Array, integratedTime = nowUtcSeconds()): MockLogEntry {
|
|
43
|
+
assertBytes(envelope, "envelope");
|
|
44
|
+
assertUtcTimestamp(integratedTime, "integratedTime");
|
|
45
|
+
|
|
46
|
+
const decodedEnvelope = decodeReceiptEnvelope(envelope);
|
|
47
|
+
const protectedHeader = decodeProtectedHeader(decodedEnvelope.protectedBytes);
|
|
48
|
+
if (!logUrlsEqual(protectedHeader.sello_log_url, this.logUrl)) {
|
|
49
|
+
throw new TypeError("envelope sello_log_url must match mock log URL");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const index = this.#entries.length;
|
|
53
|
+
const proof = buildProof(this.logUrl, index, integratedTime, envelope);
|
|
54
|
+
const entry = freezeEntry({
|
|
55
|
+
logUrl: this.logUrl,
|
|
56
|
+
index,
|
|
57
|
+
integratedTime,
|
|
58
|
+
envelope,
|
|
59
|
+
proof,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
this.#entries.push(entry);
|
|
63
|
+
|
|
64
|
+
const tokenRefHex = toHex(protectedHeader.sello_token_ref);
|
|
65
|
+
const indexes = this.#tokenIndex.get(tokenRefHex) ?? [];
|
|
66
|
+
indexes.push(index);
|
|
67
|
+
this.#tokenIndex.set(tokenRefHex, indexes);
|
|
68
|
+
|
|
69
|
+
return cloneEntry(entry);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
queryByTokenRef(tokenRef: Uint8Array): MockLogQueryResult {
|
|
73
|
+
assertTokenRef(tokenRef, "tokenRef");
|
|
74
|
+
const indexes = this.#tokenIndex.get(toHex(tokenRef)) ?? [];
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
completeness: "complete",
|
|
78
|
+
entries: indexes.map((index) => cloneEntry(this.#entries[index])),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
verifyInclusionProof(entry: MockLogEntry): boolean {
|
|
83
|
+
try {
|
|
84
|
+
assertCanonicalLogUrl(entry.logUrl, "entry.logUrl");
|
|
85
|
+
if (!logUrlsEqual(entry.logUrl, this.logUrl)) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const expected = buildProof(
|
|
90
|
+
this.logUrl,
|
|
91
|
+
entry.index,
|
|
92
|
+
entry.integratedTime,
|
|
93
|
+
entry.envelope,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
entry.proof.logUrl === expected.logUrl &&
|
|
98
|
+
entry.proof.index === expected.index &&
|
|
99
|
+
entry.proof.integratedTime === expected.integratedTime &&
|
|
100
|
+
entry.proof.envelopeHash === expected.envelopeHash &&
|
|
101
|
+
entry.proof.proofHash === expected.proofHash
|
|
102
|
+
);
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildProof(
|
|
110
|
+
logUrl: CanonicalLogUrl,
|
|
111
|
+
index: number,
|
|
112
|
+
integratedTime: string,
|
|
113
|
+
envelope: Uint8Array,
|
|
114
|
+
): MockInclusionProof {
|
|
115
|
+
const envelopeHash = toHex(sha256(envelope));
|
|
116
|
+
const proofHash = toHex(
|
|
117
|
+
sha256(
|
|
118
|
+
new TextEncoder().encode(
|
|
119
|
+
`${logUrl}\n${index}\n${integratedTime}\n${envelopeHash}`,
|
|
120
|
+
),
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
logUrl,
|
|
126
|
+
index,
|
|
127
|
+
integratedTime,
|
|
128
|
+
envelopeHash,
|
|
129
|
+
proofHash,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function freezeEntry(entry: MockLogEntry): MockLogEntry {
|
|
134
|
+
return {
|
|
135
|
+
logUrl: entry.logUrl,
|
|
136
|
+
index: entry.index,
|
|
137
|
+
integratedTime: entry.integratedTime,
|
|
138
|
+
envelope: new Uint8Array(entry.envelope),
|
|
139
|
+
proof: { ...entry.proof },
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function cloneEntry(entry: MockLogEntry): MockLogEntry {
|
|
144
|
+
return {
|
|
145
|
+
logUrl: entry.logUrl,
|
|
146
|
+
index: entry.index,
|
|
147
|
+
integratedTime: entry.integratedTime,
|
|
148
|
+
envelope: new Uint8Array(entry.envelope),
|
|
149
|
+
proof: { ...entry.proof },
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function nowUtcSeconds(): string {
|
|
154
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function assertUtcTimestamp(value: string, name: string): void {
|
|
158
|
+
if (
|
|
159
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(value) ||
|
|
160
|
+
Number.isNaN(Date.parse(value))
|
|
161
|
+
) {
|
|
162
|
+
throw new TypeError(`${name} must be an RFC 3339 UTC timestamp`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function assertBytes(value: unknown, name: string): asserts value is Uint8Array {
|
|
167
|
+
if (!(value instanceof Uint8Array)) {
|
|
168
|
+
throw new TypeError(`${name} must be a Uint8Array`);
|
|
169
|
+
}
|
|
170
|
+
}
|
package/src/log/rekor.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { decodeProtectedHeader } from "../cose/protected-header.ts";
|
|
2
|
+
import { decodeReceiptEnvelope } from "../cose/sign1.ts";
|
|
3
|
+
import { assertTokenRef, toHex } from "../crypto/identifiers.ts";
|
|
4
|
+
import {
|
|
5
|
+
type CanonicalLogUrl,
|
|
6
|
+
assertCanonicalLogUrl,
|
|
7
|
+
logUrlsEqual,
|
|
8
|
+
} from "./canonical-url.ts";
|
|
9
|
+
import {
|
|
10
|
+
type TransparencyLogEntry,
|
|
11
|
+
type TransparencyLogQueryResult,
|
|
12
|
+
} from "./types.ts";
|
|
13
|
+
|
|
14
|
+
export type RekorProofVerifier = (entry: TransparencyLogEntry) => boolean;
|
|
15
|
+
|
|
16
|
+
export type RekorDiscoveredEntryInput = {
|
|
17
|
+
tokenRef?: Uint8Array;
|
|
18
|
+
logUrl?: CanonicalLogUrl;
|
|
19
|
+
index: number;
|
|
20
|
+
integratedTime: string;
|
|
21
|
+
envelope: Uint8Array;
|
|
22
|
+
proof: unknown;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type RekorDiscoveryLogInput = {
|
|
26
|
+
logUrl: CanonicalLogUrl;
|
|
27
|
+
entries?: readonly RekorDiscoveredEntryInput[];
|
|
28
|
+
verifyInclusionProof?: RekorProofVerifier;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type IndexedEntry = {
|
|
32
|
+
tokenRefHex: string;
|
|
33
|
+
entry: TransparencyLogEntry;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export class RekorDiscoveryLog {
|
|
37
|
+
readonly logUrl: CanonicalLogUrl;
|
|
38
|
+
#entries: IndexedEntry[] = [];
|
|
39
|
+
#verifyInclusionProof: RekorProofVerifier;
|
|
40
|
+
|
|
41
|
+
constructor(input: RekorDiscoveryLogInput) {
|
|
42
|
+
assertCanonicalLogUrl(input.logUrl, "logUrl");
|
|
43
|
+
this.logUrl = input.logUrl;
|
|
44
|
+
this.#verifyInclusionProof = input.verifyInclusionProof ?? (() => false);
|
|
45
|
+
|
|
46
|
+
for (const entry of input.entries ?? []) {
|
|
47
|
+
this.addDiscoveredEntry(entry);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
addDiscoveredEntry(input: RekorDiscoveredEntryInput): TransparencyLogEntry {
|
|
52
|
+
const entryLogUrl = input.logUrl ?? this.logUrl;
|
|
53
|
+
assertCanonicalLogUrl(entryLogUrl, "entry.logUrl");
|
|
54
|
+
assertUtcTimestamp(input.integratedTime, "integratedTime");
|
|
55
|
+
assertSafeIndex(input.index);
|
|
56
|
+
assertBytes(input.envelope, "envelope");
|
|
57
|
+
|
|
58
|
+
const tokenRefHex = input.tokenRef
|
|
59
|
+
? tokenRefToHex(input.tokenRef)
|
|
60
|
+
: tokenRefFromEnvelope(input.envelope);
|
|
61
|
+
|
|
62
|
+
const entry = cloneEntry({
|
|
63
|
+
logUrl: entryLogUrl,
|
|
64
|
+
index: input.index,
|
|
65
|
+
integratedTime: input.integratedTime,
|
|
66
|
+
envelope: input.envelope,
|
|
67
|
+
proof: input.proof,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
this.#entries.push({ tokenRefHex, entry });
|
|
71
|
+
return cloneEntry(entry);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
queryByTokenRef(tokenRef: Uint8Array): TransparencyLogQueryResult {
|
|
75
|
+
const tokenRefHex = tokenRefToHex(tokenRef);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
completeness: "discovery-only",
|
|
79
|
+
entries: this.#entries
|
|
80
|
+
.filter((indexed) => indexed.tokenRefHex === tokenRefHex)
|
|
81
|
+
.map((indexed) => cloneEntry(indexed.entry)),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
verifyInclusionProof(entry: TransparencyLogEntry): boolean {
|
|
86
|
+
try {
|
|
87
|
+
assertCanonicalLogUrl(entry.logUrl, "entry.logUrl");
|
|
88
|
+
if (!logUrlsEqual(entry.logUrl, this.logUrl)) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return this.#verifyInclusionProof(entry);
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function tokenRefFromEnvelope(envelope: Uint8Array): string {
|
|
100
|
+
const decodedEnvelope = decodeReceiptEnvelope(envelope);
|
|
101
|
+
const protectedHeader = decodeProtectedHeader(decodedEnvelope.protectedBytes);
|
|
102
|
+
return tokenRefToHex(protectedHeader.sello_token_ref);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function tokenRefToHex(tokenRef: Uint8Array): string {
|
|
106
|
+
assertTokenRef(tokenRef, "tokenRef");
|
|
107
|
+
return toHex(tokenRef);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function cloneEntry(entry: TransparencyLogEntry): TransparencyLogEntry {
|
|
111
|
+
return {
|
|
112
|
+
logUrl: entry.logUrl,
|
|
113
|
+
index: entry.index,
|
|
114
|
+
integratedTime: entry.integratedTime,
|
|
115
|
+
envelope: new Uint8Array(entry.envelope),
|
|
116
|
+
proof: cloneProof(entry.proof),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function cloneProof(proof: unknown): unknown {
|
|
121
|
+
if (proof === null || typeof proof !== "object") {
|
|
122
|
+
return proof;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return globalThis.structuredClone(proof);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function assertUtcTimestamp(value: string, name: string): void {
|
|
129
|
+
if (
|
|
130
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(value) ||
|
|
131
|
+
Number.isNaN(Date.parse(value))
|
|
132
|
+
) {
|
|
133
|
+
throw new TypeError(`${name} must be an RFC 3339 UTC timestamp`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function assertSafeIndex(index: number): void {
|
|
138
|
+
if (!Number.isSafeInteger(index) || index < 0) {
|
|
139
|
+
throw new TypeError("index must be a non-negative safe integer");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function assertBytes(value: unknown, name: string): asserts value is Uint8Array {
|
|
144
|
+
if (!(value instanceof Uint8Array)) {
|
|
145
|
+
throw new TypeError(`${name} must be a Uint8Array`);
|
|
146
|
+
}
|
|
147
|
+
}
|
package/src/log/types.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type CanonicalLogUrl } from "./canonical-url.ts";
|
|
2
|
+
|
|
3
|
+
export type LogCompleteness = "complete" | "discovery-only";
|
|
4
|
+
|
|
5
|
+
export type TransparencyLogEntry = {
|
|
6
|
+
logUrl: CanonicalLogUrl;
|
|
7
|
+
index: number;
|
|
8
|
+
integratedTime: string;
|
|
9
|
+
envelope: Uint8Array;
|
|
10
|
+
proof: unknown;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type TransparencyLogQueryResult = {
|
|
14
|
+
completeness: LogCompleteness;
|
|
15
|
+
entries: TransparencyLogEntry[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type VerificationLog = {
|
|
19
|
+
logUrl: CanonicalLogUrl;
|
|
20
|
+
queryByTokenRef(tokenRef: Uint8Array): TransparencyLogQueryResult;
|
|
21
|
+
verifyInclusionProof(entry: TransparencyLogEntry): boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type ReceiptSubmissionLog = {
|
|
25
|
+
logUrl: CanonicalLogUrl;
|
|
26
|
+
append(envelope: Uint8Array, integratedTime?: string): TransparencyLogEntry;
|
|
27
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { createReceipt, type CreatedReceipt } from "../service/create-receipt.ts";
|
|
2
|
+
import { verifySelloJwsToken } from "../token/jws-profile.ts";
|
|
3
|
+
import { type ReceiptSubmissionLog } from "../log/types.ts";
|
|
4
|
+
|
|
5
|
+
export type SelloMcpHandler<Request, Response> = (
|
|
6
|
+
request: Request,
|
|
7
|
+
) => Response | Promise<Response>;
|
|
8
|
+
|
|
9
|
+
export type SelloMcpValueOrGetter<Request, Value> =
|
|
10
|
+
| Value
|
|
11
|
+
| ((request: Request) => Value);
|
|
12
|
+
|
|
13
|
+
export type SelloMcpReceiptEvent<Response = unknown> = {
|
|
14
|
+
resultStatus: "success" | "error" | "denied";
|
|
15
|
+
receipt: CreatedReceipt;
|
|
16
|
+
response?: Response;
|
|
17
|
+
error?: unknown;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type SelloMcpMiddlewareInput<Request, Response> = {
|
|
21
|
+
handler: SelloMcpHandler<Request, Response>;
|
|
22
|
+
authorizationToken: SelloMcpValueOrGetter<Request, string | Uint8Array>;
|
|
23
|
+
tokenIssuerPublicKey: Uint8Array;
|
|
24
|
+
fallbackSelloLogs?: readonly string[];
|
|
25
|
+
serviceKid: Uint8Array;
|
|
26
|
+
servicePrivateKey: Uint8Array;
|
|
27
|
+
serviceIdentifier: string;
|
|
28
|
+
log: ReceiptSubmissionLog;
|
|
29
|
+
actionType?: string | ((request: Request) => string);
|
|
30
|
+
canonicalizeInput?: (request: Request) => Uint8Array;
|
|
31
|
+
canonicalizeOutput?: (response: Response) => Uint8Array;
|
|
32
|
+
canonicalizeError?: (error: unknown) => Uint8Array;
|
|
33
|
+
isDenied?: (request: Request) => boolean | Promise<boolean>;
|
|
34
|
+
deniedResponse?: (request: Request) => Response | Promise<Response>;
|
|
35
|
+
now?: () => string;
|
|
36
|
+
onReceipt?: (event: SelloMcpReceiptEvent<Response>) => void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export class SelloMcpDeniedError extends Error {
|
|
40
|
+
readonly receipt: CreatedReceipt;
|
|
41
|
+
|
|
42
|
+
constructor(receipt: CreatedReceipt) {
|
|
43
|
+
super("MCP request denied by Sello middleware");
|
|
44
|
+
this.name = "SelloMcpDeniedError";
|
|
45
|
+
this.receipt = receipt;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createSelloMcpMiddleware<Request, Response>(
|
|
50
|
+
input: SelloMcpMiddlewareInput<Request, Response>,
|
|
51
|
+
): SelloMcpHandler<Request, Response> {
|
|
52
|
+
return async (request: Request): Promise<Response> => {
|
|
53
|
+
const authorizationToken = resolveValue(input.authorizationToken, request);
|
|
54
|
+
const verifiedToken = verifySelloJwsToken({
|
|
55
|
+
authorizationToken,
|
|
56
|
+
issuerPublicKey: input.tokenIssuerPublicKey,
|
|
57
|
+
});
|
|
58
|
+
const baseReceiptInput = {
|
|
59
|
+
authorizationTokenBytes: verifiedToken.authorizationTokenBytes,
|
|
60
|
+
ownerHpkePublicKey: verifiedToken.ownerHpkePublicKey,
|
|
61
|
+
selloLogs: verifiedToken.selloLogs ?? input.fallbackSelloLogs ?? [],
|
|
62
|
+
serviceKid: input.serviceKid,
|
|
63
|
+
servicePrivateKey: input.servicePrivateKey,
|
|
64
|
+
serviceIdentifier: input.serviceIdentifier,
|
|
65
|
+
log: input.log,
|
|
66
|
+
actionType: resolveActionType(input.actionType, request),
|
|
67
|
+
actionInputBytes: (input.canonicalizeInput ?? canonicalJsonBytes)(request),
|
|
68
|
+
timestamp: (input.now ?? nowUtcSeconds)(),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (input.isDenied && (await input.isDenied(request))) {
|
|
72
|
+
const response = input.deniedResponse
|
|
73
|
+
? await input.deniedResponse(request)
|
|
74
|
+
: undefined;
|
|
75
|
+
const receipt = createReceipt({
|
|
76
|
+
...baseReceiptInput,
|
|
77
|
+
actionOutputBytes: new Uint8Array(),
|
|
78
|
+
resultStatus: "denied",
|
|
79
|
+
});
|
|
80
|
+
input.onReceipt?.({ resultStatus: "denied", receipt, response });
|
|
81
|
+
|
|
82
|
+
if (input.deniedResponse) {
|
|
83
|
+
return response as Response;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw new SelloMcpDeniedError(receipt);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let response: Response;
|
|
90
|
+
try {
|
|
91
|
+
response = await input.handler(request);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const receipt = createReceipt({
|
|
94
|
+
...baseReceiptInput,
|
|
95
|
+
actionOutputBytes: (input.canonicalizeError ?? canonicalErrorBytes)(
|
|
96
|
+
error,
|
|
97
|
+
),
|
|
98
|
+
resultStatus: "error",
|
|
99
|
+
});
|
|
100
|
+
input.onReceipt?.({ resultStatus: "error", receipt, error });
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const receipt = createReceipt({
|
|
105
|
+
...baseReceiptInput,
|
|
106
|
+
actionOutputBytes: (input.canonicalizeOutput ?? canonicalJsonBytes)(
|
|
107
|
+
response,
|
|
108
|
+
),
|
|
109
|
+
resultStatus: "success",
|
|
110
|
+
});
|
|
111
|
+
input.onReceipt?.({ resultStatus: "success", receipt, response });
|
|
112
|
+
return response;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function canonicalJsonBytes(value: unknown): Uint8Array {
|
|
117
|
+
return new TextEncoder().encode(canonicalJson(value));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function canonicalJson(value: unknown): string {
|
|
121
|
+
if (value === null) {
|
|
122
|
+
return "null";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
switch (typeof value) {
|
|
126
|
+
case "boolean":
|
|
127
|
+
case "number":
|
|
128
|
+
case "string":
|
|
129
|
+
return primitiveJson(value);
|
|
130
|
+
case "object":
|
|
131
|
+
if (Array.isArray(value)) {
|
|
132
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return objectJson(value as Record<string, unknown>);
|
|
136
|
+
default:
|
|
137
|
+
throw new TypeError("value must be JSON-serializable");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function primitiveJson(value: boolean | number | string): string {
|
|
142
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
143
|
+
throw new TypeError("number must be finite");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return JSON.stringify(value);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function objectJson(value: Record<string, unknown>): string {
|
|
150
|
+
const prototype = Object.getPrototypeOf(value);
|
|
151
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
152
|
+
throw new TypeError("object must be a plain JSON object");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const entries = Object.entries(value);
|
|
156
|
+
entries.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
|
|
157
|
+
|
|
158
|
+
return `{${entries
|
|
159
|
+
.map(([key, child]) => `${JSON.stringify(key)}:${canonicalJson(child)}`)
|
|
160
|
+
.join(",")}}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function canonicalErrorBytes(error: unknown): Uint8Array {
|
|
164
|
+
if (error instanceof Error) {
|
|
165
|
+
return canonicalJsonBytes({
|
|
166
|
+
name: error.name,
|
|
167
|
+
message: error.message,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return canonicalJsonBytes({ error: String(error) });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function resolveValue<Request, Value>(
|
|
175
|
+
value: SelloMcpValueOrGetter<Request, Value>,
|
|
176
|
+
request: Request,
|
|
177
|
+
): Value {
|
|
178
|
+
if (typeof value === "function") {
|
|
179
|
+
return (value as (request: Request) => Value)(request);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function resolveActionType<Request>(
|
|
186
|
+
actionType: SelloMcpMiddlewareInput<Request, unknown>["actionType"],
|
|
187
|
+
request: Request,
|
|
188
|
+
): string {
|
|
189
|
+
if (typeof actionType === "function") {
|
|
190
|
+
return actionType(request);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return actionType ?? "tools/call";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function nowUtcSeconds(): string {
|
|
197
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
198
|
+
}
|