@page-scanner/cli 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +6 -1
  2. package/dist/bin.js +1 -1
  3. package/dist/cli/args.d.ts +9 -3
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +35 -0
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/commands.d.ts.map +1 -1
  8. package/dist/cli/commands.js +27 -0
  9. package/dist/cli/commands.js.map +1 -1
  10. package/dist/cli/format.d.ts +1 -1
  11. package/dist/daemon/client.d.ts +32 -2
  12. package/dist/daemon/client.d.ts.map +1 -1
  13. package/dist/daemon/client.js +63 -8
  14. package/dist/daemon/client.js.map +1 -1
  15. package/dist/daemon/rpc-server.js +2 -2
  16. package/dist/daemon/run.d.ts +6 -0
  17. package/dist/daemon/run.d.ts.map +1 -1
  18. package/dist/daemon/run.js +12 -6
  19. package/dist/daemon/run.js.map +1 -1
  20. package/dist/daemon/self.d.ts +20 -0
  21. package/dist/daemon/self.d.ts.map +1 -1
  22. package/dist/daemon/self.js +22 -0
  23. package/dist/daemon/self.js.map +1 -1
  24. package/dist/daemon/state.js +1 -1
  25. package/dist/daemon/state.js.map +1 -1
  26. package/dist/diff/image.js +1 -1
  27. package/dist/diff/image.js.map +1 -1
  28. package/dist/diff/png.d.ts +1 -1
  29. package/dist/diff/png.d.ts.map +1 -1
  30. package/dist/diff/png.js +4 -4
  31. package/dist/diff/png.js.map +1 -1
  32. package/dist/diff/text.js +4 -4
  33. package/dist/errors.d.ts +1 -1
  34. package/dist/errors.js +1 -1
  35. package/dist/index.d.ts +2 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +2 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/output.d.ts +24 -0
  40. package/dist/output.d.ts.map +1 -1
  41. package/dist/output.js +45 -2
  42. package/dist/output.js.map +1 -1
  43. package/dist/verify.d.ts +31 -0
  44. package/dist/verify.d.ts.map +1 -0
  45. package/dist/verify.js +115 -0
  46. package/dist/verify.js.map +1 -0
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/package.json +1 -1
package/dist/verify.js ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * `page-scanner verify` (#55): checks a file against the integrity record the
3
+ * extension wrote beside it.
4
+ *
5
+ * Two questions, answered separately. Is this the file that was exported? Its
6
+ * SHA-256 and length against the record's. And, when the record is signed,
7
+ * does the signature hold over the record, with the key the record names?
8
+ * The second only says the holder of that key made the record; whether that
9
+ * key is the person it should be is the thumbprint's to answer, which is why
10
+ * it is printed. Neither says anything about the time, which is the capturing
11
+ * computer's clock, or about what the page showed anyone else.
12
+ *
13
+ * The signed bytes are the record's canonical JSON without `signature`: keys
14
+ * sorted at every depth, no whitespace, the extension's `canonicalJson`.
15
+ */
16
+ import { createHash, webcrypto } from 'node:crypto';
17
+ import { existsSync, readFileSync } from 'node:fs';
18
+ import { resolve } from 'node:path';
19
+ import { PageScannerError } from './errors.js';
20
+ export const RECORD_SUFFIX = '.integrity.json';
21
+ const RECORD_FORMAT = 'page-scanner-integrity/1';
22
+ /** The extension's `canonicalJson`: the bytes its signature covers. */
23
+ export function canonicalJson(value) {
24
+ if (Array.isArray(value))
25
+ return `[${value.map(canonicalJson).join(',')}]`;
26
+ if (value && typeof value === 'object') {
27
+ const entries = Object.entries(value)
28
+ .filter(([, item]) => item !== undefined)
29
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
30
+ return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(',')}}`;
31
+ }
32
+ return JSON.stringify(value);
33
+ }
34
+ function badRequest(message, hint) {
35
+ return new PageScannerError('BAD_REQUEST', message, hint);
36
+ }
37
+ function readRecord(path) {
38
+ let parsed;
39
+ try {
40
+ parsed = JSON.parse(readFileSync(path, 'utf8'));
41
+ }
42
+ catch (error) {
43
+ throw badRequest(`Could not read the record ${path}: ${error instanceof Error ? error.message : String(error)}`);
44
+ }
45
+ const record = parsed;
46
+ if (!record ||
47
+ record.format !== RECORD_FORMAT ||
48
+ typeof record.file?.sha256 !== 'string' ||
49
+ typeof record.file.bytes !== 'number') {
50
+ throw badRequest(`${path} is not a Page Scanner integrity record.`);
51
+ }
52
+ return record;
53
+ }
54
+ async function signatureHolds(record) {
55
+ const { signature, ...signed } = record;
56
+ if (!signature || signature.algorithm !== 'ES256')
57
+ return false;
58
+ try {
59
+ const key = await webcrypto.subtle.importKey('jwk', {
60
+ kty: signature.publicKey.kty,
61
+ crv: signature.publicKey.crv,
62
+ x: signature.publicKey.x,
63
+ y: signature.publicKey.y,
64
+ }, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']);
65
+ const thumbprint = createHash('sha256')
66
+ .update(JSON.stringify({
67
+ crv: signature.publicKey.crv,
68
+ kty: signature.publicKey.kty,
69
+ x: signature.publicKey.x,
70
+ y: signature.publicKey.y,
71
+ }))
72
+ .digest('base64url');
73
+ if (thumbprint !== signature.keyThumbprint)
74
+ return false;
75
+ return await webcrypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, key, Buffer.from(signature.value, 'base64url'), Buffer.from(canonicalJson(signed), 'utf8'));
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
81
+ /** Checks `filePath` against its record, `<file>.integrity.json` unless one is named. */
82
+ export async function verifyCapture(filePath, options = {}) {
83
+ const cwd = options.cwd ?? process.cwd();
84
+ const file = resolve(cwd, filePath);
85
+ const recordPath = resolve(cwd, options.record ?? `${filePath}${RECORD_SUFFIX}`);
86
+ if (!existsSync(recordPath)) {
87
+ throw badRequest(`There is no record at ${recordPath}.`, 'Name it with --record, or export with an integrity record beside the file.');
88
+ }
89
+ const record = readRecord(recordPath);
90
+ let bytes;
91
+ try {
92
+ bytes = readFileSync(file);
93
+ }
94
+ catch (error) {
95
+ throw badRequest(`Could not read ${file}: ${error instanceof Error ? error.message : String(error)}`);
96
+ }
97
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
98
+ const matches = sha256 === record.file.sha256.toLowerCase() && bytes.length === record.file.bytes;
99
+ const signature = record.signature
100
+ ? { valid: await signatureHolds(record), keyThumbprint: record.signature.keyThumbprint }
101
+ : null;
102
+ return {
103
+ verified: matches && (signature === null || signature.valid),
104
+ file: { path: file, matches, sha256, bytes: bytes.length },
105
+ record: {
106
+ path: recordPath,
107
+ expectedSha256: record.file.sha256,
108
+ expectedBytes: record.file.bytes,
109
+ url: record.page?.url ?? null,
110
+ captured: record.captured?.local ?? record.captured?.utc ?? null,
111
+ },
112
+ signature,
113
+ };
114
+ }
115
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"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"}
package/dist/version.d.ts CHANGED
@@ -7,5 +7,5 @@
7
7
  * `version.test.ts` asserts this matches package.json, so the copy cannot
8
8
  * drift without a red test.
9
9
  */
10
- export declare const CLI_VERSION = "0.2.0";
10
+ export declare const CLI_VERSION = "0.2.2";
11
11
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -7,5 +7,5 @@
7
7
  * `version.test.ts` asserts this matches package.json, so the copy cannot
8
8
  * drift without a red test.
9
9
  */
10
- export const CLI_VERSION = '0.2.0';
10
+ export const CLI_VERSION = '0.2.2';
11
11
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@page-scanner/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Capture a whole web page from the command line, through the Page Scanner Chrome extension.",
5
5
  "keywords": [
6
6
  "chrome",