can2cup 0.10.2 → 0.10.4
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 +25 -11
- package/SKILL.md +3 -0
- package/dist/cli/index.js +79 -2
- package/docs/SELF-HOST.md +121 -0
- package/package.json +7 -2
- package/src/protocol/canon.ts +17 -0
- package/src/protocol/crypto.ts +38 -0
- package/src/protocol/display.ts +11 -0
- package/src/protocol/e2e.ts +57 -0
- package/src/protocol/envelope.ts +127 -0
- package/src/protocol/index.ts +9 -0
- package/src/protocol/mandate.ts +61 -0
- package/src/protocol/principal.ts +90 -0
- package/src/protocol/release.ts +35 -0
- package/src/protocol/room.ts +115 -0
- package/src/protocol/semver.ts +14 -0
- package/src/relay/a2a.ts +212 -0
- package/src/relay/anchor.ts +159 -0
- package/src/relay/bridge.ts +1710 -0
- package/src/relay/index.ts +806 -0
- package/src/relay/join-page.ts +79 -0
- package/src/relay/mcp-http.ts +627 -0
- package/tsconfig.relay.json +12 -0
- package/wrangler.toml +106 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 3161 trusted timestamps over the transcript head.
|
|
3
|
+
*
|
|
4
|
+
* The problem this closes: the relay signs the head, so a client that pinned the
|
|
5
|
+
* relay key can prove a fork after the fact — but only against a relay that keeps
|
|
6
|
+
* its key honest. The operator holds that key. For a transcript meant to be cited
|
|
7
|
+
* later, "trust the operator's clock" is exactly the objection the reader will
|
|
8
|
+
* raise. A Time Stamping Authority is a neutral third party that signs
|
|
9
|
+
* "this digest existed at this time", and it is the primitive eIDAS/PAdES/CAdES
|
|
10
|
+
* already build on, so the attestation is one a lawyer recognises.
|
|
11
|
+
*
|
|
12
|
+
* Scope, deliberately: we BUILD the TimeStampReq and STORE the TimeStampResp as
|
|
13
|
+
* an opaque DER blob. We do not verify the CMS SignedData or walk the TSA's X.509
|
|
14
|
+
* chain here — doing that properly needs a real PKI stack, and a half-verified
|
|
15
|
+
* token is worse than an unverified one. Verification belongs where the tooling
|
|
16
|
+
* exists: `openssl ts -verify`. The relay only checks PKIStatus so it never
|
|
17
|
+
* stores a rejection as if it were evidence.
|
|
18
|
+
*
|
|
19
|
+
* Disabled unless TSA_URL is set. An unset relay answers "not configured" rather
|
|
20
|
+
* than silently skipping, so nobody believes they have an anchor they do not have.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// ------------------------------------------------------------------ DER ---
|
|
24
|
+
|
|
25
|
+
const tag = (t: number, body: Uint8Array): Uint8Array => {
|
|
26
|
+
const len = body.length;
|
|
27
|
+
let header: number[];
|
|
28
|
+
if (len < 0x80) header = [t, len];
|
|
29
|
+
else {
|
|
30
|
+
const bytes: number[] = [];
|
|
31
|
+
for (let n = len; n > 0; n = Math.floor(n / 256)) bytes.unshift(n % 256);
|
|
32
|
+
header = [t, 0x80 | bytes.length, ...bytes];
|
|
33
|
+
}
|
|
34
|
+
const out = new Uint8Array(header.length + len);
|
|
35
|
+
out.set(header, 0);
|
|
36
|
+
out.set(body, header.length);
|
|
37
|
+
return out;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const concat = (...parts: Uint8Array[]): Uint8Array => {
|
|
41
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
42
|
+
let at = 0;
|
|
43
|
+
for (const p of parts) { out.set(p, at); at += p.length; }
|
|
44
|
+
return out;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const SEQUENCE = 0x30, INTEGER = 0x02, OCTET_STRING = 0x04, NULL = 0x05, BOOLEAN = 0x01, OID = 0x06;
|
|
48
|
+
|
|
49
|
+
/** 2.16.840.1.101.3.4.2.1 — id-sha256 */
|
|
50
|
+
const OID_SHA256 = new Uint8Array([0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01]);
|
|
51
|
+
|
|
52
|
+
export function hexToBytes(hex: string): Uint8Array {
|
|
53
|
+
if (!/^[0-9a-f]*$/i.test(hex) || hex.length % 2) throw new Error("not hex");
|
|
54
|
+
const out = new Uint8Array(hex.length / 2);
|
|
55
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const bytesToB64 = (b: Uint8Array): string => {
|
|
60
|
+
let s = "";
|
|
61
|
+
for (const x of b) s += String.fromCharCode(x);
|
|
62
|
+
return btoa(s);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* TimeStampReq ::= SEQUENCE {
|
|
67
|
+
* version INTEGER {v1(1)}, messageImprint MessageImprint,
|
|
68
|
+
* reqPolicy TSAPolicyId OPTIONAL, nonce INTEGER OPTIONAL,
|
|
69
|
+
* certReq BOOLEAN DEFAULT FALSE, extensions [0] IMPLICIT Extensions OPTIONAL }
|
|
70
|
+
*
|
|
71
|
+
* `digest` is the sha256 the TSA will attest to — for us, the chain head hash,
|
|
72
|
+
* which is already a sha256 over the canonical envelope.
|
|
73
|
+
*/
|
|
74
|
+
export function buildTimeStampReq(digest: Uint8Array, nonce: Uint8Array): Uint8Array {
|
|
75
|
+
if (digest.length !== 32) throw new Error("sha256 digest must be 32 bytes");
|
|
76
|
+
const algId = tag(SEQUENCE, concat(tag(OID, OID_SHA256), tag(NULL, new Uint8Array(0))));
|
|
77
|
+
const messageImprint = tag(SEQUENCE, concat(algId, tag(OCTET_STRING, digest)));
|
|
78
|
+
// DER INTEGER is signed: a leading bit of 1 would read as negative.
|
|
79
|
+
const n = nonce[0] & 0x80 ? concat(new Uint8Array([0]), nonce) : nonce;
|
|
80
|
+
return tag(SEQUENCE, concat(
|
|
81
|
+
tag(INTEGER, new Uint8Array([1])),
|
|
82
|
+
messageImprint,
|
|
83
|
+
tag(INTEGER, n),
|
|
84
|
+
tag(BOOLEAN, new Uint8Array([0xff])), // certReq: ask the TSA to include its cert
|
|
85
|
+
));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Read PKIStatus out of a TimeStampResp. Structure:
|
|
90
|
+
* TimeStampResp ::= SEQUENCE { status PKIStatusInfo, timeStampToken TST OPTIONAL }
|
|
91
|
+
* PKIStatusInfo ::= SEQUENCE { status INTEGER, ... }
|
|
92
|
+
* 0 = granted, 1 = grantedWithMods; anything else is a rejection.
|
|
93
|
+
*/
|
|
94
|
+
export function readPkiStatus(resp: Uint8Array): number | undefined {
|
|
95
|
+
let i = 0;
|
|
96
|
+
const readLen = (): number => {
|
|
97
|
+
let len = resp[i++];
|
|
98
|
+
if (len & 0x80) {
|
|
99
|
+
const n = len & 0x7f;
|
|
100
|
+
len = 0;
|
|
101
|
+
for (let k = 0; k < n; k++) len = len * 256 + resp[i++];
|
|
102
|
+
}
|
|
103
|
+
return len;
|
|
104
|
+
};
|
|
105
|
+
if (resp[i++] !== SEQUENCE) return undefined; // TimeStampResp
|
|
106
|
+
readLen();
|
|
107
|
+
if (resp[i++] !== SEQUENCE) return undefined; // PKIStatusInfo
|
|
108
|
+
readLen();
|
|
109
|
+
if (resp[i++] !== INTEGER) return undefined;
|
|
110
|
+
const len = readLen();
|
|
111
|
+
let v = 0;
|
|
112
|
+
for (let k = 0; k < len; k++) v = v * 256 + resp[i++];
|
|
113
|
+
return v;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ------------------------------------------------------------- request ---
|
|
117
|
+
|
|
118
|
+
export interface Anchor {
|
|
119
|
+
seq: number;
|
|
120
|
+
hash: string; // the transcript head this attests to
|
|
121
|
+
requestedAt: string; // relay clock — informational only; the TSA's clock is inside the token
|
|
122
|
+
tsa: string;
|
|
123
|
+
status: number; // PKIStatus: 0 granted, 1 grantedWithMods
|
|
124
|
+
token: string; // base64 DER TimeStampResp — verify with `openssl ts -verify`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export class AnchorError extends Error {}
|
|
128
|
+
|
|
129
|
+
/** POST an RFC 3161 query and return the stored anchor. Throws AnchorError on any
|
|
130
|
+
* outcome that is not a granted token, so a failure is never stored as evidence. */
|
|
131
|
+
export async function requestTimestamp(
|
|
132
|
+
tsaUrl: string, seq: number, headHash: string, nonceHex: string,
|
|
133
|
+
): Promise<Anchor> {
|
|
134
|
+
const req = buildTimeStampReq(hexToBytes(headHash), hexToBytes(nonceHex));
|
|
135
|
+
let res: Response;
|
|
136
|
+
try {
|
|
137
|
+
res = await fetch(tsaUrl, {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: { "content-type": "application/timestamp-query" },
|
|
140
|
+
// workers-types' BodyInit predates TS's generic Uint8Array<ArrayBufferLike>; the
|
|
141
|
+
// runtime accepts a typed array here. Narrow cast rather than copying the buffer.
|
|
142
|
+
body: req as unknown as BodyInit,
|
|
143
|
+
});
|
|
144
|
+
} catch (e) {
|
|
145
|
+
throw new AnchorError(`TSA unreachable: ${(e as Error).message}`);
|
|
146
|
+
}
|
|
147
|
+
if (!res.ok) throw new AnchorError(`TSA returned HTTP ${res.status}`);
|
|
148
|
+
|
|
149
|
+
const body = new Uint8Array(await res.arrayBuffer());
|
|
150
|
+
if (body.length === 0) throw new AnchorError("TSA returned an empty body");
|
|
151
|
+
const status = readPkiStatus(body);
|
|
152
|
+
if (status === undefined) throw new AnchorError("TSA response is not a TimeStampResp");
|
|
153
|
+
if (status !== 0 && status !== 1) throw new AnchorError(`TSA rejected the request (PKIStatus ${status})`);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
seq, hash: headHash, requestedAt: new Date().toISOString(),
|
|
157
|
+
tsa: tsaUrl, status, token: bytesToB64(body),
|
|
158
|
+
};
|
|
159
|
+
}
|