deeplink-parity 0.4.0 → 0.5.1
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 +98 -6
- package/dist/cli.js +35 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
Checks that what your app **declares** about deep links matches what is actually **hosted** — on both iOS and Android, from one command.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
|
|
13
|
-
npx deeplink-parity ./my-app-ios ./my-app-android
|
|
12
|
+
npx deeplink-parity .
|
|
14
13
|
```
|
|
15
14
|
|
|
16
15
|
```
|
|
@@ -30,6 +29,53 @@ WARN promo.example.com
|
|
|
30
29
|
|
|
31
30
|
<br>
|
|
32
31
|
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
Nothing to configure. Domains are read from your app and the matching files are fetched
|
|
35
|
+
from them.
|
|
36
|
+
|
|
37
|
+
**One repository** — a monorepo, Flutter, or React Native:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx deeplink-parity .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Two repositories** — native iOS and Android, which is the case worth checking:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx deeplink-parity ./my-app-ios ./my-app-android
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**On GitHub Actions**, one line:
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
- uses: actions/checkout@v4
|
|
53
|
+
- uses: camosss/deeplink-parity@v1
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Findings show up as annotations on the run. Add `sha256` to include the Android signing
|
|
57
|
+
key in the check, and `fail-on: never` to report without failing while an existing backlog
|
|
58
|
+
is cleared.
|
|
59
|
+
|
|
60
|
+
<br>
|
|
61
|
+
|
|
62
|
+
## Contents
|
|
63
|
+
|
|
64
|
+
- [Why](#why)
|
|
65
|
+
- [What it checks](#what-it-checks)
|
|
66
|
+
- [Usage](#usage)
|
|
67
|
+
- [GitHub Action](#github-action)
|
|
68
|
+
- [In CI](#in-ci)
|
|
69
|
+
- [Run it on a schedule](#run-it-on-a-schedule)
|
|
70
|
+
- [Validate before deploying](#validate-before-deploying)
|
|
71
|
+
- [Native, Flutter and React Native](#native-flutter-and-react-native)
|
|
72
|
+
- [Hardened against real projects](#hardened-against-real-projects)
|
|
73
|
+
- [What it does not do](#what-it-does-not-do)
|
|
74
|
+
- [Development](#development)
|
|
75
|
+
- [License](#license)
|
|
76
|
+
|
|
77
|
+
<br>
|
|
78
|
+
|
|
33
79
|
## Why
|
|
34
80
|
|
|
35
81
|
Deep links fail **silently**. There is no crash, no error log, no red test. A user taps a link, the browser opens instead of your app, and they shrug and move on. Nobody finds out for months.
|
|
@@ -81,7 +127,10 @@ npx deeplink-parity [path...] [options]
|
|
|
81
127
|
|
|
82
128
|
--sha256 <fingerprint> Android signing fingerprint to look for in assetlinks.json
|
|
83
129
|
--well-known <dir> Read well-known files from <dir>/<domain>/ instead of the network
|
|
84
|
-
--json Machine-readable output
|
|
130
|
+
--json Machine-readable output on stdout
|
|
131
|
+
--output <file> Also write the JSON result to a file
|
|
132
|
+
--format github GitHub Actions annotations (auto-detected on Actions)
|
|
133
|
+
-h, --help Show this message
|
|
85
134
|
```
|
|
86
135
|
|
|
87
136
|
Pass one path per checkout. A monorepo holding both platforms works as a single path,
|
|
@@ -97,6 +146,35 @@ npx deeplink-parity . --sha256 "AB:CD:…" # include the fingerpri
|
|
|
97
146
|
The Android signing fingerprint comes from Play Console → App integrity → App signing.
|
|
98
147
|
Without it the fingerprint check is skipped and reported as such.
|
|
99
148
|
|
|
149
|
+
### GitHub Action
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
- uses: camosss/deeplink-parity@v1
|
|
153
|
+
with:
|
|
154
|
+
paths: ios android # one per checkout; omit for a single repo
|
|
155
|
+
sha256: ${{ secrets.ANDROID_SHA256 }}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Findings appear as annotations on the run, and counts are available to later steps:
|
|
159
|
+
|
|
160
|
+
```yaml
|
|
161
|
+
- uses: camosss/deeplink-parity@v1
|
|
162
|
+
id: links
|
|
163
|
+
with:
|
|
164
|
+
fail-on: never # report without blocking while a backlog is cleared
|
|
165
|
+
- run: echo "${{ steps.links.outputs.errors }} errors, ${{ steps.links.outputs.warnings }} warnings"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
| Input | Default | |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `paths` | `.` | Checkout paths, whitespace separated |
|
|
171
|
+
| `sha256` | — | Android signing fingerprint |
|
|
172
|
+
| `well-known` | — | Read from disk instead of the network |
|
|
173
|
+
| `fail-on` | `error` | `never` to report without failing the step |
|
|
174
|
+
| `version` | pinned | npm version to run |
|
|
175
|
+
|
|
176
|
+
Outputs: `errors`, `warnings`, `notices`, `domains`, `report` (path to the JSON result).
|
|
177
|
+
|
|
100
178
|
### In CI
|
|
101
179
|
|
|
102
180
|
| Exit code | Meaning |
|
|
@@ -110,7 +188,7 @@ writes nothing but JSON to stdout; progress notes go to stderr. On GitHub Action
|
|
|
110
188
|
findings are also emitted as annotations, which appear on the run summary and against the
|
|
111
189
|
file when one is involved — set `--format github` to force it elsewhere.
|
|
112
190
|
|
|
113
|
-
### Run it on a schedule
|
|
191
|
+
### Run it on a schedule
|
|
114
192
|
|
|
115
193
|
Your deep-link configuration changes a few times a year. The things that break it do not live in your repo at all:
|
|
116
194
|
|
|
@@ -123,12 +201,26 @@ A pull-request check never sees any of this. **A daily scheduled run does.** See
|
|
|
123
201
|
|
|
124
202
|
### Validate before deploying
|
|
125
203
|
|
|
126
|
-
`--well-known <dir>` reads the files from disk instead of the network, so the web team can
|
|
204
|
+
`--well-known <dir>` reads the files from disk instead of the network, so the web team can
|
|
205
|
+
check staged files before they ship — and CI can run with no egress at all.
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
well-known/
|
|
209
|
+
└── links.example.com/
|
|
210
|
+
├── apple-app-site-association
|
|
211
|
+
└── assetlinks.json
|
|
212
|
+
```
|
|
127
213
|
|
|
128
214
|
```bash
|
|
129
|
-
npx deeplink-parity . --well-known ./
|
|
215
|
+
npx deeplink-parity . --well-known ./well-known
|
|
130
216
|
```
|
|
131
217
|
|
|
218
|
+
This answers a different question, though: whether the app matches the files you intend to
|
|
219
|
+
publish, not whether it matches what is live right now. The failures this tool exists to
|
|
220
|
+
catch — a deploy putting a redirect in front of `/.well-known/`, an expired domain, a
|
|
221
|
+
rotated key — only show up against the real host. Files hosted by an attribution vendor
|
|
222
|
+
cannot be checked offline at all.
|
|
223
|
+
|
|
132
224
|
<br>
|
|
133
225
|
|
|
134
226
|
## Native, Flutter and React Native
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
3
|
import { resolve } from 'node:path';
|
|
3
4
|
import { localSource, networkSource } from './fetch/wellKnown.js';
|
|
4
5
|
import { exitCodeFor, printReport } from './report/console.js';
|
|
@@ -18,7 +19,8 @@ Usage
|
|
|
18
19
|
Options
|
|
19
20
|
--sha256 <fingerprint> Android signing fingerprint to look for in assetlinks.json
|
|
20
21
|
--well-known <dir> Read well-known files from <dir>/<domain>/ instead of the network
|
|
21
|
-
--json Machine-readable output
|
|
22
|
+
--json Machine-readable output on stdout
|
|
23
|
+
--output <file> Also write the JSON result to a file
|
|
22
24
|
--format github GitHub Actions annotations (auto-detected on Actions)
|
|
23
25
|
-h, --help Show this message
|
|
24
26
|
`;
|
|
@@ -29,6 +31,7 @@ function parseArgs(argv) {
|
|
|
29
31
|
let help = false;
|
|
30
32
|
let sha256;
|
|
31
33
|
let wellKnown;
|
|
34
|
+
let output;
|
|
32
35
|
// Actions sets GITHUB_ACTIONS=true; annotate by default there
|
|
33
36
|
let format = process.env.GITHUB_ACTIONS === 'true' ? 'github' : 'console';
|
|
34
37
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -43,6 +46,8 @@ function parseArgs(argv) {
|
|
|
43
46
|
wellKnown = args[++i];
|
|
44
47
|
else if (arg === '--format')
|
|
45
48
|
format = args[++i];
|
|
49
|
+
else if (arg === '--output')
|
|
50
|
+
output = args[++i];
|
|
46
51
|
else if (!arg.startsWith('-'))
|
|
47
52
|
roots.push(arg);
|
|
48
53
|
}
|
|
@@ -53,10 +58,11 @@ function parseArgs(argv) {
|
|
|
53
58
|
sha256,
|
|
54
59
|
wellKnown,
|
|
55
60
|
format,
|
|
61
|
+
output,
|
|
56
62
|
};
|
|
57
63
|
}
|
|
58
64
|
async function main() {
|
|
59
|
-
const { roots, json, help, sha256, wellKnown, format } = parseArgs(process.argv);
|
|
65
|
+
const { roots, json, help, sha256, wellKnown, format, output } = parseArgs(process.argv);
|
|
60
66
|
if (help) {
|
|
61
67
|
console.log(USAGE);
|
|
62
68
|
return;
|
|
@@ -78,27 +84,36 @@ async function main() {
|
|
|
78
84
|
console.error('Expected a .entitlements file with applinks:, or an AndroidManifest.xml with intent-filters.');
|
|
79
85
|
process.exit(2);
|
|
80
86
|
}
|
|
87
|
+
const payload = {
|
|
88
|
+
ios: result.iosApps.map((a) => ({
|
|
89
|
+
entitlements: a.entitlementsPath,
|
|
90
|
+
teamId: a.teamId,
|
|
91
|
+
bundleId: a.bundleId,
|
|
92
|
+
domains: a.domains,
|
|
93
|
+
})),
|
|
94
|
+
android: result.androidApps.map((a) => ({
|
|
95
|
+
manifest: a.manifestPath,
|
|
96
|
+
packageIds: a.packageIds,
|
|
97
|
+
hosts: a.hosts.map((h) => h.host),
|
|
98
|
+
})),
|
|
99
|
+
summary: {
|
|
100
|
+
domains: result.domains.length,
|
|
101
|
+
error: result.findings.filter((f) => f.severity === 'error').length,
|
|
102
|
+
warn: result.findings.filter((f) => f.severity === 'warn').length,
|
|
103
|
+
info: result.findings.filter((f) => f.severity === 'info').length,
|
|
104
|
+
},
|
|
105
|
+
findings: result.findings,
|
|
106
|
+
};
|
|
107
|
+
// A file keeps stdout free, so annotations and the readable report can coexist with
|
|
108
|
+
// machine-readable output in the same run.
|
|
109
|
+
if (output)
|
|
110
|
+
await writeFile(output, `${JSON.stringify(payload, null, 2)}\n`);
|
|
81
111
|
if (json) {
|
|
82
|
-
console.log(JSON.stringify(
|
|
83
|
-
ios: result.iosApps.map((a) => ({
|
|
84
|
-
entitlements: a.entitlementsPath,
|
|
85
|
-
teamId: a.teamId,
|
|
86
|
-
bundleId: a.bundleId,
|
|
87
|
-
domains: a.domains,
|
|
88
|
-
})),
|
|
89
|
-
android: result.androidApps.map((a) => ({
|
|
90
|
-
manifest: a.manifestPath,
|
|
91
|
-
packageIds: a.packageIds,
|
|
92
|
-
hosts: a.hosts.map((h) => h.host),
|
|
93
|
-
})),
|
|
94
|
-
findings: result.findings,
|
|
95
|
-
}, null, 2));
|
|
96
|
-
}
|
|
97
|
-
else if (format === 'github') {
|
|
98
|
-
printGithubAnnotations(result.findings);
|
|
99
|
-
printReport(result.findings, result.domains);
|
|
112
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
100
113
|
}
|
|
101
114
|
else {
|
|
115
|
+
if (format === 'github')
|
|
116
|
+
printGithubAnnotations(result.findings);
|
|
102
117
|
printReport(result.findings, result.domains);
|
|
103
118
|
}
|
|
104
119
|
process.exit(exitCodeFor(result.findings));
|
package/package.json
CHANGED