deeplink-parity 0.3.0 → 0.4.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/README.md +12 -1
- package/dist/cli.js +23 -2
- package/dist/fetch/wellKnown.js +7 -1
- package/dist/report/console.js +3 -1
- package/dist/report/github.js +31 -0
- package/dist/run.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,7 +97,18 @@ npx deeplink-parity . --sha256 "AB:CD:…" # include the fingerpri
|
|
|
97
97
|
The Android signing fingerprint comes from Play Console → App integrity → App signing.
|
|
98
98
|
Without it the fingerprint check is skipped and reported as such.
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
### In CI
|
|
101
|
+
|
|
102
|
+
| Exit code | Meaning |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `0` | No errors. Warnings and notices do not fail the run |
|
|
105
|
+
| `1` | At least one error — a link is broken today |
|
|
106
|
+
| `2` | Nothing to check, or the run itself failed |
|
|
107
|
+
|
|
108
|
+
Colour is emitted only to a terminal, so piped and captured output stays plain. `--json`
|
|
109
|
+
writes nothing but JSON to stdout; progress notes go to stderr. On GitHub Actions the
|
|
110
|
+
findings are also emitted as annotations, which appear on the run summary and against the
|
|
111
|
+
file when one is involved — set `--format github` to force it elsewhere.
|
|
101
112
|
|
|
102
113
|
### Run it on a schedule — this is the point
|
|
103
114
|
|
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { localSource, networkSource } from './fetch/wellKnown.js';
|
|
4
4
|
import { exitCodeFor, printReport } from './report/console.js';
|
|
5
|
+
import { printGithubAnnotations } from './report/github.js';
|
|
5
6
|
import { run } from './run.js';
|
|
6
7
|
const USAGE = `deeplink-parity — check that what your app declares about deep links
|
|
7
8
|
matches what is actually hosted, across iOS and Android.
|
|
@@ -18,6 +19,7 @@ Options
|
|
|
18
19
|
--sha256 <fingerprint> Android signing fingerprint to look for in assetlinks.json
|
|
19
20
|
--well-known <dir> Read well-known files from <dir>/<domain>/ instead of the network
|
|
20
21
|
--json Machine-readable output
|
|
22
|
+
--format github GitHub Actions annotations (auto-detected on Actions)
|
|
21
23
|
-h, --help Show this message
|
|
22
24
|
`;
|
|
23
25
|
function parseArgs(argv) {
|
|
@@ -27,6 +29,8 @@ function parseArgs(argv) {
|
|
|
27
29
|
let help = false;
|
|
28
30
|
let sha256;
|
|
29
31
|
let wellKnown;
|
|
32
|
+
// Actions sets GITHUB_ACTIONS=true; annotate by default there
|
|
33
|
+
let format = process.env.GITHUB_ACTIONS === 'true' ? 'github' : 'console';
|
|
30
34
|
for (let i = 0; i < args.length; i++) {
|
|
31
35
|
const arg = args[i];
|
|
32
36
|
if (arg === '--json')
|
|
@@ -37,6 +41,8 @@ function parseArgs(argv) {
|
|
|
37
41
|
sha256 = args[++i];
|
|
38
42
|
else if (arg === '--well-known')
|
|
39
43
|
wellKnown = args[++i];
|
|
44
|
+
else if (arg === '--format')
|
|
45
|
+
format = args[++i];
|
|
40
46
|
else if (!arg.startsWith('-'))
|
|
41
47
|
roots.push(arg);
|
|
42
48
|
}
|
|
@@ -46,16 +52,27 @@ function parseArgs(argv) {
|
|
|
46
52
|
help,
|
|
47
53
|
sha256,
|
|
48
54
|
wellKnown,
|
|
55
|
+
format,
|
|
49
56
|
};
|
|
50
57
|
}
|
|
51
58
|
async function main() {
|
|
52
|
-
const { roots, json, help, sha256, wellKnown } = parseArgs(process.argv);
|
|
59
|
+
const { roots, json, help, sha256, wellKnown, format } = parseArgs(process.argv);
|
|
53
60
|
if (help) {
|
|
54
61
|
console.log(USAGE);
|
|
55
62
|
return;
|
|
56
63
|
}
|
|
57
64
|
const source = wellKnown ? localSource(resolve(wellKnown)) : networkSource();
|
|
58
|
-
const result = await run({
|
|
65
|
+
const result = await run({
|
|
66
|
+
roots,
|
|
67
|
+
source,
|
|
68
|
+
sha256,
|
|
69
|
+
onDiscovered: (count) => {
|
|
70
|
+
// some apps declare a domain per country; say so before spending minutes on it
|
|
71
|
+
if (!wellKnown && count > 50) {
|
|
72
|
+
console.error(`Checking ${count} domains — requests are pooled, so this will take a while.`);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
});
|
|
59
76
|
if (result.iosApps.length === 0 && result.androidApps.length === 0 && result.findings.length === 0) {
|
|
60
77
|
console.error(`No app configuration declaring deep links was found in ${roots.join(', ')}`);
|
|
61
78
|
console.error('Expected a .entitlements file with applinks:, or an AndroidManifest.xml with intent-filters.');
|
|
@@ -77,6 +94,10 @@ async function main() {
|
|
|
77
94
|
findings: result.findings,
|
|
78
95
|
}, null, 2));
|
|
79
96
|
}
|
|
97
|
+
else if (format === 'github') {
|
|
98
|
+
printGithubAnnotations(result.findings);
|
|
99
|
+
printReport(result.findings, result.domains);
|
|
100
|
+
}
|
|
80
101
|
else {
|
|
81
102
|
printReport(result.findings, result.domains);
|
|
82
103
|
}
|
package/dist/fetch/wellKnown.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
const TIMEOUT_MS = 10_000;
|
|
4
|
+
/**
|
|
5
|
+
* Identify the tool to the hosts being read. An operator seeing these requests in a log
|
|
6
|
+
* should be able to tell what they are and who to ask, rather than finding an anonymous
|
|
7
|
+
* Node default.
|
|
8
|
+
*/
|
|
9
|
+
const USER_AGENT = 'deeplink-parity (+https://github.com/camosss/deeplink-parity)';
|
|
4
10
|
export const AASA_FILE = 'apple-app-site-association';
|
|
5
11
|
export const ASSETLINKS_FILE = 'assetlinks.json';
|
|
6
12
|
export function aasaUrl(domain) {
|
|
@@ -20,7 +26,7 @@ async function fetchRaw(url) {
|
|
|
20
26
|
const res = await fetch(url, {
|
|
21
27
|
redirect: 'manual',
|
|
22
28
|
signal: controller.signal,
|
|
23
|
-
headers: { accept: 'application/json' },
|
|
29
|
+
headers: { accept: 'application/json', 'user-agent': USER_AGENT },
|
|
24
30
|
});
|
|
25
31
|
const redirected = res.status >= 300 && res.status < 400;
|
|
26
32
|
return {
|
package/dist/report/console.js
CHANGED
|
@@ -11,7 +11,9 @@ const ORDER = ['error', 'warn', 'info'];
|
|
|
11
11
|
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
12
12
|
const paint = (s, c) => (useColor ? `${c}${s}${RESET}` : s);
|
|
13
13
|
export function printReport(findings, checkedDomains) {
|
|
14
|
-
|
|
14
|
+
// every escape goes through paint(), which is a no-op when stdout is not a terminal
|
|
15
|
+
const separator = paint('·', DIM);
|
|
16
|
+
console.log(`\n${paint('deeplink-parity', BOLD)} ${separator} ${checkedDomains.length} domain(s) checked\n`);
|
|
15
17
|
if (findings.length === 0) {
|
|
16
18
|
console.log(paint('No problems found', '[32m'));
|
|
17
19
|
console.log();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { isAbsolute } from 'node:path';
|
|
2
|
+
const LEVEL = {
|
|
3
|
+
error: 'error',
|
|
4
|
+
warn: 'warning',
|
|
5
|
+
info: 'notice',
|
|
6
|
+
};
|
|
7
|
+
/** Workflow commands treat these characters as syntax and need them escaped. */
|
|
8
|
+
function escapeData(value) {
|
|
9
|
+
return value.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
|
|
10
|
+
}
|
|
11
|
+
function escapeProperty(value) {
|
|
12
|
+
return escapeData(value).replace(/:/g, '%3A').replace(/,/g, '%2C');
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* GitHub Actions annotation format, so findings appear on the run summary — and on the
|
|
16
|
+
* changed lines when a finding points at a file in the repository.
|
|
17
|
+
* https://docs.github.com/actions/reference/workflow-commands-for-github-actions
|
|
18
|
+
*/
|
|
19
|
+
export function printGithubAnnotations(findings) {
|
|
20
|
+
for (const f of findings) {
|
|
21
|
+
const props = [`title=${escapeProperty(`deeplink-parity ${f.rule}`)}`];
|
|
22
|
+
// Annotations anchor to a path inside the checkout. A source can also be a URL or an
|
|
23
|
+
// absolute path outside it — neither anchors to anything, so the annotation stays
|
|
24
|
+
// on the run summary instead of pointing at a file that will not resolve.
|
|
25
|
+
if (f.source && !/^https?:\/\//.test(f.source) && !isAbsolute(f.source)) {
|
|
26
|
+
props.push(`file=${escapeProperty(f.source)}`);
|
|
27
|
+
}
|
|
28
|
+
const body = [f.domain, f.message, f.detail].filter(Boolean).join(' — ');
|
|
29
|
+
console.log(`::${LEVEL[f.severity]} ${props.join(',')}::${escapeData(body)}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
package/dist/run.js
CHANGED
|
@@ -9,7 +9,7 @@ import { isWildcardDomain, wildcardFinding } from './rules/wildcard.js';
|
|
|
9
9
|
function emptyView() {
|
|
10
10
|
return { domains: new Set(), paths: new Map() };
|
|
11
11
|
}
|
|
12
|
-
export async function run({ roots, source, sha256 }) {
|
|
12
|
+
export async function run({ roots, source, sha256, onDiscovered }) {
|
|
13
13
|
const discovered = await Promise.all(roots.map(async (root) => Promise.all([discoverIos(root), discoverAndroid(root)])));
|
|
14
14
|
const iosApps = discovered.flatMap(([ios]) => ios);
|
|
15
15
|
const androidApps = discovered.flatMap(([, android]) => android);
|
|
@@ -21,6 +21,10 @@ export async function run({ roots, source, sha256 }) {
|
|
|
21
21
|
}
|
|
22
22
|
const ios = emptyView();
|
|
23
23
|
const android = emptyView();
|
|
24
|
+
onDiscovered?.(new Set([
|
|
25
|
+
...iosApps.flatMap((a) => a.domains),
|
|
26
|
+
...androidApps.flatMap((a) => a.hosts.map((h) => h.host)),
|
|
27
|
+
]).size);
|
|
24
28
|
for (const app of iosApps) {
|
|
25
29
|
// the same domain can be declared by several targets; fetch it once
|
|
26
30
|
const fresh = app.domains.filter((d) => !ios.domains.has(d));
|
package/package.json
CHANGED