@signwho/verify 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/dist/index.cjs ADDED
@@ -0,0 +1,481 @@
1
+ 'use strict';
2
+
3
+ var pkijs = require('pkijs');
4
+ var pdfLib = require('pdf-lib');
5
+ var pako = require('pako');
6
+
7
+ function _interopNamespace(e) {
8
+ if (e && e.__esModule) return e;
9
+ var n = Object.create(null);
10
+ if (e) {
11
+ Object.keys(e).forEach(function (k) {
12
+ if (k !== 'default') {
13
+ var d = Object.getOwnPropertyDescriptor(e, k);
14
+ Object.defineProperty(n, k, d.get ? d : {
15
+ enumerable: true,
16
+ get: function () { return e[k]; }
17
+ });
18
+ }
19
+ });
20
+ }
21
+ n.default = e;
22
+ return Object.freeze(n);
23
+ }
24
+
25
+ var pkijs__namespace = /*#__PURE__*/_interopNamespace(pkijs);
26
+
27
+ // src/verify-checks.ts
28
+
29
+ // src/signwho-crypto.ts
30
+ async function hashData(data) {
31
+ const encoded = new TextEncoder().encode(data);
32
+ const buffer = await crypto.subtle.digest("SHA-256", encoded);
33
+ return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
34
+ }
35
+ async function hashArrayBuffer(buffer) {
36
+ const hash = await crypto.subtle.digest("SHA-256", buffer);
37
+ return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
38
+ }
39
+ function uint8ToBase64(bytes) {
40
+ let binary = "";
41
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
42
+ return btoa(binary);
43
+ }
44
+ function bytesToBase64Url(bytes) {
45
+ return uint8ToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
46
+ }
47
+ function base64UrlToBytes(s) {
48
+ const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
49
+ const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + pad;
50
+ const bin = atob(b64);
51
+ const out = new Uint8Array(bin.length);
52
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
53
+ return out;
54
+ }
55
+ async function computeTriangleHash(selfieHash, biometricTimestamp, documentHash) {
56
+ return hashData(`${selfieHash}||${biometricTimestamp}||${documentHash}`);
57
+ }
58
+ async function computeWebAuthnChallenge(documentHash, challengeNonce) {
59
+ const input = new TextEncoder().encode(`${documentHash}||${challengeNonce}`);
60
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", input));
61
+ return bytesToBase64Url(digest);
62
+ }
63
+
64
+ // src/freetsa-cacert.ts
65
+ var FREETSA_CA_PEM = `
66
+ -----BEGIN CERTIFICATE-----
67
+ MIIH/zCCBeegAwIBAgIJAMHphhYNqOmAMA0GCSqGSIb3DQEBDQUAMIGVMREwDwYD
68
+ VQQKEwhGcmVlIFRTQTEQMA4GA1UECxMHUm9vdCBDQTEYMBYGA1UEAxMPd3d3LmZy
69
+ ZWV0c2Eub3JnMSIwIAYJKoZIhvcNAQkBFhNidXNpbGV6YXNAZ21haWwuY29tMRIw
70
+ EAYDVQQHEwlXdWVyemJ1cmcxDzANBgNVBAgTBkJheWVybjELMAkGA1UEBhMCREUw
71
+ HhcNMTYwMzEzMDE1MjEzWhcNNDEwMzA3MDE1MjEzWjCBlTERMA8GA1UEChMIRnJl
72
+ ZSBUU0ExEDAOBgNVBAsTB1Jvb3QgQ0ExGDAWBgNVBAMTD3d3dy5mcmVldHNhLm9y
73
+ ZzEiMCAGCSqGSIb3DQEJARYTYnVzaWxlemFzQGdtYWlsLmNvbTESMBAGA1UEBxMJ
74
+ V3VlcnpidXJnMQ8wDQYDVQQIEwZCYXllcm4xCzAJBgNVBAYTAkRFMIICIjANBgkq
75
+ hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtgKODjAy8REQ2WTNqUudAnjhlCrpE6ql
76
+ mQfNppeTmVvZrH4zutn+NwTaHAGpjSGv4/WRpZ1wZ3BRZ5mPUBZyLgq0YrIfQ5Fx
77
+ 0s/MRZPzc1r3lKWrMR9sAQx4mN4z11xFEO529L0dFJjPF9MD8Gpd2feWzGyptlel
78
+ b+PqT+++fOa2oY0+NaMM7l/xcNHPOaMz0/2olk0i22hbKeVhvokPCqhFhzsuhKsm
79
+ q4Of/o+t6dI7sx5h0nPMm4gGSRhfq+z6BTRgCrqQG2FOLoVFgt6iIm/BnNffUr7V
80
+ DYd3zZmIwFOj/H3DKHoGik/xK3E82YA2ZulVOFRW/zj4ApjPa5OFbpIkd0pmzxzd
81
+ EcL479hSA9dFiyVmSxPtY5ze1P+BE9bMU1PScpRzw8MHFXxyKqW13Qv7LWw4sbk3
82
+ SciB7GACbQiVGzgkvXG6y85HOuvWNvC5GLSiyP9GlPB0V68tbxz4JVTRdw/Xn/XT
83
+ FNzRBM3cq8lBOAVt/PAX5+uFcv1S9wFE8YjaBfWCP1jdBil+c4e+0tdywT2oJmYB
84
+ BF/kEt1wmGwMmHunNEuQNzh1FtJY54hbUfiWi38mASE7xMtMhfj/C4SvapiDN837
85
+ gYaPfs8x3KZxbX7C3YAsFnJinlwAUss1fdKar8Q/YVs7H/nU4c4Ixxxz4f67fcVq
86
+ M2ITKentbCMCAwEAAaOCAk4wggJKMAwGA1UdEwQFMAMBAf8wDgYDVR0PAQH/BAQD
87
+ AgHGMB0GA1UdDgQWBBT6VQ2MNGZRQ0z357OnbJWveuaklzCBygYDVR0jBIHCMIG/
88
+ gBT6VQ2MNGZRQ0z357OnbJWveuakl6GBm6SBmDCBlTERMA8GA1UEChMIRnJlZSBU
89
+ U0ExEDAOBgNVBAsTB1Jvb3QgQ0ExGDAWBgNVBAMTD3d3dy5mcmVldHNhLm9yZzEi
90
+ MCAGCSqGSIb3DQEJARYTYnVzaWxlemFzQGdtYWlsLmNvbTESMBAGA1UEBxMJV3Vl
91
+ cnpidXJnMQ8wDQYDVQQIEwZCYXllcm4xCzAJBgNVBAYTAkRFggkAwemGFg2o6YAw
92
+ MwYDVR0fBCwwKjAooCagJIYiaHR0cDovL3d3dy5mcmVldHNhLm9yZy9yb290X2Nh
93
+ LmNybDCBzwYDVR0gBIHHMIHEMIHBBgorBgEEAYHyJAEBMIGyMDMGCCsGAQUFBwIB
94
+ FidodHRwOi8vd3d3LmZyZWV0c2Eub3JnL2ZyZWV0c2FfY3BzLmh0bWwwMgYIKwYB
95
+ BQUHAgEWJmh0dHA6Ly93d3cuZnJlZXRzYS5vcmcvZnJlZXRzYV9jcHMucGRmMEcG
96
+ CCsGAQUFBwICMDsaOUZyZWVUU0EgdHJ1c3RlZCB0aW1lc3RhbXBpbmcgU29mdHdh
97
+ cmUgYXMgYSBTZXJ2aWNlIChTYWFTKTA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUH
98
+ MAGGG2h0dHA6Ly93d3cuZnJlZXRzYS5vcmc6MjU2MDANBgkqhkiG9w0BAQ0FAAOC
99
+ AgEAaK9+v5OFYu9M6ztYC+L69sw1omdyli89lZAfpWMMh9CRmJhM6KBqM/ipwoLt
100
+ nxyxGsbCPhcQjuTvzm+ylN6VwTMmIlVyVSLKYZcdSjt/eCUN+41K7sD7GVmxZBAF
101
+ ILnBDmTGJmLkrU0KuuIpj8lI/E6Z6NnmuP2+RAQSHsfBQi6sssnXMo4HOW5gtPO7
102
+ gDrUpVXID++1P4XndkoKn7Svw5n0zS9fv1hxBcYIHPPQUze2u30bAQt0n0iIyRLz
103
+ aWuhtpAtd7ffwEbASgzB7E+NGF4tpV37e8KiA2xiGSRqT5ndu28fgpOY87gD3ArZ
104
+ DctZvvTCfHdAS5kEO3gnGGeZEVLDmfEsv8TGJa3AljVa5E40IQDsUXpQLi8G+UC4
105
+ 1DWZu8EVT4rnYaCw1VX7ShOR1PNCCvjb8S8tfdudd9zhU3gEB0rxdeTy1tVbNLXW
106
+ 99y90xcwr1ZIDUwM/xQ/noO8FRhm0LoPC73Ef+J4ZBdrvWwauF3zJe33d4ibxEcb
107
+ 8/pz5WzFkeixYM2nsHhqHsBKw7JPouKNXRnl5IAE1eFmqDyC7G/VT7OF669xM6hb
108
+ Ut5G21JE4cNK6NNucS+fzg1JPX0+3VhsYZjj7D5uljRvQXrJ8iHgr/M6j2oLHvTA
109
+ I2MLdq2qjZFDOCXsxBxJpbmLGBx9ow6ZerlUxzws2AWv2pk=
110
+ -----END CERTIFICATE-----
111
+ `;
112
+
113
+ // src/verify-checks.ts
114
+ var toHex = (b) => Array.from(b).map((x) => x.toString(16).padStart(2, "0")).join("");
115
+ var eqHex = (a, b) => !!a && !!b && a.toLowerCase() === b.toLowerCase();
116
+ var ab = (u) => u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
117
+ function base64ToBytes(s) {
118
+ const bin = atob(s.replace(/\s+/g, ""));
119
+ const out = new Uint8Array(bin.length);
120
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
121
+ return out;
122
+ }
123
+ function derEcdsaToRaw(der, size) {
124
+ let i = 0;
125
+ if (der[i++] !== 48) throw new Error("bad der seq");
126
+ if (der[i] & 128) {
127
+ i += 1 + (der[i] & 127);
128
+ } else i++;
129
+ if (der[i++] !== 2) throw new Error("bad der r");
130
+ let rl = der[i++];
131
+ let r = der.slice(i, i + rl);
132
+ i += rl;
133
+ if (der[i++] !== 2) throw new Error("bad der s");
134
+ let sl = der[i++];
135
+ let s = der.slice(i, i + sl);
136
+ const strip = (x) => {
137
+ let j = 0;
138
+ while (j < x.length - 1 && x[j] === 0) j++;
139
+ return x.slice(j);
140
+ };
141
+ r = strip(r);
142
+ s = strip(s);
143
+ const out = new Uint8Array(size * 2);
144
+ out.set(r, size - r.length);
145
+ out.set(s, size * 2 - s.length);
146
+ return out;
147
+ }
148
+ var curveSize = (c) => c === "P-521" ? 66 : c === "P-384" ? 48 : 32;
149
+ var DIGEST_OIDS = {
150
+ "2.16.840.1.101.3.4.2.1": "SHA-256",
151
+ "2.16.840.1.101.3.4.2.2": "SHA-384",
152
+ "2.16.840.1.101.3.4.2.3": "SHA-512",
153
+ "1.3.14.3.2.26": "SHA-1"
154
+ };
155
+ function ensurePkijsEngine() {
156
+ try {
157
+ const e = pkijs__namespace.getEngine();
158
+ if (e && e.subtle) return;
159
+ } catch {
160
+ }
161
+ pkijs__namespace.setEngine("signwho", new pkijs__namespace.CryptoEngine({ name: "signwho", crypto: globalThis.crypto }));
162
+ }
163
+ async function checkFileIntegrity(pdfBytes, ev) {
164
+ const id = "file", label = "File matches the signed record";
165
+ if (!ev.signedPdfHash) return { id, label, status: "skip", detail: "This evidence.json has no signedPdfHash. Use the STANDALONE evidence.json from the email/download (the copy embedded in the PDF omits it by design)." };
166
+ if (!pdfBytes) return { id, label, status: "skip", detail: "Provide the signed PDF to check it against signedPdfHash." };
167
+ try {
168
+ const hex = await hashArrayBuffer(ab(pdfBytes));
169
+ return eqHex(hex, ev.signedPdfHash) ? { id, label, status: "pass", detail: `SHA-256 of the PDF you provided equals evidence.signedPdfHash (${hex.slice(0, 16)}\u2026).` } : { id, label, status: "fail", detail: `SHA-256 of the provided PDF (${hex.slice(0, 16)}\u2026) does NOT match evidence.signedPdfHash (${ev.signedPdfHash.slice(0, 16)}\u2026). This is not the delivered file, or it was modified.` };
170
+ } catch (e) {
171
+ return { id, label, status: "fail", detail: "Could not hash the PDF: " + e.message };
172
+ }
173
+ }
174
+ async function checkTriangleHash(ev) {
175
+ const id = "triangle", label = "Triangle Authentication binding intact";
176
+ if (ev.selfieIncluded === false)
177
+ return { id, label, status: "skip", detail: "No selfie \u2014 the signer completed without a camera capture, so there is no selfie/time/document Triangle binding for this signature (by design, not a failure)." };
178
+ if (!ev.selfieHash || ev.biometricTimestamp == null || !ev.documentHash || !ev.triangleHash)
179
+ return { id, label, status: "skip", detail: "Missing fields (selfieHash / biometricTimestamp / documentHash / triangleHash)." };
180
+ try {
181
+ const recomputed = await computeTriangleHash(ev.selfieHash, ev.biometricTimestamp, ev.documentHash);
182
+ return eqHex(recomputed, ev.triangleHash) ? { id, label, status: "pass", detail: "SHA-256(selfieHash || biometricTimestamp || documentHash) equals evidence.triangleHash \u2014 the three artifacts are bound together." } : { id, label, status: "fail", detail: "Recomputed triangleHash does not match evidence.triangleHash \u2014 a bound component was altered." };
183
+ } catch (e) {
184
+ return { id, label, status: "fail", detail: e.message };
185
+ }
186
+ }
187
+ async function checkWebAuthn(ev) {
188
+ const id = "webauthn", label = "Signature cryptographically verified (WebAuthn)";
189
+ const p = ev.webauthnProof;
190
+ if (!p || !p.signature) return { id, label, status: "skip", detail: "No WebAuthn assertion in this signature (signer had no usable platform/roaming authenticator \u2014 recorded as manual/passkey)." };
191
+ if (!ev.documentHash || !p.challengeNonce || !p.clientDataJSON || !p.authenticatorData || !p.publicKey || p.alg !== -7 && p.alg !== -257)
192
+ return { id, label, status: "fail", detail: "Assertion is missing required fields (publicKey/alg/authenticatorData/clientDataJSON/challengeNonce)." };
193
+ try {
194
+ const expected = await computeWebAuthnChallenge(ev.documentHash, p.challengeNonce);
195
+ const cdj = JSON.parse(new TextDecoder().decode(base64UrlToBytes(p.clientDataJSON)));
196
+ if (cdj.type !== "webauthn.get") return { id, label, status: "fail", detail: `clientDataJSON.type is "${cdj.type}", expected "webauthn.get".` };
197
+ if (cdj.challenge !== expected) return { id, label, status: "fail", detail: "The challenge in clientDataJSON is not SHA-256(documentHash || challengeNonce) \u2014 the assertion is not bound to this document." };
198
+ if (p.challenge && p.challenge !== expected) return { id, label, status: "fail", detail: "Stored challenge does not match the recomputed document-bound challenge." };
199
+ const authData = base64UrlToBytes(p.authenticatorData);
200
+ const cdjBytes = base64UrlToBytes(p.clientDataJSON);
201
+ const cdjHash = new Uint8Array(await crypto.subtle.digest("SHA-256", ab(cdjBytes)));
202
+ const signed = new Uint8Array(authData.length + cdjHash.length);
203
+ signed.set(authData, 0);
204
+ signed.set(cdjHash, authData.length);
205
+ const spki = base64UrlToBytes(p.publicKey);
206
+ const sig = base64UrlToBytes(p.signature);
207
+ let ok;
208
+ if (p.alg === -7) {
209
+ const key = await crypto.subtle.importKey("spki", ab(spki), { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
210
+ ok = await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, key, ab(derEcdsaToRaw(sig, 32)), ab(signed));
211
+ } else {
212
+ const key = await crypto.subtle.importKey("spki", ab(spki), { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["verify"]);
213
+ ok = await crypto.subtle.verify({ name: "RSASSA-PKCS1-v1_5" }, key, ab(sig), ab(signed));
214
+ }
215
+ if (!ok) return { id, label, status: "fail", detail: "The WebAuthn assertion signature did not verify against the stored public key." };
216
+ const uv = (authData[32] & 4) !== 0;
217
+ return { id, label, status: "pass", detail: `Assertion over the document-bound challenge verifies against the public key (${p.alg === -7 ? "ES256" : "RS256"})${uv ? ", user verification (biometric/PIN) flag set" : ""}. Proves the private key holder signed THIS document.` };
218
+ } catch (e) {
219
+ return { id, label, status: "fail", detail: "Verification error: " + e.message };
220
+ }
221
+ }
222
+ async function checkTsa(ev) {
223
+ const id = "tsa", label = "Timestamp valid (FreeTSA, non-qualified)";
224
+ const t = ev.tsaTimestamp;
225
+ if (!t || !t.token) return { id, label, status: "skip", detail: "No RFC-3161 timestamp (best-effort at signing; FreeTSA may have been unreachable)." };
226
+ const tsaTarget = ev.triangleHash || ev.documentHash;
227
+ const tsaTargetName = ev.triangleHash ? "triangleHash" : "documentHash";
228
+ if (!tsaTarget) return { id, label, status: "fail", detail: "Cannot check the timestamp without a triangleHash or documentHash to match." };
229
+ try {
230
+ ensurePkijsEngine();
231
+ const tokenBytes = base64ToBytes(t.token);
232
+ const ci = pkijs__namespace.ContentInfo.fromBER(ab(tokenBytes));
233
+ const sd = new pkijs__namespace.SignedData({ schema: ci.content });
234
+ const si = sd.signerInfos[0];
235
+ const digAlg = DIGEST_OIDS[si.digestAlgorithm.algorithmId] || "SHA-256";
236
+ const ecView = si ? sd.encapContentInfo.eContent.valueBlock.valueHexView : new Uint8Array();
237
+ const eContent = ab(ecView);
238
+ const tst = pkijs__namespace.TSTInfo.fromBER(eContent);
239
+ const mi = toHex(new Uint8Array(tst.messageImprint.hashedMessage.valueBlock.valueHexView));
240
+ if (!eqHex(mi, tsaTarget)) return { id, label, status: "fail", detail: `The timestamp covers a different hash than evidence.${tsaTargetName}.` };
241
+ let md = null;
242
+ for (const a of si.signedAttrs?.attributes || []) if (a.type === "1.2.840.113549.1.9.4") md = new Uint8Array(a.values[0].valueBlock.valueHexView);
243
+ const ecHash = new Uint8Array(await crypto.subtle.digest(digAlg, eContent));
244
+ if (!md || toHex(md) !== toHex(ecHash)) return { id, label, status: "fail", detail: "The signed messageDigest does not match the timestamp content." };
245
+ const sa = new Uint8Array(si.signedAttrs.encodedValue.slice(0));
246
+ sa[0] = 49;
247
+ const signerCert = sd.certificates?.find((c) => {
248
+ try {
249
+ return c.serialNumber.isEqual(si.sid.serialNumber);
250
+ } catch {
251
+ return false;
252
+ }
253
+ }) || sd.certificates[0];
254
+ const isEc = si.signatureAlgorithm.algorithmId.startsWith("1.2.840.10045");
255
+ let sigOk;
256
+ if (isEc) {
257
+ const pub = await signerCert.getPublicKey();
258
+ const raw = derEcdsaToRaw(new Uint8Array(si.signature.valueBlock.valueHexView), curveSize(pub.algorithm.namedCurve));
259
+ sigOk = await crypto.subtle.verify({ name: "ECDSA", hash: digAlg }, pub, ab(raw), ab(sa));
260
+ } else {
261
+ const pub = await signerCert.getPublicKey({ algorithm: { algorithm: { name: "RSASSA-PKCS1-v1_5", hash: digAlg }, usages: ["verify"] } });
262
+ sigOk = await crypto.subtle.verify({ name: "RSASSA-PKCS1-v1_5" }, pub, ab(new Uint8Array(si.signature.valueBlock.valueHexView)), ab(sa));
263
+ }
264
+ if (!sigOk) return { id, label, status: "fail", detail: "The TSA signature over the timestamp did not verify." };
265
+ const genTimeDate = tst.genTime instanceof Date ? tst.genTime : new Date(String(tst.genTime));
266
+ if (isNaN(genTimeDate.getTime())) return { id, label, status: "fail", detail: "The timestamp has no readable genTime." };
267
+ const nb = signerCert.notBefore.value, na = signerCert.notAfter.value;
268
+ if (genTimeDate < nb || genTimeDate > na) return { id, label, status: "fail", detail: `genTime (${genTimeDate.toISOString()}) is outside the TSA signing certificate's validity window (${nb.toISOString()} \u2026 ${na.toISOString()}).` };
269
+ const pem = FREETSA_CA_PEM.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
270
+ const caDer = base64ToBytes(pem);
271
+ const ca = pkijs__namespace.Certificate.fromBER(ab(caDer));
272
+ const chain = await new pkijs__namespace.CertificateChainValidationEngine({ certs: [signerCert], trustedCerts: [ca], checkDate: genTimeDate }).verify();
273
+ if (!chain.result) return { id, label, status: "fail", detail: "The TSA certificate does not chain to the bundled FreeTSA CA as of genTime (code " + chain.resultCode + ")." };
274
+ const gt = genTimeDate.toISOString();
275
+ return { id, label, status: "pass", facts: { genTime: genTimeDate.toISOString(), serial: toHex(new Uint8Array(tst.serialNumber.valueBlock.valueHexView)) }, detail: `FreeTSA attests this signature's ${tsaTargetName} existed at ${gt} (RFC-3161, ${digAlg}; chain validated to the bundled FreeTSA CA as of that time). Non-qualified TSA.` };
276
+ } catch (e) {
277
+ return { id, label, status: "fail", detail: "Timestamp parse/verify error: " + e.message };
278
+ }
279
+ }
280
+ function selfieNote(ev) {
281
+ if (ev.selfieIncluded === false) {
282
+ return {
283
+ id: "selfie",
284
+ label: "No selfie \u2014 not captured",
285
+ status: "info",
286
+ detail: `This signature was completed WITHOUT a selfie (the signer couldn't or chose not to use a camera). ${ev.webauthnProof ? "It is device-verified (WebAuthn) and timestamped, but carries no photographic evidence and no identity binding." : "It carries neither a selfie nor a device signature \u2014 it is document + timestamp only, the lowest assurance tier."}`
287
+ };
288
+ }
289
+ return {
290
+ id: "selfie",
291
+ label: "Selfie captured \u2014 not identity-verified",
292
+ status: "info",
293
+ detail: `A selfie was captured at signing (hash bound into the Triangle, verified above). ${ev.faceDetected ? "A face was detected by an in-browser heuristic. " : ""}This is NOT identity verification \u2014 SignWho does not match the face to a government ID or a real-world identity.`
294
+ };
295
+ }
296
+ var TSA_GAP_NOTE_THRESHOLD_MS = 10 * 60 * 1e3;
297
+ function describeGap(ms) {
298
+ const abs = Math.abs(ms);
299
+ const minutes = Math.round(abs / 6e4);
300
+ if (minutes < 120) return `${minutes} minute${minutes === 1 ? "" : "s"}`;
301
+ const hours = Math.round(abs / 36e5);
302
+ if (hours < 48) return `${hours} hours`;
303
+ const days = Math.round(abs / 864e5);
304
+ return `${days} days`;
305
+ }
306
+ function timestampGapNote(ev, tsaCheck) {
307
+ if (!tsaCheck || tsaCheck.status !== "pass" || !tsaCheck.facts?.genTime) return null;
308
+ if (typeof ev.timestamp !== "number" || !Number.isFinite(ev.timestamp)) return null;
309
+ const genMs = Date.parse(tsaCheck.facts.genTime);
310
+ if (Number.isNaN(genMs)) return null;
311
+ const gap = genMs - ev.timestamp;
312
+ if (Math.abs(gap) <= TSA_GAP_NOTE_THRESHOLD_MS) return null;
313
+ const when = gap > 0 ? "after" : "before";
314
+ return {
315
+ id: "tsa-gap",
316
+ label: `Timestamp obtained ${describeGap(gap)} ${when} the recorded signing time`,
317
+ status: "info",
318
+ detail: `The independent timestamp was obtained ${describeGap(gap)} ${when} the recorded signing time (genTime ${tsaCheck.facts.genTime}, evidence.timestamp ${new Date(ev.timestamp).toISOString()}). The token is still valid \u2014 it proves the hash existed at genTime \u2014 but the signer's declared time and the third-party time disagree by more than ${TSA_GAP_NOTE_THRESHOLD_MS / 6e4} minutes.`,
319
+ facts: { gapMs: String(gap) }
320
+ };
321
+ }
322
+ async function runAllChecks(pdfBytes, ev) {
323
+ const tsa = await checkTsa(ev);
324
+ const gap = timestampGapNote(ev, tsa);
325
+ return [
326
+ await checkFileIntegrity(pdfBytes, ev),
327
+ await checkWebAuthn(ev),
328
+ tsa,
329
+ await checkTriangleHash(ev),
330
+ selfieNote(ev),
331
+ ...gap ? [gap] : []
332
+ ];
333
+ }
334
+ async function extractEmbeddedEvidence(pdfBytes) {
335
+ try {
336
+ const doc = await pdfLib.PDFDocument.load(pdfBytes, { ignoreEncryption: true, updateMetadata: false });
337
+ const names = doc.catalog.lookupMaybe(pdfLib.PDFName.of("Names"), pdfLib.PDFDict);
338
+ const ef = names?.lookupMaybe(pdfLib.PDFName.of("EmbeddedFiles"), pdfLib.PDFDict);
339
+ const arr = ef?.lookupMaybe(pdfLib.PDFName.of("Names"), pdfLib.PDFArray);
340
+ if (!arr) return null;
341
+ for (let i = 0; i + 1 < arr.size(); i += 2) {
342
+ const filespec = arr.lookup(i + 1, pdfLib.PDFDict);
343
+ const efDict = filespec.lookupMaybe(pdfLib.PDFName.of("EF"), pdfLib.PDFDict);
344
+ const stream = efDict?.lookup(pdfLib.PDFName.of("F"));
345
+ if (!stream || !(stream instanceof pdfLib.PDFRawStream)) continue;
346
+ let bytes = stream.contents;
347
+ const filter = stream.dict.get(pdfLib.PDFName.of("Filter"));
348
+ if (filter && filter.toString().includes("FlateDecode")) bytes = pako.inflate(bytes);
349
+ const text = new TextDecoder().decode(bytes);
350
+ if (text.trim().startsWith("{")) return JSON.parse(text);
351
+ }
352
+ } catch {
353
+ }
354
+ return null;
355
+ }
356
+ function deriveTier(ev, checks) {
357
+ const noSelfie = ev.selfieIncluded === false || !ev.triangleHash && !ev.selfieHash;
358
+ if (!noSelfie) return "identity";
359
+ const webauthnPass = checks.find((c) => c.id === "webauthn")?.status === "pass";
360
+ return webauthnPass ? "device" : "floor";
361
+ }
362
+ async function verifyPackage(pdfBytes, ev) {
363
+ const checks = await runAllChecks(pdfBytes, ev);
364
+ const passes = checks.filter((c) => c.status === "pass").length;
365
+ const fails = checks.filter((c) => c.status === "fail").length;
366
+ return { evidence: ev, checks, tier: deriveTier(ev, checks), passes, fails, allGood: fails === 0 && passes > 0 };
367
+ }
368
+
369
+ // src/dispute-summary.ts
370
+ var VERIFY_URL = "https://signwho.com/verify";
371
+ var NOT_RECORDED = "not recorded in this package";
372
+ function utcStamp(value) {
373
+ if (value === null || value === void 0 || value === "") return null;
374
+ const d = new Date(value);
375
+ if (Number.isNaN(d.getTime())) return null;
376
+ return d.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, " UTC");
377
+ }
378
+ function disputeSummaryFilename(documentName) {
379
+ const base = (documentName || "").replace(/\.pdf$/i, "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
380
+ return `SignWho-Dispute-Summary-${base || "document"}.txt`;
381
+ }
382
+ var TIER_LABEL = {
383
+ identity: "Identity-bound \u2014 selfie, device signature and independent timestamp",
384
+ device: "Device-verified, no selfie \u2014 device signature and independent timestamp",
385
+ floor: "Document + timestamp only \u2014 neither identity nor device verified"
386
+ };
387
+ var TAG = { pass: "PASS", fail: "FAIL", skip: "N/A ", info: "NOTE" };
388
+ function buildDisputeSummary(input) {
389
+ const { report, generatedAt, evidenceSource, isSample } = input;
390
+ const ev = report.evidence;
391
+ const events = ev.auditEvents || [];
392
+ const created = events.find((e) => e.type === "document_created");
393
+ const viewed = events.find((e) => e.type === "document_viewed");
394
+ const signed = events.find((e) => e.type === "document_signed");
395
+ const customerName = ev.signerName || NOT_RECORDED;
396
+ const customerEmail = signed?.signerEmail || viewed?.signerEmail || NOT_RECORDED;
397
+ const merchantEmail = ev.senderEmail || created?.senderEmail || NOT_RECORDED;
398
+ const signedAt = utcStamp(signed?.timestamp ?? ev.completedAt ?? ev.timestamp);
399
+ const tsaCheck = report.checks.find((c) => c.id === "tsa");
400
+ const tsaVerified = tsaCheck?.status === "pass" && !!tsaCheck.facts?.genTime;
401
+ const genTime = utcStamp(tsaVerified ? tsaCheck.facts.genTime : ev.tsaTimestamp?.genTime);
402
+ const tsaSerial = tsaVerified ? tsaCheck.facts.serial : ev.tsaTimestamp?.serial;
403
+ const noSelfie = ev.selfieIncluded === false || !ev.selfieHash && !ev.triangleHash;
404
+ const ipValue = (e) => e?.ip ? `${e.ip} (${utcStamp(e.timestamp)})` : null;
405
+ const L = [];
406
+ const section = (n, title, stripe) => {
407
+ L.push("", `${n}. ${title}`, ` (Stripe evidence fields: ${stripe})`);
408
+ };
409
+ const row = (label, value) => {
410
+ L.push(` ${label.padEnd(28)}${value || NOT_RECORDED}`);
411
+ };
412
+ L.push("SIGNWHO \u2014 PAYMENT DISPUTE SUMMARY");
413
+ L.push(`Generated ${utcStamp(generatedAt.getTime())} by the independent verifier at ${VERIFY_URL}.`);
414
+ L.push("Every check below ran in the browser from the two files alone; no SignWho server was consulted.");
415
+ if (isSample) L.push("THIS IS SIGNWHO'S BUNDLED SAMPLE PACKAGE \u2014 a demonstration, not a real agreement.");
416
+ if (!report.allGood) {
417
+ L.push("", `WARNING: ${report.fails} check(s) FAILED \u2014 this package does not verify. Do not rely on it as-is.`);
418
+ }
419
+ section(1, "CUSTOMER", "customer_name, customer_email_address");
420
+ row("Name (as signed):", customerName);
421
+ row("E-mail:", customerEmail);
422
+ row("Assurance level:", TIER_LABEL[report.tier]);
423
+ section(2, "CUSTOMER IP ADDRESSES", "customer_purchase_ip");
424
+ row("Viewed the document from:", ipValue(viewed));
425
+ row("Signed the document from:", ipValue(signed));
426
+ section(3, "TIMESTAMPS (UTC)", "service_date, customer_communication");
427
+ row("Document sent:", utcStamp(created?.timestamp));
428
+ row("Document viewed:", utcStamp(viewed?.timestamp));
429
+ row("Document signed:", signedAt);
430
+ row("Independent timestamp:", genTime ? `${genTime} \u2014 ${ev.tsaTimestamp?.tsa || "RFC 3161 TSA"}${tsaSerial ? `, token serial ${tsaSerial}` : ""} ${tsaVerified ? "(RFC 3161, third party)" : "(declared in the evidence, NOT verified by the timestamp check)"}` : null);
431
+ const gapNote = report.checks.find((c) => c.id === "tsa-gap");
432
+ if (gapNote) L.push(` NOTE: ${gapNote.detail}`);
433
+ section(4, "DOCUMENT", "customer_signature, service_documentation");
434
+ row("Document name:", ev.documentName);
435
+ row("Sent by (merchant):", merchantEmail);
436
+ row("SHA-256, signed PDF:", ev.signedPdfHash || (evidenceSource === "embedded" ? "not present \u2014 the evidence was read from inside the PDF; the standalone evidence.json carries the file hash" : null));
437
+ row("SHA-256, original document:", ev.documentHash);
438
+ row("Signature method:", ev.authMethod === "biometric" ? "device biometric (Face ID / fingerprint / Windows Hello) \u2014 WebAuthn assertion" : ev.authMethod === "passkey" ? "device passkey \u2014 WebAuthn assertion" : ev.authMethod === "manual" ? "manual (no device signature)" : ev.webauthnProof ? "WebAuthn assertion" : null);
439
+ row("Customer's face:", noSelfie ? "no selfie was captured for this signature" : `a selfie was captured at signing (SHA-256 ${ev.selfieHash || NOT_RECORDED}); the image is printed on the certificate page of the signed PDF`);
440
+ section(5, `VERIFICATION RESULTS \u2014 ${report.passes} of ${report.checks.filter((c) => c.status !== "info").length} checks passed`, "uncategorized_text");
441
+ for (const c of report.checks) {
442
+ L.push(` [${TAG[c.status]}] ${c.label}`);
443
+ L.push(` ${c.detail}`);
444
+ }
445
+ L.push("", "6. HOW TO RE-VERIFY (for the card network or the customer's bank)");
446
+ L.push(` Open the signed PDF and the evidence.json at ${VERIFY_URL} \u2014 the checks run in the browser, nothing is uploaded.`);
447
+ L.push(" Attach both files to the dispute: the signed PDF as the customer's signature, the evidence.json as the service documentation.");
448
+ if (input.pdfName || input.evidenceFileName) {
449
+ L.push(` Files verified for this summary: ${[input.pdfName, input.evidenceFileName].filter(Boolean).join(", ")}`);
450
+ }
451
+ L.push(" The algorithms are public standards (SHA-256, WebAuthn/FIDO2, RFC 3161); anyone can re-run them with open tools.");
452
+ L.push("");
453
+ return { text: L.join("\n"), filename: disputeSummaryFilename(ev.documentName) };
454
+ }
455
+
456
+ exports.FREETSA_CA_PEM = FREETSA_CA_PEM;
457
+ exports.TSA_GAP_NOTE_THRESHOLD_MS = TSA_GAP_NOTE_THRESHOLD_MS;
458
+ exports.VERIFY_URL = VERIFY_URL;
459
+ exports.base64UrlToBytes = base64UrlToBytes;
460
+ exports.buildDisputeSummary = buildDisputeSummary;
461
+ exports.bytesToBase64Url = bytesToBase64Url;
462
+ exports.checkFileIntegrity = checkFileIntegrity;
463
+ exports.checkTriangleHash = checkTriangleHash;
464
+ exports.checkTsa = checkTsa;
465
+ exports.checkWebAuthn = checkWebAuthn;
466
+ exports.computeTriangleHash = computeTriangleHash;
467
+ exports.computeWebAuthnChallenge = computeWebAuthnChallenge;
468
+ exports.deriveTier = deriveTier;
469
+ exports.describeGap = describeGap;
470
+ exports.disputeSummaryFilename = disputeSummaryFilename;
471
+ exports.extractEmbeddedEvidence = extractEmbeddedEvidence;
472
+ exports.hashArrayBuffer = hashArrayBuffer;
473
+ exports.hashData = hashData;
474
+ exports.runAllChecks = runAllChecks;
475
+ exports.selfieNote = selfieNote;
476
+ exports.timestampGapNote = timestampGapNote;
477
+ exports.uint8ToBase64 = uint8ToBase64;
478
+ exports.utcStamp = utcStamp;
479
+ exports.verifyPackage = verifyPackage;
480
+ //# sourceMappingURL=index.cjs.map
481
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/signwho-crypto.ts","../src/freetsa-cacert.ts","../src/verify-checks.ts","../src/dispute-summary.ts"],"names":["pkijs","PDFDocument","PDFName","PDFDict","PDFArray","PDFRawStream","inflate"],"mappings":"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","file":"index.cjs","sourcesContent":["// ============================================================================\n// SignWho shared crypto — SINGLE SOURCE OF TRUTH\n// ============================================================================\n// These functions are imported by BOTH the signer (App.tsx) and the public\n// verifier (verify.tsx). Keeping one copy is deliberate: if the verifier ever\n// re-implemented a formula and drifted from the signer, it would produce false\n// verdicts — catastrophic for a \"verify it yourself\" product.\n//\n// hashData / hashArrayBuffer / uint8ToBase64 / bytesToBase64Url were RELOCATED\n// verbatim from App.tsx (zero logic change — see signwho-crypto.test.ts for the\n// equivalence check against the old inline implementations). base64UrlToBytes,\n// computeTriangleHash and computeWebAuthnChallenge formalize logic that was\n// previously inline in App.tsx, byte-for-byte identical.\n\n/** SHA-256 of a UTF-8 string → lowercase hex. (Was App.tsx `hashData`.) */\nexport async function hashData(data: string): Promise<string> {\n const encoded = new TextEncoder().encode(data)\n const buffer = await crypto.subtle.digest('SHA-256', encoded)\n return Array.from(new Uint8Array(buffer)).map(b => b.toString(16).padStart(2, '0')).join('')\n}\n\n/** SHA-256 of raw bytes → lowercase hex. (Was App.tsx `hashArrayBuffer`.) Used for\n * documentHash (original upload) AND signedPdfHash / verifier file-integrity. */\nexport async function hashArrayBuffer(buffer: ArrayBuffer): Promise<string> {\n const hash = await crypto.subtle.digest('SHA-256', buffer)\n return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('')\n}\n\n/** Stack-safe base64 encode for large Uint8Arrays. (Was App.tsx `uint8ToBase64`.) */\nexport function uint8ToBase64(bytes: Uint8Array): string {\n let binary = ''\n for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i])\n return btoa(binary)\n}\n\n/** base64url (no padding) of bytes. (Was App.tsx `bytesToBase64Url`.) */\nexport function bytesToBase64Url(bytes: Uint8Array): string {\n return uint8ToBase64(bytes).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\n/** Decode base64url (padded or not) → bytes. */\nexport function base64UrlToBytes(s: string): Uint8Array {\n const pad = s.length % 4 === 0 ? '' : '='.repeat(4 - (s.length % 4))\n const b64 = s.replace(/-/g, '+').replace(/_/g, '/') + pad\n const bin = atob(b64)\n const out = new Uint8Array(bin.length)\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)\n return out\n}\n\n/** Triangle Authentication binding: SHA-256(\"selfieHash||biometricTimestamp||documentHash\") → hex.\n * Order and the literal \"||\" separators must match the signer exactly. */\nexport async function computeTriangleHash(\n selfieHash: string,\n biometricTimestamp: number,\n documentHash: string,\n): Promise<string> {\n return hashData(`${selfieHash}||${biometricTimestamp}||${documentHash}`)\n}\n\n/** Document-bound WebAuthn challenge: base64url(SHA-256(\"documentHash||challengeNonce\")).\n * NOTE the literal \"||\" separator — the challenge is NOT a bare concatenation. */\nexport async function computeWebAuthnChallenge(\n documentHash: string,\n challengeNonce: string,\n): Promise<string> {\n const input = new TextEncoder().encode(`${documentHash}||${challengeNonce}`)\n const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', input))\n return bytesToBase64Url(digest)\n}\n","// FreeTSA root CA certificate (https://freetsa.org/files/cacert.pem), bundled so the\n// verifier can validate the RFC-3161 timestamp token's chain fully offline — no network.\n// Proven (Step 3 spike) to chain a real FreeTSA token end-to-end.\nexport const FREETSA_CA_PEM = `\n-----BEGIN CERTIFICATE-----\nMIIH/zCCBeegAwIBAgIJAMHphhYNqOmAMA0GCSqGSIb3DQEBDQUAMIGVMREwDwYD\nVQQKEwhGcmVlIFRTQTEQMA4GA1UECxMHUm9vdCBDQTEYMBYGA1UEAxMPd3d3LmZy\nZWV0c2Eub3JnMSIwIAYJKoZIhvcNAQkBFhNidXNpbGV6YXNAZ21haWwuY29tMRIw\nEAYDVQQHEwlXdWVyemJ1cmcxDzANBgNVBAgTBkJheWVybjELMAkGA1UEBhMCREUw\nHhcNMTYwMzEzMDE1MjEzWhcNNDEwMzA3MDE1MjEzWjCBlTERMA8GA1UEChMIRnJl\nZSBUU0ExEDAOBgNVBAsTB1Jvb3QgQ0ExGDAWBgNVBAMTD3d3dy5mcmVldHNhLm9y\nZzEiMCAGCSqGSIb3DQEJARYTYnVzaWxlemFzQGdtYWlsLmNvbTESMBAGA1UEBxMJ\nV3VlcnpidXJnMQ8wDQYDVQQIEwZCYXllcm4xCzAJBgNVBAYTAkRFMIICIjANBgkq\nhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtgKODjAy8REQ2WTNqUudAnjhlCrpE6ql\nmQfNppeTmVvZrH4zutn+NwTaHAGpjSGv4/WRpZ1wZ3BRZ5mPUBZyLgq0YrIfQ5Fx\n0s/MRZPzc1r3lKWrMR9sAQx4mN4z11xFEO529L0dFJjPF9MD8Gpd2feWzGyptlel\nb+PqT+++fOa2oY0+NaMM7l/xcNHPOaMz0/2olk0i22hbKeVhvokPCqhFhzsuhKsm\nq4Of/o+t6dI7sx5h0nPMm4gGSRhfq+z6BTRgCrqQG2FOLoVFgt6iIm/BnNffUr7V\nDYd3zZmIwFOj/H3DKHoGik/xK3E82YA2ZulVOFRW/zj4ApjPa5OFbpIkd0pmzxzd\nEcL479hSA9dFiyVmSxPtY5ze1P+BE9bMU1PScpRzw8MHFXxyKqW13Qv7LWw4sbk3\nSciB7GACbQiVGzgkvXG6y85HOuvWNvC5GLSiyP9GlPB0V68tbxz4JVTRdw/Xn/XT\nFNzRBM3cq8lBOAVt/PAX5+uFcv1S9wFE8YjaBfWCP1jdBil+c4e+0tdywT2oJmYB\nBF/kEt1wmGwMmHunNEuQNzh1FtJY54hbUfiWi38mASE7xMtMhfj/C4SvapiDN837\ngYaPfs8x3KZxbX7C3YAsFnJinlwAUss1fdKar8Q/YVs7H/nU4c4Ixxxz4f67fcVq\nM2ITKentbCMCAwEAAaOCAk4wggJKMAwGA1UdEwQFMAMBAf8wDgYDVR0PAQH/BAQD\nAgHGMB0GA1UdDgQWBBT6VQ2MNGZRQ0z357OnbJWveuaklzCBygYDVR0jBIHCMIG/\ngBT6VQ2MNGZRQ0z357OnbJWveuakl6GBm6SBmDCBlTERMA8GA1UEChMIRnJlZSBU\nU0ExEDAOBgNVBAsTB1Jvb3QgQ0ExGDAWBgNVBAMTD3d3dy5mcmVldHNhLm9yZzEi\nMCAGCSqGSIb3DQEJARYTYnVzaWxlemFzQGdtYWlsLmNvbTESMBAGA1UEBxMJV3Vl\ncnpidXJnMQ8wDQYDVQQIEwZCYXllcm4xCzAJBgNVBAYTAkRFggkAwemGFg2o6YAw\nMwYDVR0fBCwwKjAooCagJIYiaHR0cDovL3d3dy5mcmVldHNhLm9yZy9yb290X2Nh\nLmNybDCBzwYDVR0gBIHHMIHEMIHBBgorBgEEAYHyJAEBMIGyMDMGCCsGAQUFBwIB\nFidodHRwOi8vd3d3LmZyZWV0c2Eub3JnL2ZyZWV0c2FfY3BzLmh0bWwwMgYIKwYB\nBQUHAgEWJmh0dHA6Ly93d3cuZnJlZXRzYS5vcmcvZnJlZXRzYV9jcHMucGRmMEcG\nCCsGAQUFBwICMDsaOUZyZWVUU0EgdHJ1c3RlZCB0aW1lc3RhbXBpbmcgU29mdHdh\ncmUgYXMgYSBTZXJ2aWNlIChTYWFTKTA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUH\nMAGGG2h0dHA6Ly93d3cuZnJlZXRzYS5vcmc6MjU2MDANBgkqhkiG9w0BAQ0FAAOC\nAgEAaK9+v5OFYu9M6ztYC+L69sw1omdyli89lZAfpWMMh9CRmJhM6KBqM/ipwoLt\nnxyxGsbCPhcQjuTvzm+ylN6VwTMmIlVyVSLKYZcdSjt/eCUN+41K7sD7GVmxZBAF\nILnBDmTGJmLkrU0KuuIpj8lI/E6Z6NnmuP2+RAQSHsfBQi6sssnXMo4HOW5gtPO7\ngDrUpVXID++1P4XndkoKn7Svw5n0zS9fv1hxBcYIHPPQUze2u30bAQt0n0iIyRLz\naWuhtpAtd7ffwEbASgzB7E+NGF4tpV37e8KiA2xiGSRqT5ndu28fgpOY87gD3ArZ\nDctZvvTCfHdAS5kEO3gnGGeZEVLDmfEsv8TGJa3AljVa5E40IQDsUXpQLi8G+UC4\n1DWZu8EVT4rnYaCw1VX7ShOR1PNCCvjb8S8tfdudd9zhU3gEB0rxdeTy1tVbNLXW\n99y90xcwr1ZIDUwM/xQ/noO8FRhm0LoPC73Ef+J4ZBdrvWwauF3zJe33d4ibxEcb\n8/pz5WzFkeixYM2nsHhqHsBKw7JPouKNXRnl5IAE1eFmqDyC7G/VT7OF669xM6hb\nUt5G21JE4cNK6NNucS+fzg1JPX0+3VhsYZjj7D5uljRvQXrJ8iHgr/M6j2oLHvTA\nI2MLdq2qjZFDOCXsxBxJpbmLGBx9ow6ZerlUxzws2AWv2pk=\n-----END CERTIFICATE-----\n`\n","// ============================================================================\n// SignWho public verifier — the actual checks (pure, framework-free, testable).\n// Everything runs locally in the caller's environment (browser); no network.\n// Uses the SHARED signing crypto (./signwho-crypto) so the verifier can never\n// drift from the signer over a formula.\n// ============================================================================\nimport * as pkijs from 'pkijs'\nimport { PDFDocument, PDFName, PDFDict, PDFArray, PDFRawStream } from 'pdf-lib'\nimport { inflate } from 'pako' // pako 3: named exports only (no default export in its ESM build)\nimport {\n hashArrayBuffer, base64UrlToBytes,\n computeTriangleHash, computeWebAuthnChallenge,\n} from './signwho-crypto'\nimport { FREETSA_CA_PEM } from './freetsa-cacert'\n\nexport type CheckStatus = 'pass' | 'fail' | 'skip' | 'info'\n// `facts`: values a check AUTHENTICATED while passing (e.g. the TSA's genTime and serial, read\n// from the signed token rather than from the evidence's own copy). Machine-readable so the\n// dispute summary reports verified facts; the UI shows `detail`.\nexport interface CheckResult { id: string; label: string; status: CheckStatus; detail: string; facts?: Record<string, string> }\n\n// Minimal shape of the standalone evidence.json (see SignatureEvidence in App.tsx).\nexport interface Evidence {\n documentHash?: string\n selfieHash?: string\n biometricTimestamp?: number\n triangleHash?: string\n signedPdfHash?: string\n faceDetected?: boolean\n selfieIncluded?: boolean // explicitly false on the no-selfie path (TSA covers documentHash)\n signerName?: string\n documentName?: string\n webauthnProof?: {\n publicKey?: string; alg?: number; authenticatorData?: string\n clientDataJSON?: string; signature?: string; challenge?: string; challengeNonce?: string\n }\n tsaTimestamp?: { token?: string; genTime?: string | null; tsa?: string; serial?: string | null }\n // Not verification inputs — read only by the dispute summary (lib/dispute-summary.ts), which\n // reports what the package carries and says \"not recorded\" for what it does not.\n authMethod?: 'biometric' | 'passkey' | 'manual'\n timestamp?: number\n completedAt?: number\n senderEmail?: string\n auditEvents?: { type: string; timestamp: number; ip: string | null; senderEmail: string | null; signerEmail: string | null }[]\n}\n\nconst toHex = (b: Uint8Array) => Array.from(b).map(x => x.toString(16).padStart(2, '0')).join('')\nconst eqHex = (a?: string, b?: string) => !!a && !!b && a.toLowerCase() === b.toLowerCase()\nconst ab = (u: Uint8Array): ArrayBuffer => u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength) as ArrayBuffer\n\nfunction base64ToBytes(s: string): Uint8Array {\n const bin = atob(s.replace(/\\s+/g, ''))\n const out = new Uint8Array(bin.length)\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)\n return out\n}\n\n// DER ECDSA SEQUENCE{INTEGER r, INTEGER s} → raw r‖s (each left-padded to `size`).\nfunction derEcdsaToRaw(der: Uint8Array, size: number): Uint8Array {\n let i = 0\n if (der[i++] !== 0x30) throw new Error('bad der seq')\n if (der[i] & 0x80) { i += 1 + (der[i] & 0x7f) } else i++\n if (der[i++] !== 0x02) throw new Error('bad der r')\n let rl = der[i++]; let r = der.slice(i, i + rl); i += rl\n if (der[i++] !== 0x02) throw new Error('bad der s')\n let sl = der[i++]; let s = der.slice(i, i + sl)\n const strip = (x: Uint8Array) => { let j = 0; while (j < x.length - 1 && x[j] === 0) j++; return x.slice(j) }\n r = strip(r); s = strip(s)\n const out = new Uint8Array(size * 2)\n out.set(r, size - r.length); out.set(s, size * 2 - s.length)\n return out\n}\n\nconst curveSize = (c?: string) => (c === 'P-521' ? 66 : c === 'P-384' ? 48 : 32)\nconst DIGEST_OIDS: Record<string, string> = {\n '2.16.840.1.101.3.4.2.1': 'SHA-256', '2.16.840.1.101.3.4.2.2': 'SHA-384',\n '2.16.840.1.101.3.4.2.3': 'SHA-512', '1.3.14.3.2.26': 'SHA-1',\n}\n\n// In the browser pkijs auto-uses window.crypto; in Node (tests) we wire globalThis.crypto.\nfunction ensurePkijsEngine() {\n try { const e = pkijs.getEngine(); if (e && (e as { subtle?: unknown }).subtle) return } catch { /* set below */ }\n pkijs.setEngine('signwho', new pkijs.CryptoEngine({ name: 'signwho', crypto: globalThis.crypto }))\n}\n\n// ---- 1. File integrity ----\nexport async function checkFileIntegrity(pdfBytes: Uint8Array | null, ev: Evidence): Promise<CheckResult> {\n const id = 'file', label = 'File matches the signed record'\n if (!ev.signedPdfHash) return { id, label, status: 'skip', detail: 'This evidence.json has no signedPdfHash. Use the STANDALONE evidence.json from the email/download (the copy embedded in the PDF omits it by design).' }\n if (!pdfBytes) return { id, label, status: 'skip', detail: 'Provide the signed PDF to check it against signedPdfHash.' }\n try {\n const hex = await hashArrayBuffer(ab(pdfBytes))\n return eqHex(hex, ev.signedPdfHash)\n ? { id, label, status: 'pass', detail: `SHA-256 of the PDF you provided equals evidence.signedPdfHash (${hex.slice(0, 16)}…).` }\n : { id, label, status: 'fail', detail: `SHA-256 of the provided PDF (${hex.slice(0, 16)}…) does NOT match evidence.signedPdfHash (${ev.signedPdfHash.slice(0, 16)}…). This is not the delivered file, or it was modified.` }\n } catch (e) { return { id, label, status: 'fail', detail: 'Could not hash the PDF: ' + (e as Error).message } }\n}\n\n// ---- 2. Triangle hash ----\nexport async function checkTriangleHash(ev: Evidence): Promise<CheckResult> {\n const id = 'triangle', label = 'Triangle Authentication binding intact'\n if (ev.selfieIncluded === false)\n return { id, label, status: 'skip', detail: 'No selfie — the signer completed without a camera capture, so there is no selfie/time/document Triangle binding for this signature (by design, not a failure).' }\n if (!ev.selfieHash || ev.biometricTimestamp == null || !ev.documentHash || !ev.triangleHash)\n return { id, label, status: 'skip', detail: 'Missing fields (selfieHash / biometricTimestamp / documentHash / triangleHash).' }\n try {\n const recomputed = await computeTriangleHash(ev.selfieHash, ev.biometricTimestamp, ev.documentHash)\n return eqHex(recomputed, ev.triangleHash)\n ? { id, label, status: 'pass', detail: 'SHA-256(selfieHash || biometricTimestamp || documentHash) equals evidence.triangleHash — the three artifacts are bound together.' }\n : { id, label, status: 'fail', detail: 'Recomputed triangleHash does not match evidence.triangleHash — a bound component was altered.' }\n } catch (e) { return { id, label, status: 'fail', detail: (e as Error).message } }\n}\n\n// ---- 3. WebAuthn assertion ----\nexport async function checkWebAuthn(ev: Evidence): Promise<CheckResult> {\n const id = 'webauthn', label = 'Signature cryptographically verified (WebAuthn)'\n const p = ev.webauthnProof\n if (!p || !p.signature) return { id, label, status: 'skip', detail: 'No WebAuthn assertion in this signature (signer had no usable platform/roaming authenticator — recorded as manual/passkey).' }\n if (!ev.documentHash || !p.challengeNonce || !p.clientDataJSON || !p.authenticatorData || !p.publicKey || (p.alg !== -7 && p.alg !== -257))\n return { id, label, status: 'fail', detail: 'Assertion is missing required fields (publicKey/alg/authenticatorData/clientDataJSON/challengeNonce).' }\n try {\n // (a) recompute the document-bound challenge and confirm it matches the signed clientDataJSON\n const expected = await computeWebAuthnChallenge(ev.documentHash, p.challengeNonce)\n const cdj = JSON.parse(new TextDecoder().decode(base64UrlToBytes(p.clientDataJSON)))\n if (cdj.type !== 'webauthn.get') return { id, label, status: 'fail', detail: `clientDataJSON.type is \"${cdj.type}\", expected \"webauthn.get\".` }\n if (cdj.challenge !== expected) return { id, label, status: 'fail', detail: 'The challenge in clientDataJSON is not SHA-256(documentHash || challengeNonce) — the assertion is not bound to this document.' }\n if (p.challenge && p.challenge !== expected) return { id, label, status: 'fail', detail: 'Stored challenge does not match the recomputed document-bound challenge.' }\n\n // (b) verify the signature over authenticatorData ‖ SHA-256(clientDataJSON)\n const authData = base64UrlToBytes(p.authenticatorData)\n const cdjBytes = base64UrlToBytes(p.clientDataJSON)\n const cdjHash = new Uint8Array(await crypto.subtle.digest('SHA-256', ab(cdjBytes)))\n const signed = new Uint8Array(authData.length + cdjHash.length)\n signed.set(authData, 0); signed.set(cdjHash, authData.length)\n const spki = base64UrlToBytes(p.publicKey)\n const sig = base64UrlToBytes(p.signature)\n\n let ok: boolean\n if (p.alg === -7) {\n const key = await crypto.subtle.importKey('spki', ab(spki), { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify'])\n ok = await crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, key, ab(derEcdsaToRaw(sig, 32)), ab(signed))\n } else {\n const key = await crypto.subtle.importKey('spki', ab(spki), { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['verify'])\n ok = await crypto.subtle.verify({ name: 'RSASSA-PKCS1-v1_5' }, key, ab(sig), ab(signed))\n }\n if (!ok) return { id, label, status: 'fail', detail: 'The WebAuthn assertion signature did not verify against the stored public key.' }\n const uv = (authData[32] & 0x04) !== 0\n return { id, label, status: 'pass', detail: `Assertion over the document-bound challenge verifies against the public key (${p.alg === -7 ? 'ES256' : 'RS256'})${uv ? ', user verification (biometric/PIN) flag set' : ''}. Proves the private key holder signed THIS document.` }\n } catch (e) { return { id, label, status: 'fail', detail: 'Verification error: ' + (e as Error).message } }\n}\n\n// ---- 4. RFC-3161 timestamp (FreeTSA) ----\nexport async function checkTsa(ev: Evidence): Promise<CheckResult> {\n const id = 'tsa', label = 'Timestamp valid (FreeTSA, non-qualified)'\n const t = ev.tsaTimestamp\n if (!t || !t.token) return { id, label, status: 'skip', detail: 'No RFC-3161 timestamp (best-effort at signing; FreeTSA may have been unreachable).' }\n // With a selfie the TSA covers the triangleHash; on the no-selfie path it covers the documentHash.\n const tsaTarget = ev.triangleHash || ev.documentHash\n const tsaTargetName = ev.triangleHash ? 'triangleHash' : 'documentHash'\n if (!tsaTarget) return { id, label, status: 'fail', detail: 'Cannot check the timestamp without a triangleHash or documentHash to match.' }\n try {\n ensurePkijsEngine()\n const tokenBytes = base64ToBytes(t.token)\n const ci = pkijs.ContentInfo.fromBER(ab(tokenBytes))\n const sd = new pkijs.SignedData({ schema: ci.content })\n const si = sd.signerInfos[0]\n const digAlg = DIGEST_OIDS[si.digestAlgorithm.algorithmId] || 'SHA-256'\n\n const ecView = si ? sd.encapContentInfo.eContent!.valueBlock.valueHexView : new Uint8Array()\n const eContent = ab(ecView)\n const tst = pkijs.TSTInfo.fromBER(eContent)\n\n // (a) messageImprint == the recorded triangleHash (with selfie) or documentHash (no selfie)\n const mi = toHex(new Uint8Array(tst.messageImprint.hashedMessage.valueBlock.valueHexView))\n if (!eqHex(mi, tsaTarget)) return { id, label, status: 'fail', detail: `The timestamp covers a different hash than evidence.${tsaTargetName}.` }\n\n // (b) messageDigest signed-attr == digest(eContent)\n let md: Uint8Array | null = null\n for (const a of si.signedAttrs?.attributes || []) if (a.type === '1.2.840.113549.1.9.4') md = new Uint8Array(a.values[0].valueBlock.valueHexView)\n const ecHash = new Uint8Array(await crypto.subtle.digest(digAlg, eContent))\n if (!md || toHex(md) !== toHex(ecHash)) return { id, label, status: 'fail', detail: 'The signed messageDigest does not match the timestamp content.' }\n\n // (c) SignerInfo signature over signedAttrs (DER SET, tag 0x31)\n const sa = new Uint8Array((si.signedAttrs!.encodedValue as ArrayBuffer).slice(0)); sa[0] = 0x31\n const signerCert = (sd.certificates as pkijs.Certificate[] | undefined)?.find(c => { try { return c.serialNumber.isEqual(si.sid.serialNumber) } catch { return false } }) || (sd.certificates as pkijs.Certificate[])[0]\n const isEc = si.signatureAlgorithm.algorithmId.startsWith('1.2.840.10045')\n let sigOk: boolean\n if (isEc) {\n const pub = await signerCert.getPublicKey()\n const raw = derEcdsaToRaw(new Uint8Array(si.signature.valueBlock.valueHexView), curveSize((pub.algorithm as EcKeyAlgorithm).namedCurve))\n sigOk = await crypto.subtle.verify({ name: 'ECDSA', hash: digAlg }, pub, ab(raw), ab(sa))\n } else {\n const pub = await signerCert.getPublicKey({ algorithm: { algorithm: { name: 'RSASSA-PKCS1-v1_5', hash: digAlg }, usages: ['verify'] } })\n sigOk = await crypto.subtle.verify({ name: 'RSASSA-PKCS1-v1_5' }, pub, ab(new Uint8Array(si.signature.valueBlock.valueHexView)), ab(sa))\n }\n if (!sigOk) return { id, label, status: 'fail', detail: 'The TSA signature over the timestamp did not verify.' }\n\n // (d) chain the TSA signing cert to the bundled FreeTSA CA — AS OF genTime, NOT \"now\". A\n // timestamp proves a fact as of genTime, so the chain must be valid then; validating as of now\n // would make a signature that was valid at signing read FAIL once FreeTSA's signing cert\n // eventually expires (breaking long-term verifiability). genTime comes from the TSTInfo, which\n // steps (b)+(c) already authenticated — so it can't be attacker-shifted without breaking the\n // signature; the bundled CA stays the trust anchor.\n const genTimeDate = tst.genTime instanceof Date ? tst.genTime : new Date(String(tst.genTime))\n if (isNaN(genTimeDate.getTime())) return { id, label, status: 'fail', detail: 'The timestamp has no readable genTime.' }\n const nb = signerCert.notBefore.value, na = signerCert.notAfter.value\n if (genTimeDate < nb || genTimeDate > na) return { id, label, status: 'fail', detail: `genTime (${genTimeDate.toISOString()}) is outside the TSA signing certificate's validity window (${nb.toISOString()} … ${na.toISOString()}).` }\n const pem = FREETSA_CA_PEM.replace(/-----[^-]+-----/g, '').replace(/\\s+/g, '')\n const caDer = base64ToBytes(pem)\n const ca = pkijs.Certificate.fromBER(ab(caDer))\n const chain = await new pkijs.CertificateChainValidationEngine({ certs: [signerCert], trustedCerts: [ca], checkDate: genTimeDate }).verify()\n if (!chain.result) return { id, label, status: 'fail', detail: 'The TSA certificate does not chain to the bundled FreeTSA CA as of genTime (code ' + chain.resultCode + ').' }\n\n const gt = genTimeDate.toISOString()\n return { id, label, status: 'pass', facts: { genTime: genTimeDate.toISOString(), serial: toHex(new Uint8Array(tst.serialNumber.valueBlock.valueHexView)) }, detail: `FreeTSA attests this signature's ${tsaTargetName} existed at ${gt} (RFC-3161, ${digAlg}; chain validated to the bundled FreeTSA CA as of that time). Non-qualified TSA.` }\n } catch (e) { return { id, label, status: 'fail', detail: 'Timestamp parse/verify error: ' + (e as Error).message } }\n}\n\n// ---- Honest non-cryptographic note: the selfie is captured, NOT identity-verified ----\nexport function selfieNote(ev: Evidence): CheckResult {\n if (ev.selfieIncluded === false) {\n return {\n id: 'selfie', label: 'No selfie — not captured', status: 'info',\n detail: `This signature was completed WITHOUT a selfie (the signer couldn't or chose not to use a camera). ${ev.webauthnProof ? 'It is device-verified (WebAuthn) and timestamped, but carries no photographic evidence and no identity binding.' : 'It carries neither a selfie nor a device signature — it is document + timestamp only, the lowest assurance tier.'}`,\n }\n }\n return {\n id: 'selfie', label: 'Selfie captured — not identity-verified', status: 'info',\n detail: `A selfie was captured at signing (hash bound into the Triangle, verified above). ${ev.faceDetected ? 'A face was detected by an in-browser heuristic. ' : ''}This is NOT identity verification — SignWho does not match the face to a government ID or a real-world identity.`,\n }\n}\n\n// ---- Honest consistency flag: the TSA time vs the signer's self-declared signing time ----\n// The token is requested seconds after the ceremony, so a gap of more than a few minutes\n// between the AUTHENTICATED genTime (from the passing TSA check) and evidence.timestamp is\n// something a reviewer should see. It is a NOTE, never a failure: the timestamp itself is\n// still valid, and `timestamp` is not a verification input. Returns null when there is no\n// passing TSA check, no recorded signing time, or the gap is within the threshold.\nexport const TSA_GAP_NOTE_THRESHOLD_MS = 10 * 60 * 1000\n\nexport function describeGap(ms: number): string {\n const abs = Math.abs(ms)\n const minutes = Math.round(abs / 60_000)\n if (minutes < 120) return `${minutes} minute${minutes === 1 ? '' : 's'}`\n const hours = Math.round(abs / 3_600_000)\n if (hours < 48) return `${hours} hours`\n const days = Math.round(abs / 86_400_000)\n return `${days} days`\n}\n\nexport function timestampGapNote(ev: Evidence, tsaCheck: CheckResult | undefined): CheckResult | null {\n if (!tsaCheck || tsaCheck.status !== 'pass' || !tsaCheck.facts?.genTime) return null\n if (typeof ev.timestamp !== 'number' || !Number.isFinite(ev.timestamp)) return null\n const genMs = Date.parse(tsaCheck.facts.genTime)\n if (Number.isNaN(genMs)) return null\n const gap = genMs - ev.timestamp\n if (Math.abs(gap) <= TSA_GAP_NOTE_THRESHOLD_MS) return null\n const when = gap > 0 ? 'after' : 'before'\n return {\n id: 'tsa-gap', label: `Timestamp obtained ${describeGap(gap)} ${when} the recorded signing time`, status: 'info',\n detail: `The independent timestamp was obtained ${describeGap(gap)} ${when} the recorded signing time (genTime ${tsaCheck.facts.genTime}, evidence.timestamp ${new Date(ev.timestamp).toISOString()}). The token is still valid — it proves the hash existed at genTime — but the signer's declared time and the third-party time disagree by more than ${TSA_GAP_NOTE_THRESHOLD_MS / 60_000} minutes.`,\n facts: { gapMs: String(gap) },\n }\n}\n\nexport async function runAllChecks(pdfBytes: Uint8Array | null, ev: Evidence): Promise<CheckResult[]> {\n const tsa = await checkTsa(ev)\n const gap = timestampGapNote(ev, tsa)\n return [\n await checkFileIntegrity(pdfBytes, ev),\n await checkWebAuthn(ev),\n tsa,\n await checkTriangleHash(ev),\n selfieNote(ev),\n ...(gap ? [gap] : []),\n ]\n}\n\n// ---- Embedded evidence (the \"save the PDF = hold the evidence\" path) ----\n// Best-effort: pull the evidence.json embedded inside the signed PDF. The embedded copy omits\n// signedPdfHash by design, so file-integrity still needs the standalone evidence.json. Returns\n// null if no embedded evidence is found. Relocated verbatim from verify.tsx so the embedded path\n// is testable outside the UI.\nexport async function extractEmbeddedEvidence(pdfBytes: Uint8Array): Promise<Evidence | null> {\n try {\n const doc = await PDFDocument.load(pdfBytes, { ignoreEncryption: true, updateMetadata: false })\n const names = doc.catalog.lookupMaybe(PDFName.of('Names'), PDFDict)\n const ef = names?.lookupMaybe(PDFName.of('EmbeddedFiles'), PDFDict)\n const arr = ef?.lookupMaybe(PDFName.of('Names'), PDFArray)\n if (!arr) return null\n for (let i = 0; i + 1 < arr.size(); i += 2) {\n const filespec = arr.lookup(i + 1, PDFDict)\n const efDict = filespec.lookupMaybe(PDFName.of('EF'), PDFDict)\n const stream = efDict?.lookup(PDFName.of('F')) as unknown as PDFRawStream | undefined\n if (!stream || !(stream instanceof PDFRawStream)) continue\n let bytes: Uint8Array = stream.contents\n const filter = stream.dict.get(PDFName.of('Filter'))\n if (filter && filter.toString().includes('FlateDecode')) bytes = inflate(bytes)\n const text = new TextDecoder().decode(bytes)\n if (text.trim().startsWith('{')) return JSON.parse(text) as Evidence\n }\n } catch { /* fall through to null */ }\n return null\n}\n\n// ---- Assurance tier ----\n// Derived ONLY from the evidence the checks actually ran on and the results of those checks —\n// never from UI state. (verify.tsx once derived it from a React `evidence` state that stayed\n// null when the evidence came out of the PDF, so a full Triangle signature was labelled\n// \"device-verified, no selfie\" while the triangle check itself read PASS.)\n//\n// identity — the package carries a selfie (selfieHash + triangleHash): the full Triangle.\n// device — no selfie, but the WebAuthn assertion verified: a device-held key signed it.\n// floor — no selfie and no verified device signature: document + timestamp only.\n//\n// The tier describes what the package CLAIMS and, for `device`, what verified. Whether the\n// identity-tier claims hold is what `allGood` on the report says — the UI shows a tier only when\n// every applicable check passed.\nexport type AssuranceTier = 'identity' | 'device' | 'floor'\n\nexport function deriveTier(ev: Evidence, checks: CheckResult[]): AssuranceTier {\n const noSelfie = ev.selfieIncluded === false || (!ev.triangleHash && !ev.selfieHash)\n if (!noSelfie) return 'identity'\n const webauthnPass = checks.find(c => c.id === 'webauthn')?.status === 'pass'\n return webauthnPass ? 'device' : 'floor'\n}\n\n// One object that carries the evidence that was checked, the check results, and everything\n// derived from them. The UI stores and displays this; it never re-derives from its own state.\nexport interface VerificationReport {\n evidence: Evidence\n checks: CheckResult[]\n tier: AssuranceTier\n passes: number\n fails: number\n allGood: boolean\n}\n\nexport async function verifyPackage(pdfBytes: Uint8Array | null, ev: Evidence): Promise<VerificationReport> {\n const checks = await runAllChecks(pdfBytes, ev)\n const passes = checks.filter(c => c.status === 'pass').length\n const fails = checks.filter(c => c.status === 'fail').length\n return { evidence: ev, checks, tier: deriveTier(ev, checks), passes, fails, allGood: fails === 0 && passes > 0 }\n}\n","// ============================================================================\n// Dispute summary — a plain-text digest of a verified signature package, in the order a card\n// network's dispute form asks for evidence (Stripe's `evidence` object: customer name and\n// e-mail, the customer's IP, when the service was delivered, the customer's signature and the\n// service documentation, then the rest). Built in the browser from the VerificationReport that\n// /verify already holds — nothing is fetched and nothing is inferred: a field the package does\n// not carry says so instead of being left blank, so the merchant never pastes a guess into a\n// dispute response. Pure (no DOM): tested in dispute-summary.test.ts.\n// ============================================================================\nimport type { VerificationReport, CheckResult } from './verify-checks'\n\nexport interface DisputeSummaryInputs {\n report: VerificationReport\n generatedAt: Date\n pdfName?: string // the file the user dropped\n evidenceFileName?: string // the evidence.json the user dropped\n evidenceSource: 'standalone' | 'embedded' | null // where the evidence came from\n isSample?: boolean // the bundled demo package\n}\n\nexport interface DisputeSummary { text: string; filename: string }\n\nexport const VERIFY_URL = 'https://signwho.com/verify'\nconst NOT_RECORDED = 'not recorded in this package'\n\n// \"2026-06-14 07:22:56 UTC\" from epoch milliseconds or an ISO string; null when absent/invalid.\nexport function utcStamp(value: number | string | null | undefined): string | null {\n if (value === null || value === undefined || value === '') return null\n const d = new Date(value)\n if (Number.isNaN(d.getTime())) return null\n return d.toISOString().replace('T', ' ').replace(/\\.\\d{3}Z$/, ' UTC')\n}\n\n// The download name: the document's base name with anything outside [A-Za-z0-9._-] collapsed\n// to '-', so it is safe on every OS and readable (\"NDA (final).pdf\" → \"NDA-final\").\nexport function disputeSummaryFilename(documentName: string | undefined): string {\n const base = (documentName || '').replace(/\\.pdf$/i, '').replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '')\n return `SignWho-Dispute-Summary-${base || 'document'}.txt`\n}\n\nconst TIER_LABEL = {\n identity: 'Identity-bound — selfie, device signature and independent timestamp',\n device: 'Device-verified, no selfie — device signature and independent timestamp',\n floor: 'Document + timestamp only — neither identity nor device verified',\n} as const\n\nconst TAG: Record<CheckResult['status'], string> = { pass: 'PASS', fail: 'FAIL', skip: 'N/A ', info: 'NOTE' }\n\nexport function buildDisputeSummary(input: DisputeSummaryInputs): DisputeSummary {\n const { report, generatedAt, evidenceSource, isSample } = input\n const ev = report.evidence\n const events = ev.auditEvents || []\n const created = events.find(e => e.type === 'document_created')\n const viewed = events.find(e => e.type === 'document_viewed')\n const signed = events.find(e => e.type === 'document_signed')\n\n const customerName = ev.signerName || NOT_RECORDED\n const customerEmail = signed?.signerEmail || viewed?.signerEmail || NOT_RECORDED\n const merchantEmail = ev.senderEmail || created?.senderEmail || NOT_RECORDED\n const signedAt = utcStamp(signed?.timestamp ?? ev.completedAt ?? ev.timestamp)\n // The independent timestamp: what the TSA check AUTHENTICATED from the signed token (facts),\n // not the evidence's own copy of it. The copy is used only when the check did not pass, and\n // is labelled as unverified.\n const tsaCheck = report.checks.find(c => c.id === 'tsa')\n const tsaVerified = tsaCheck?.status === 'pass' && !!tsaCheck.facts?.genTime\n const genTime = utcStamp(tsaVerified ? tsaCheck.facts!.genTime : ev.tsaTimestamp?.genTime)\n const tsaSerial = tsaVerified ? tsaCheck.facts!.serial : ev.tsaTimestamp?.serial\n const noSelfie = ev.selfieIncluded === false || (!ev.selfieHash && !ev.triangleHash)\n\n const ipValue = (e?: { ip: string | null; timestamp: number }) => e?.ip ? `${e.ip} (${utcStamp(e.timestamp)})` : null\n\n const L: string[] = []\n const section = (n: number, title: string, stripe: string) => { L.push('', `${n}. ${title}`, ` (Stripe evidence fields: ${stripe})`) }\n const row = (label: string, value: string | null | undefined) => { L.push(` ${label.padEnd(28)}${value || NOT_RECORDED}`) }\n\n L.push('SIGNWHO — PAYMENT DISPUTE SUMMARY')\n L.push(`Generated ${utcStamp(generatedAt.getTime())} by the independent verifier at ${VERIFY_URL}.`)\n L.push('Every check below ran in the browser from the two files alone; no SignWho server was consulted.')\n if (isSample) L.push('THIS IS SIGNWHO\\'S BUNDLED SAMPLE PACKAGE — a demonstration, not a real agreement.')\n if (!report.allGood) {\n L.push('', `WARNING: ${report.fails} check(s) FAILED — this package does not verify. Do not rely on it as-is.`)\n }\n\n section(1, 'CUSTOMER', 'customer_name, customer_email_address')\n row('Name (as signed):', customerName)\n row('E-mail:', customerEmail)\n row('Assurance level:', TIER_LABEL[report.tier])\n\n section(2, 'CUSTOMER IP ADDRESSES', 'customer_purchase_ip')\n row('Viewed the document from:', ipValue(viewed))\n row('Signed the document from:', ipValue(signed))\n\n section(3, 'TIMESTAMPS (UTC)', 'service_date, customer_communication')\n row('Document sent:', utcStamp(created?.timestamp))\n row('Document viewed:', utcStamp(viewed?.timestamp))\n row('Document signed:', signedAt)\n row('Independent timestamp:', genTime\n ? `${genTime} — ${ev.tsaTimestamp?.tsa || 'RFC 3161 TSA'}${tsaSerial ? `, token serial ${tsaSerial}` : ''} ${tsaVerified ? '(RFC 3161, third party)' : '(declared in the evidence, NOT verified by the timestamp check)'}`\n : null)\n // The verifier's consistency flag (tsa-gap, status \"info\"): the third-party time and the\n // signer's declared time disagree by more than a few minutes. Surfaced next to the times it\n // is about, in addition to its row in the check results below.\n const gapNote = report.checks.find(c => c.id === 'tsa-gap')\n if (gapNote) L.push(` NOTE: ${gapNote.detail}`)\n\n section(4, 'DOCUMENT', 'customer_signature, service_documentation')\n row('Document name:', ev.documentName)\n row('Sent by (merchant):', merchantEmail)\n row('SHA-256, signed PDF:', ev.signedPdfHash\n || (evidenceSource === 'embedded' ? 'not present — the evidence was read from inside the PDF; the standalone evidence.json carries the file hash' : null))\n row('SHA-256, original document:', ev.documentHash)\n row('Signature method:', ev.authMethod === 'biometric' ? 'device biometric (Face ID / fingerprint / Windows Hello) — WebAuthn assertion'\n : ev.authMethod === 'passkey' ? 'device passkey — WebAuthn assertion'\n : ev.authMethod === 'manual' ? 'manual (no device signature)' : ev.webauthnProof ? 'WebAuthn assertion' : null)\n row('Customer\\'s face:', noSelfie\n ? 'no selfie was captured for this signature'\n : `a selfie was captured at signing (SHA-256 ${ev.selfieHash || NOT_RECORDED}); the image is printed on the certificate page of the signed PDF`)\n\n section(5, `VERIFICATION RESULTS — ${report.passes} of ${report.checks.filter(c => c.status !== 'info').length} checks passed`, 'uncategorized_text')\n for (const c of report.checks) {\n L.push(` [${TAG[c.status]}] ${c.label}`)\n L.push(` ${c.detail}`)\n }\n\n L.push('', '6. HOW TO RE-VERIFY (for the card network or the customer\\'s bank)')\n L.push(` Open the signed PDF and the evidence.json at ${VERIFY_URL} — the checks run in the browser, nothing is uploaded.`)\n L.push(' Attach both files to the dispute: the signed PDF as the customer\\'s signature, the evidence.json as the service documentation.')\n if (input.pdfName || input.evidenceFileName) {\n L.push(` Files verified for this summary: ${[input.pdfName, input.evidenceFileName].filter(Boolean).join(', ')}`)\n }\n L.push(' The algorithms are public standards (SHA-256, WebAuthn/FIDO2, RFC 3161); anyone can re-run them with open tools.')\n L.push('')\n\n return { text: L.join('\\n'), filename: disputeSummaryFilename(ev.documentName) }\n}\n"]}