@intincrab/updiff 0.1.1 → 0.2.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/report.js CHANGED
@@ -31,11 +31,14 @@ export function renderText(report, options = {}) {
31
31
  for (const f of group) {
32
32
  lines.push(` ${p.sev(sev, CHANGE_MARK[f.change])} ${f.title}`);
33
33
  if (f.detail)
34
- lines.push(` ${clip(f.detail, max)}`);
34
+ for (const d of clip(f.detail, max).split('\n'))
35
+ lines.push(` ${d}`);
35
36
  if (f.where?.length)
36
37
  lines.push(p.dim(` at ${f.where.join(', ')}`));
37
38
  }
38
39
  }
40
+ for (const note of report.notes ?? [])
41
+ lines.push('', p.dim(`Note: ${note}`));
39
42
  if (report.findings.some((f) => f.category === 'host'))
40
43
  lines.push('', p.dim(HOSTNAME_CAVEAT));
41
44
  return lines.join('\n') + '\n';
@@ -62,13 +65,17 @@ export function renderMarkdown(report) {
62
65
  lines.push(`## ${SEVERITY_LABEL[sev]}`, '');
63
66
  for (const f of group) {
64
67
  lines.push(`- ${CHANGE_MARK[f.change] === '+' ? 'Added' : CHANGE_MARK[f.change] === '-' ? 'Removed' : 'Changed'}: **${mdEscape(f.title)}**`);
65
- if (f.detail)
68
+ if (f.detail?.includes('\n'))
69
+ lines.push('', ' ```diff', ...f.detail.split('\n').map((d) => ` ${d}`), ' ```', '');
70
+ else if (f.detail)
66
71
  lines.push(` ${mdEscape(f.detail)}`);
67
72
  if (f.where?.length)
68
73
  lines.push(` <sub>${f.where.map((w) => `\`${w}\``).join(', ')}</sub>`);
69
74
  }
70
75
  lines.push('');
71
76
  }
77
+ for (const note of report.notes ?? [])
78
+ lines.push(`> Note: ${mdEscape(note)}`, '');
72
79
  if (report.findings.some((f) => f.category === 'host'))
73
80
  lines.push(`> ${HOSTNAME_CAVEAT}`, '');
74
81
  lines.push('<sub>Generated by updiff. Static inspection only: the app was not run.</sub>', '');
@@ -99,13 +106,35 @@ export function renderScan(s, options = {}) {
99
106
  lines.push(`Signing ${sig.status}${sig.teamId ? `, team ${sig.teamId}` : ''}${sig.authorities[0] ? ` (${sig.authorities[0]})` : ''}${sig.hardenedRuntime ? ', hardened runtime' : ', no hardened runtime'}`);
100
107
  const ents = Object.keys(main?.entitlements ?? {});
101
108
  const sandboxed = main?.entitlements?.['com.apple.security.app-sandbox'] === true;
109
+ if (s.notarization) {
110
+ const n = s.notarization;
111
+ const stapled = n.stapled === undefined ? '' : n.stapled ? ', ticket stapled' : ', ticket not stapled';
112
+ if (n.error)
113
+ lines.push(`Gatekeeper could not verify the signature: ${n.error}`);
114
+ else
115
+ lines.push(`Gatekeeper ${n.assessment}${n.source ? ` (${n.source})` : ''}${stapled}`);
116
+ }
117
+ if (s.info.appStoreReceipt)
118
+ lines.push('App Store yes (bundle has a Mac App Store receipt)');
102
119
  lines.push(`Sandbox ${sandboxed ? 'yes' : 'no'}`);
103
120
  lines.push(`Entitlements ${ents.length ? ents.join(', ') : 'none'}`);
104
121
  const privacy = Object.keys(s.info.privacyUsage);
105
122
  lines.push(`Privacy asks ${privacy.length ? privacy.map((k) => k.replace(/^NS/, '').replace(/UsageDescription$/, '')).join(', ') : 'none'}`);
106
123
  lines.push(`URL schemes ${s.info.urlSchemes.length ? s.info.urlSchemes.join(', ') : 'none'}`);
124
+ const sparkle = s.code.some((c) => c.kind === 'framework' && c.path.endsWith('/Sparkle.framework'));
107
125
  if (s.info.sparkleFeedUrl)
108
126
  lines.push(`Update feed ${s.info.sparkleFeedUrl}`);
127
+ for (const [i, url] of (s.info.sparkleFeedsInCode ?? []).entries())
128
+ lines.push(`${i === 0 && !s.info.sparkleFeedUrl ? 'Update feed ' : ' '} ${url} (in code)`);
129
+ if (sparkle && !s.info.sparkleFeedUrl && !s.info.sparkleFeedsInCode?.length) {
130
+ lines.push('Update feed Sparkle is bundled, but no feed was found (it may be built at runtime)');
131
+ }
132
+ if (s.local?.sparkleFeedInDefaults)
133
+ lines.push(` ${s.local.sparkleFeedInDefaults} (set in this Mac's preferences)`);
134
+ if (s.local && (s.local.sparkleAutomaticChecks !== undefined || s.local.sparkleAutomaticallyUpdate !== undefined)) {
135
+ const onOff = (v) => (v === undefined ? 'default' : v ? 'on' : 'off');
136
+ lines.push(`This Mac automatic update checks ${onOff(s.local.sparkleAutomaticChecks)}, automatic install ${onOff(s.local.sparkleAutomaticallyUpdate)}`);
137
+ }
109
138
  const counts = new Map();
110
139
  for (const c of s.code)
111
140
  if (c.kind !== 'main')
package/dist/store.js CHANGED
@@ -81,6 +81,27 @@ export class Store {
81
81
  await Promise.all(drop.map((d) => rm(d.file, { force: true })));
82
82
  return drop.length;
83
83
  }
84
+ get extensionsDir() {
85
+ return join(this.home, 'extensions');
86
+ }
87
+ async saveExtensions(snapshot) {
88
+ await mkdir(this.extensionsDir, { recursive: true });
89
+ const file = join(this.extensionsDir, `${snapshot.takenAt.replace(/[:.]/g, '-')}.json`);
90
+ await writeFile(file, JSON.stringify(snapshot));
91
+ const all = (await readdir(this.extensionsDir)).filter((n) => n.endsWith('.json')).sort();
92
+ await Promise.all(all.slice(0, Math.max(0, all.length - 10)).map((n) => rm(join(this.extensionsDir, n), { force: true })));
93
+ return file;
94
+ }
95
+ async latestExtensions() {
96
+ try {
97
+ const all = (await readdir(this.extensionsDir)).filter((n) => n.endsWith('.json')).sort();
98
+ const last = all.at(-1);
99
+ return last ? JSON.parse(await readFile(join(this.extensionsDir, last), 'utf8')) : undefined;
100
+ }
101
+ catch {
102
+ return undefined;
103
+ }
104
+ }
84
105
  async saveReport(bundleId, fromVersion, toVersion, ext, content) {
85
106
  const dir = join(this.reportsDir, safe(bundleId));
86
107
  await mkdir(dir, { recursive: true });
package/dist/util.js CHANGED
@@ -37,3 +37,37 @@ export function stableJson(v) {
37
37
  }
38
38
  return JSON.stringify(v);
39
39
  }
40
+ /**
41
+ * Line diff of two texts as "+ line" / "- line" entries (unchanged lines left out).
42
+ * Uses a longest-common-subsequence table, so keep inputs to a few thousand lines.
43
+ */
44
+ export function lineDiff(before, after) {
45
+ const a = before.split('\n');
46
+ const b = after.split('\n');
47
+ if (a.length * b.length > 4_000_000)
48
+ return [`(too long to compare line by line: ${a.length} → ${b.length} lines)`];
49
+ const lcs = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
50
+ for (let i = a.length - 1; i >= 0; i--) {
51
+ for (let j = b.length - 1; j >= 0; j--) {
52
+ lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
53
+ }
54
+ }
55
+ const out = [];
56
+ let i = 0;
57
+ let j = 0;
58
+ while (i < a.length && j < b.length) {
59
+ if (a[i] === b[j]) {
60
+ i++;
61
+ j++;
62
+ }
63
+ else if (lcs[i + 1][j] >= lcs[i][j + 1])
64
+ out.push(`- ${a[i++]}`);
65
+ else
66
+ out.push(`+ ${b[j++]}`);
67
+ }
68
+ while (i < a.length)
69
+ out.push(`- ${a[i++]}`);
70
+ while (j < b.length)
71
+ out.push(`+ ${b[j++]}`);
72
+ return out;
73
+ }
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export const TOOL_NAME = 'updiff';
2
- export const TOOL_VERSION = '0.1.1';
2
+ export const TOOL_VERSION = '0.2.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intincrab/updiff",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "See what a macOS app update actually changed: entitlements, privacy prompts, background items, helpers, bundled packages and hostnames.",
5
5
  "repository": {
6
6
  "type": "git",