deeplink-doctor 1.0.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.
Files changed (112) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +188 -0
  4. package/dist/checks/calculateExitCode.d.ts +4 -0
  5. package/dist/checks/calculateExitCode.js +4 -0
  6. package/dist/checks/calculateExitCode.js.map +1 -0
  7. package/dist/checks/checkAasaAppIds.d.ts +2 -0
  8. package/dist/checks/checkAasaAppIds.js +15 -0
  9. package/dist/checks/checkAasaAppIds.js.map +1 -0
  10. package/dist/checks/checkAppIdentifiers.d.ts +2 -0
  11. package/dist/checks/checkAppIdentifiers.js +13 -0
  12. package/dist/checks/checkAppIdentifiers.js.map +1 -0
  13. package/dist/checks/checkAssetlinks.d.ts +2 -0
  14. package/dist/checks/checkAssetlinks.js +21 -0
  15. package/dist/checks/checkAssetlinks.js.map +1 -0
  16. package/dist/checks/checkAssociatedDomains.d.ts +3 -0
  17. package/dist/checks/checkAssociatedDomains.js +7 -0
  18. package/dist/checks/checkAssociatedDomains.js.map +1 -0
  19. package/dist/checks/checkAutoVerifyHosts.d.ts +2 -0
  20. package/dist/checks/checkAutoVerifyHosts.js +6 -0
  21. package/dist/checks/checkAutoVerifyHosts.js.map +1 -0
  22. package/dist/checks/checkConfig.d.ts +2 -0
  23. package/dist/checks/checkConfig.js +11 -0
  24. package/dist/checks/checkConfig.js.map +1 -0
  25. package/dist/checks/checkDeadLinks.d.ts +2 -0
  26. package/dist/checks/checkDeadLinks.js +9 -0
  27. package/dist/checks/checkDeadLinks.js.map +1 -0
  28. package/dist/checks/checkRemote.d.ts +2 -0
  29. package/dist/checks/checkRemote.js +4 -0
  30. package/dist/checks/checkRemote.js.map +1 -0
  31. package/dist/checks/checkScheme.d.ts +3 -0
  32. package/dist/checks/checkScheme.js +13 -0
  33. package/dist/checks/checkScheme.js.map +1 -0
  34. package/dist/checks/checkUnreachableRoutes.d.ts +2 -0
  35. package/dist/checks/checkUnreachableRoutes.js +11 -0
  36. package/dist/checks/checkUnreachableRoutes.js.map +1 -0
  37. package/dist/checks/runChecks.d.ts +2 -0
  38. package/dist/checks/runChecks.js +8 -0
  39. package/dist/checks/runChecks.js.map +1 -0
  40. package/dist/checks/targetMatchesRoute.d.ts +2 -0
  41. package/dist/checks/targetMatchesRoute.js +14 -0
  42. package/dist/checks/targetMatchesRoute.js.map +1 -0
  43. package/dist/commands/check.d.ts +21 -0
  44. package/dist/commands/check.js +42 -0
  45. package/dist/commands/check.js.map +1 -0
  46. package/dist/config/findingExplanations.d.ts +2 -0
  47. package/dist/config/findingExplanations.js +19 -0
  48. package/dist/config/findingExplanations.js.map +1 -0
  49. package/dist/config/findingSeverities.d.ts +2 -0
  50. package/dist/config/findingSeverities.js +16 -0
  51. package/dist/config/findingSeverities.js.map +1 -0
  52. package/dist/index.d.ts +2 -0
  53. package/dist/index.js +59 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/lib/generateFinding.d.ts +5 -0
  56. package/dist/lib/generateFinding.js +8 -0
  57. package/dist/lib/generateFinding.js.map +1 -0
  58. package/dist/links/extractDomains.d.ts +2 -0
  59. package/dist/links/extractDomains.js +11 -0
  60. package/dist/links/extractDomains.js.map +1 -0
  61. package/dist/links/extractLinkConfig.d.ts +2 -0
  62. package/dist/links/extractLinkConfig.js +15 -0
  63. package/dist/links/extractLinkConfig.js.map +1 -0
  64. package/dist/links/extractLinkTargets.d.ts +2 -0
  65. package/dist/links/extractLinkTargets.js +25 -0
  66. package/dist/links/extractLinkTargets.js.map +1 -0
  67. package/dist/parsers/expoConfig.d.ts +4 -0
  68. package/dist/parsers/expoConfig.js +16 -0
  69. package/dist/parsers/expoConfig.js.map +1 -0
  70. package/dist/parsers/routes.d.ts +2 -0
  71. package/dist/parsers/routes.js +23 -0
  72. package/dist/parsers/routes.js.map +1 -0
  73. package/dist/report/human.d.ts +3 -0
  74. package/dist/report/human.js +72 -0
  75. package/dist/report/human.js.map +1 -0
  76. package/dist/report/json.d.ts +3 -0
  77. package/dist/report/json.js +19 -0
  78. package/dist/report/json.js.map +1 -0
  79. package/dist/routes/parseRoutePath.d.ts +3 -0
  80. package/dist/routes/parseRoutePath.js +53 -0
  81. package/dist/routes/parseRoutePath.js.map +1 -0
  82. package/dist/routes/routeHandlesPath.d.ts +2 -0
  83. package/dist/routes/routeHandlesPath.js +12 -0
  84. package/dist/routes/routeHandlesPath.js.map +1 -0
  85. package/dist/routes/routeHandlesPrefix.d.ts +2 -0
  86. package/dist/routes/routeHandlesPrefix.js +15 -0
  87. package/dist/routes/routeHandlesPrefix.js.map +1 -0
  88. package/dist/sources/aasa.d.ts +2 -0
  89. package/dist/sources/aasa.js +16 -0
  90. package/dist/sources/aasa.js.map +1 -0
  91. package/dist/sources/assetlinks.d.ts +2 -0
  92. package/dist/sources/assetlinks.js +13 -0
  93. package/dist/sources/assetlinks.js.map +1 -0
  94. package/dist/sources/httpFetcher.d.ts +2 -0
  95. package/dist/sources/httpFetcher.js +5 -0
  96. package/dist/sources/httpFetcher.js.map +1 -0
  97. package/dist/sources/loadAssociations.d.ts +2 -0
  98. package/dist/sources/loadAssociations.js +49 -0
  99. package/dist/sources/loadAssociations.js.map +1 -0
  100. package/dist/suppressions/applySuppressions.d.ts +5 -0
  101. package/dist/suppressions/applySuppressions.js +38 -0
  102. package/dist/suppressions/applySuppressions.js.map +1 -0
  103. package/dist/suppressions/parseSuppressionConfig.d.ts +2 -0
  104. package/dist/suppressions/parseSuppressionConfig.js +28 -0
  105. package/dist/suppressions/parseSuppressionConfig.js.map +1 -0
  106. package/dist/suppressions/readSuppressionConfig.d.ts +5 -0
  107. package/dist/suppressions/readSuppressionConfig.js +32 -0
  108. package/dist/suppressions/readSuppressionConfig.js.map +1 -0
  109. package/dist/types.d.ts +92 -0
  110. package/dist/types.js +15 -0
  111. package/dist/types.js.map +1 -0
  112. package/package.json +83 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ ## [1.0.1](https://github.com/KMavr/deeplink-doctor/compare/deeplink-doctor-v1.0.0...deeplink-doctor-v1.0.1) (2026-06-25)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * add repository metadata for provenance ([#16](https://github.com/KMavr/deeplink-doctor/issues/16)) ([7bbb22c](https://github.com/KMavr/deeplink-doctor/commit/7bbb22cbf1d4273cc849738111caa9abc028270c))
9
+
10
+ ## 1.0.0 (2026-06-25)
11
+
12
+
13
+ ### Features
14
+
15
+ * parse the expo-router route tree from the app/ directory ([#1](https://github.com/KMavr/deeplink-doctor/issues/1)) ([a574747](https://github.com/KMavr/deeplink-doctor/commit/a574747b7dbdff78020d92c0057cbecf278e77ec))
16
+ * read native deep-link config from `expo config --json` ([#2](https://github.com/KMavr/deeplink-doctor/issues/2)) ([43f4ad5](https://github.com/KMavr/deeplink-doctor/commit/43f4ad51ab9f32f92fe902555caefe119e913d23))
17
+ * reconcile routes against native config — DL001/DL002 + check command ([#3](https://github.com/KMavr/deeplink-doctor/issues/3)) ([8ce30cd](https://github.com/KMavr/deeplink-doctor/commit/8ce30cd30f3b30015dfdf789da8b64e29cab3ef4))
18
+ * add config-hygiene checks DL003 and DL101–DL104 ([#4](https://github.com/KMavr/deeplink-doctor/issues/4)) ([1cce5cc](https://github.com/KMavr/deeplink-doctor/commit/1cce5cc1287345843bfcfda6c14f4a5ea5eac69d))
19
+ * add governed suppressions with DL901/DL902 accountability checks ([#5](https://github.com/KMavr/deeplink-doctor/issues/5)) ([ae7d789](https://github.com/KMavr/deeplink-doctor/commit/ae7d7893ed3949bcb8fb2bdfa53959255b585bf1))
20
+ * verify hosted association files over the network — DL2xx via --remote ([#6](https://github.com/KMavr/deeplink-doctor/issues/6)) ([2ce500f](https://github.com/KMavr/deeplink-doctor/commit/2ce500f3c28c710a7c7f634b7b3f8eca5dc0c9fa))
21
+ * add --config flag for suppression config path ([#7](https://github.com/KMavr/deeplink-doctor/issues/7)) ([cd581fe](https://github.com/KMavr/deeplink-doctor/commit/cd581fe6743af5e7b729ab6f9ac08e3a1d043ae1))
22
+ * add --explain to append finding explanations ([#12](https://github.com/KMavr/deeplink-doctor/issues/12)) ([c0bc95c](https://github.com/KMavr/deeplink-doctor/commit/c0bc95c9d9eb1f5a58db9d48fc06ab308bcbb3e3))
23
+ * add --silent flag to hide warnings ([#11](https://github.com/KMavr/deeplink-doctor/issues/11)) ([3055f94](https://github.com/KMavr/deeplink-doctor/commit/3055f94c73c7d2d65501a28ee1353ef800de35b8))
24
+ * include suppressed findings in JSON output ([#8](https://github.com/KMavr/deeplink-doctor/issues/8)) ([1eb1f13](https://github.com/KMavr/deeplink-doctor/commit/1eb1f13d0f1818bcb164ad4cf95ed1dd36b74e39))
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Konstantinos Mavrikas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # deeplink-doctor
2
+
3
+ [![CI](https://github.com/KMavr/deeplink-doctor/actions/workflows/ci.yml/badge.svg)](https://github.com/KMavr/deeplink-doctor/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/deeplink-doctor.svg)](https://www.npmjs.com/package/deeplink-doctor)
5
+ [![npm downloads](https://img.shields.io/npm/dm/deeplink-doctor.svg)](https://www.npmjs.com/package/deeplink-doctor)
6
+
7
+ > Statically reconcile an Expo project's **route tree**, its **native deep-link
8
+ > config**, and (optionally) its **hosted association files** — and report where
9
+ > they disagree.
10
+
11
+ Deep links break in ways nothing catches until a user taps one in production: an
12
+ `intentFilter` advertises a path no screen handles, a screen can never be reached
13
+ by any link, or the `apple-app-site-association` file you hosted names the wrong
14
+ bundle. Apple's validator and Android's `pm verify-app-links` check the _hosted
15
+ file_ or _device state_ — none of them read your repo, so none can tell you the
16
+ repo and the config disagree. That repo-aware cross-check is the whole point of
17
+ this tool.
18
+
19
+ Zero config, no setup, runs over `npx`.
20
+
21
+ ## Quickstart
22
+
23
+ ```sh
24
+ # from the root of an Expo (expo-router) project
25
+ npx deeplink-doctor
26
+ ```
27
+
28
+ That runs every static check — no network, safe on every commit. Add `--remote`
29
+ to also fetch and validate your hosted association files.
30
+
31
+ Requires Node 18+ and an `expo-router` project (it reads your `app/` directory
32
+ and `npx expo config --json`).
33
+
34
+ ## What it catches
35
+
36
+ A project can look perfectly configured locally yet be dead in production. Static
37
+ checks run offline:
38
+
39
+ ```sh
40
+ $ npx deeplink-doctor
41
+ DL001 error No route handles deep link target "/promo"
42
+
43
+ 1 issue (1 error, 0 warnings)
44
+ ```
45
+
46
+ …and the hosted files are the other half of the handshake. `--remote` fetches
47
+ them and confirms they name _your_ app:
48
+
49
+ ```sh
50
+ $ npx deeplink-doctor check --remote --explain
51
+ DL202 error AASA appID does not match ios.bundleIdentifier "com.example.app"
52
+ The hosted AASA lists no appID matching your ios.bundleIdentifier (an
53
+ appID is <teamId>.<bundleIdentifier>). iOS will not associate the domain
54
+ with your app. Fix: add your team + bundle to applinks.details[].appID(s)
55
+ in the hosted AASA.
56
+
57
+ DL203 error assetlinks.json has no entry for android.package "com.example.app"
58
+ The hosted assetlinks.json has no statement for your android.package.
59
+ Android will not verify your App Links. Fix: add a statement whose
60
+ target.package_name is your package.
61
+
62
+ 2 issues (2 errors, 0 warnings)
63
+ ```
64
+
65
+ Green offline, red against the live world — the classic "works in dev, broken in
66
+ prod." That's exactly the gap `--remote` closes.
67
+
68
+ ## How it works
69
+
70
+ It reconciles up to three sources of truth:
71
+
72
+ 1. **Route tree** — parses your `app/` directory the way expo-router does
73
+ (dynamic `[id]`, catch-all `[...rest]`, `(groups)`, `_layout`, `+`-special and
74
+ API routes).
75
+ 2. **Native config** — reads the _resolved_ config from `npx expo config --json`
76
+ (`scheme`, `ios.associatedDomains`/`bundleIdentifier`, `android.package`/
77
+ `intentFilters`).
78
+ 3. **Hosted association files** _(opt-in, `--remote`)_ — fetches
79
+ `https://<domain>/.well-known/apple-app-site-association` and
80
+ `assetlinks.json` and checks they point back at your app.
81
+
82
+ ## Commands
83
+
84
+ | Command | Description |
85
+ | ------------------------------------------- | ----------------------------------------------- |
86
+ | `deeplink-doctor` / `deeplink-doctor check` | Run the checks and report mismatches (default). |
87
+ | `deeplink-doctor routes` | Print the parsed route tree (debug). |
88
+
89
+ ### Flags (for `check`)
90
+
91
+ | Flag | Effect |
92
+ | ----------------- | -------------------------------------------------------------------------- |
93
+ | `--remote` | Also fetch and validate hosted association files (makes network requests). |
94
+ | `--domain <host>` | Override the domain(s) probed by `--remote`. |
95
+ | `--strict` | Promote warnings to failures (non-zero exit on any finding). |
96
+ | `--silent` | Hide warnings (errors only). Ignored when `--strict` is set. |
97
+ | `--explain` | Append a long-form explanation to each finding. |
98
+ | `--config <path>` | Path to a `deeplink.config.json` (defaults to the project root). |
99
+ | `--json` | Emit a stable machine-readable schema instead of the human report. |
100
+
101
+ ### Exit codes
102
+
103
+ | Code | Meaning |
104
+ | ---- | ------------------------------------------------------------------ |
105
+ | `0` | Clean (or only warnings, without `--strict`). |
106
+ | `1` | One or more findings at failure severity. |
107
+ | `2` | Tool or usage error (e.g. not an Expo project, unreadable config). |
108
+
109
+ ## Checks
110
+
111
+ Severity is the default; `--strict` promotes every warning to a failure.
112
+
113
+ | Code | Severity | Meaning |
114
+ | ------- | -------- | ------------------------------------------------------------------- |
115
+ | `DL001` | error | A path advertised by the native config matches no route. |
116
+ | `DL002` | warn | A deep-linkable route is reachable by no configured link. |
117
+ | `DL003` | error | A custom-scheme link is configured but `scheme` is missing. |
118
+ | `DL101` | error | An `associatedDomains` entry is missing its `applinks:` prefix. |
119
+ | `DL102` | error | `ios.bundleIdentifier` missing while iOS deep links are configured. |
120
+ | `DL103` | error | `android.package` missing while Android deep links are configured. |
121
+ | `DL104` | warn | `intentFilter` has `autoVerify` but no `data.host` to verify. |
122
+ | `DL201` | error | A hosted file is unreachable, not JSON, or served via a redirect. |
123
+ | `DL202` | error | The AASA names no appID matching `ios.bundleIdentifier`. |
124
+ | `DL203` | error | `assetlinks.json` has no entry for `android.package`. |
125
+ | `DL204` | warn | The matched `assetlinks` entry has empty cert fingerprints. |
126
+ | `DL901` | warn | A suppression rule is missing a `reason`/`owner`. |
127
+ | `DL902` | warn | A suppression rule targets an unknown code (likely a typo). |
128
+
129
+ Run `check --explain` to print the full why-and-fix for each finding you have.
130
+
131
+ ## Suppressions
132
+
133
+ `DL002` in particular is false-positive-prone by design (admin-only or
134
+ intentionally unlinked screens). Suppress findings in a `deeplink.config.json` at
135
+ your project root — but suppressions are **governed**, not silent: each one needs
136
+ a `reason` and an `owner`, so an ignore is always accountable.
137
+
138
+ ```json
139
+ {
140
+ "ignore": [
141
+ {
142
+ "code": "DL002",
143
+ "route": "/internal/debug",
144
+ "reason": "admin-only screen, never linked",
145
+ "owner": "your-handle",
146
+ "revisitWhen": "expo-router@>=6"
147
+ }
148
+ ]
149
+ }
150
+ ```
151
+
152
+ A rule missing `reason`/`owner` raises `DL901`; a rule targeting a code the tool
153
+ doesn't emit raises `DL902`. Suppressed findings stay out of the exit code but are
154
+ reported as a count (and listed in full under `--json`).
155
+
156
+ ## CI
157
+
158
+ `--json` emits a stable schema for pipelines:
159
+
160
+ ```json
161
+ {
162
+ "summary": { "total": 2, "errors": 2, "warnings": 0, "suppressed": 1 },
163
+ "findings": [{ "code": "DL202", "severity": "error", "message": "...", "target": "..." }],
164
+ "suppressed": [{ "code": "DL002", "severity": "warn", "message": "...", "route": "..." }]
165
+ }
166
+ ```
167
+
168
+ Gate a pull request with the static checks on every push, and the full
169
+ `--remote` run before a release:
170
+
171
+ ```sh
172
+ npx deeplink-doctor --strict # fast, hermetic, every commit
173
+ npx deeplink-doctor --remote --strict # full, networked, pre-release
174
+ ```
175
+
176
+ ## Scope
177
+
178
+ v1 is a focused linter for **expo-router** projects. Out of scope (by design):
179
+
180
+ - **SHA-256 fingerprint verification** against real signing certs — that needs
181
+ EAS/Play credentials. `DL204` flags an empty fingerprint list; it does not
182
+ verify the values.
183
+ - **React Navigation `linking.config`** — expo-router is the v1 target.
184
+ - **Runtime/device testing** — `adb` and Apple's validator already own that.
185
+
186
+ ## License
187
+
188
+ MIT © Konstantinos Mavrikas
@@ -0,0 +1,4 @@
1
+ import type { Finding } from '../types.js';
2
+ export declare const calculateExitCode: (findings: Finding[], options: {
3
+ strict: boolean;
4
+ }) => 0 | 1;
@@ -0,0 +1,4 @@
1
+ export const calculateExitCode = (findings, options) => findings.some(({ severity }) => severity === 'error' || (severity === 'warn' && options.strict))
2
+ ? 1
3
+ : 0;
4
+ //# sourceMappingURL=calculateExitCode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calculateExitCode.js","sourceRoot":"","sources":["../../src/checks/calculateExitCode.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,QAAmB,EAAE,OAA4B,EAAS,EAAE,CAC5F,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,QAAQ,KAAK,OAAO,IAAI,CAAC,QAAQ,KAAK,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9F,CAAC,CAAC,CAAC;IACH,CAAC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { AasaModel, Finding, LinkConfig } from '../types.js';
2
+ export declare const checkAasaAppIds: (aasa: AasaModel, config: LinkConfig) => Finding[];
@@ -0,0 +1,15 @@
1
+ import { generateFinding } from '../lib/generateFinding.js';
2
+ export const checkAasaAppIds = (aasa, config) => {
3
+ const bundleIdentifierConfig = config.ios.bundleIdentifier;
4
+ if (!bundleIdentifierConfig) {
5
+ return [];
6
+ }
7
+ const formattedAppIdsAasa = aasa.appIDs.map((appID) => appID.slice(appID.indexOf('.') + 1));
8
+ if (formattedAppIdsAasa.includes(bundleIdentifierConfig)) {
9
+ return [];
10
+ }
11
+ return [
12
+ generateFinding('DL202', `AASA appID does not match ios.bundleIdentifier "${bundleIdentifierConfig}"`),
13
+ ];
14
+ };
15
+ //# sourceMappingURL=checkAasaAppIds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkAasaAppIds.js","sourceRoot":"","sources":["../../src/checks/checkAasaAppIds.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAG5D,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAe,EAAE,MAAkB,EAAa,EAAE;IAChF,MAAM,sBAAsB,GAAG,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC;IAC3D,IAAI,CAAC,sBAAsB,EAAE,CAAC;QAC5B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,mBAAmB,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAE5F,IAAI,mBAAmB,CAAC,QAAQ,CAAC,sBAAsB,CAAC,EAAE,CAAC;QACzD,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO;QACL,eAAe,CACb,OAAO,EACP,mDAAmD,sBAAsB,GAAG,CAC7E;KACF,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { Finding, LinkConfig } from '../types.js';
2
+ export declare const checkAppIdentifiers: (config: LinkConfig) => Finding[];
@@ -0,0 +1,13 @@
1
+ import { generateFinding } from '../lib/generateFinding.js';
2
+ export const checkAppIdentifiers = (config) => {
3
+ const missingIosBundleIdentifier = config.ios.associatedDomains.length > 0 && !config.ios.bundleIdentifier
4
+ ? [
5
+ generateFinding('DL102', 'ios.bundleIdentifier is required when associatedDomains are configured'),
6
+ ]
7
+ : [];
8
+ const missingAndroidIntentFilters = config.android.intentFilters.length > 0 && !config.android.package
9
+ ? [generateFinding('DL103', 'android.package is required when intentFilters are configured')]
10
+ : [];
11
+ return [...missingIosBundleIdentifier, ...missingAndroidIntentFilters];
12
+ };
13
+ //# sourceMappingURL=checkAppIdentifiers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkAppIdentifiers.js","sourceRoot":"","sources":["../../src/checks/checkAppIdentifiers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAG5D,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,MAAkB,EAAa,EAAE;IACnE,MAAM,0BAA0B,GAC9B,MAAM,CAAC,GAAG,CAAC,iBAAiB,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,gBAAgB;QACrE,CAAC,CAAC;YACE,eAAe,CACb,OAAO,EACP,wEAAwE,CACzE;SACF;QACH,CAAC,CAAC,EAAE,CAAC;IAET,MAAM,2BAA2B,GAC/B,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO;QAChE,CAAC,CAAC,CAAC,eAAe,CAAC,OAAO,EAAE,+DAA+D,CAAC,CAAC;QAC7F,CAAC,CAAC,EAAE,CAAC;IAET,OAAO,CAAC,GAAG,0BAA0B,EAAE,GAAG,2BAA2B,CAAC,CAAC;AACzE,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { AssetlinkEntry, Finding, LinkConfig } from '../types.js';
2
+ export declare const checkAssetlinks: (assetlinks: AssetlinkEntry[], config: LinkConfig) => Finding[];
@@ -0,0 +1,21 @@
1
+ import { generateFinding } from '../lib/generateFinding.js';
2
+ export const checkAssetlinks = (assetlinks, config) => {
3
+ const pkg = config.android.package;
4
+ if (!pkg) {
5
+ return [];
6
+ }
7
+ if (assetlinks.every(({ packageName }) => packageName !== pkg)) {
8
+ return [generateFinding('DL203', `assetlinks.json has no entry for android.package "${pkg}"`)];
9
+ }
10
+ return assetlinks.flatMap(({ packageName, fingerprints }) => {
11
+ if (pkg === packageName) {
12
+ return fingerprints.length > 0
13
+ ? []
14
+ : [
15
+ generateFinding('DL204', `assetlinks entry for "${pkg}" has an empty sha256_cert_fingerprints array`),
16
+ ];
17
+ }
18
+ return [];
19
+ });
20
+ };
21
+ //# sourceMappingURL=checkAssetlinks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkAssetlinks.js","sourceRoot":"","sources":["../../src/checks/checkAssetlinks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAG5D,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,UAA4B,EAAE,MAAkB,EAAa,EAAE;IAC7F,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;IACnC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,WAAW,KAAK,GAAG,CAAC,EAAE,CAAC;QAC/D,OAAO,CAAC,eAAe,CAAC,OAAO,EAAE,qDAAqD,GAAG,GAAG,CAAC,CAAC,CAAC;IACjG,CAAC;IAED,OAAO,UAAU,CAAC,OAAO,CAAC,CAAC,EAAE,WAAW,EAAE,YAAY,EAAE,EAAE,EAAE;QAC1D,IAAI,GAAG,KAAK,WAAW,EAAE,CAAC;YACxB,OAAO,YAAY,CAAC,MAAM,GAAG,CAAC;gBAC5B,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC;oBACE,eAAe,CACb,OAAO,EACP,yBAAyB,GAAG,+CAA+C,CAC5E;iBACF,CAAC;QACR,CAAC;QACD,OAAO,EAAE,CAAC;IACZ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Finding, LinkConfig } from '../types.js';
2
+ export declare const RECOGNIZED_DOMAIN_PREFIXES: string[];
3
+ export declare const checkAssociatedDomains: (config: LinkConfig) => Finding[];
@@ -0,0 +1,7 @@
1
+ import { generateFinding } from '../lib/generateFinding.js';
2
+ export const RECOGNIZED_DOMAIN_PREFIXES = ['applinks:', 'webcredentials:', 'activitycontinuation:'];
3
+ export const checkAssociatedDomains = (config) => {
4
+ const invalidAssociatedDomains = config.ios.associatedDomains.filter((domain) => RECOGNIZED_DOMAIN_PREFIXES.every((prefix) => !domain.startsWith(prefix)));
5
+ return invalidAssociatedDomains.map((domain) => generateFinding('DL101', `associatedDomains entry "${domain}" is missing a scheme prefix (e.g. "${RECOGNIZED_DOMAIN_PREFIXES.join('", "')}")`));
6
+ };
7
+ //# sourceMappingURL=checkAssociatedDomains.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkAssociatedDomains.js","sourceRoot":"","sources":["../../src/checks/checkAssociatedDomains.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAG5D,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,WAAW,EAAE,iBAAiB,EAAE,uBAAuB,CAAC,CAAC;AAEpG,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,MAAkB,EAAa,EAAE;IACtE,MAAM,wBAAwB,GAAa,MAAM,CAAC,GAAG,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CACxF,0BAA0B,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CACzE,CAAC;IAEF,OAAO,wBAAwB,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAC7C,eAAe,CACb,OAAO,EACP,4BAA4B,MAAM,uCAAuC,0BAA0B,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CACrH,CACF,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { Finding, LinkConfig } from '../types.js';
2
+ export declare const checkAutoVerifyHosts: (config: LinkConfig) => Finding[];
@@ -0,0 +1,6 @@
1
+ import { generateFinding } from '../lib/generateFinding.js';
2
+ export const checkAutoVerifyHosts = (config) => {
3
+ const missingHostIntentFilters = config.android.intentFilters.filter((intentFilter) => intentFilter.autoVerify && !intentFilter.data.find((item) => item.host !== undefined));
4
+ return missingHostIntentFilters.map(() => generateFinding('DL104', 'intentFilter has autoVerify enabled but no data.host to verify'));
5
+ };
6
+ //# sourceMappingURL=checkAutoVerifyHosts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkAutoVerifyHosts.js","sourceRoot":"","sources":["../../src/checks/checkAutoVerifyHosts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAG5D,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,MAAkB,EAAa,EAAE;IACpE,MAAM,wBAAwB,GAAG,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAClE,CAAC,YAAY,EAAE,EAAE,CACf,YAAY,CAAC,UAAU,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CACxF,CAAC;IAEF,OAAO,wBAAwB,CAAC,GAAG,CAAC,GAAG,EAAE,CACvC,eAAe,CAAC,OAAO,EAAE,gEAAgE,CAAC,CAC3F,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { Finding, LinkConfig } from '../types.js';
2
+ export declare const checkConfig: (config: LinkConfig) => Finding[];
@@ -0,0 +1,11 @@
1
+ import { checkAppIdentifiers } from './checkAppIdentifiers.js';
2
+ import { checkAssociatedDomains } from './checkAssociatedDomains.js';
3
+ import { checkAutoVerifyHosts } from './checkAutoVerifyHosts.js';
4
+ import { checkScheme } from './checkScheme.js';
5
+ export const checkConfig = (config) => [
6
+ ...checkScheme(config),
7
+ ...checkAssociatedDomains(config),
8
+ ...checkAppIdentifiers(config),
9
+ ...checkAutoVerifyHosts(config),
10
+ ];
11
+ //# sourceMappingURL=checkConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkConfig.js","sourceRoot":"","sources":["../../src/checks/checkConfig.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAAkB,EAAa,EAAE,CAAC;IAC5D,GAAG,WAAW,CAAC,MAAM,CAAC;IACtB,GAAG,sBAAsB,CAAC,MAAM,CAAC;IACjC,GAAG,mBAAmB,CAAC,MAAM,CAAC;IAC9B,GAAG,oBAAoB,CAAC,MAAM,CAAC;CAChC,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { Finding, LinkTarget, PathData } from '../types.js';
2
+ export declare const checkDeadLinks: (routes: PathData[], targets: LinkTarget[]) => Finding[];
@@ -0,0 +1,9 @@
1
+ import { generateFinding } from '../lib/generateFinding.js';
2
+ import { targetMatchesRoute } from './targetMatchesRoute.js';
3
+ export const checkDeadLinks = (routes, targets) => {
4
+ const deadTargets = targets.filter((target) => !routes.some((route) => targetMatchesRoute(route, target)));
5
+ return deadTargets.map((target) => generateFinding('DL001', `No route handles deep link target "${target.raw}"`, {
6
+ target: target.raw,
7
+ }));
8
+ };
9
+ //# sourceMappingURL=checkDeadLinks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkDeadLinks.js","sourceRoot":"","sources":["../../src/checks/checkDeadLinks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,MAAkB,EAAE,OAAqB,EAAa,EAAE;IACrF,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,CAChC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CACvE,CAAC;IAEF,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAChC,eAAe,CAAC,OAAO,EAAE,sCAAsC,MAAM,CAAC,GAAG,GAAG,EAAE;QAC5E,MAAM,EAAE,MAAM,CAAC,GAAG;KACnB,CAAC,CACH,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { AasaModel, AssetlinkEntry, Finding, LinkConfig } from '../types.js';
2
+ export declare const checkRemote: (aasa: AasaModel, assetlinks: AssetlinkEntry[], config: LinkConfig) => Finding[];
@@ -0,0 +1,4 @@
1
+ import { checkAasaAppIds } from './checkAasaAppIds.js';
2
+ import { checkAssetlinks } from './checkAssetlinks.js';
3
+ export const checkRemote = (aasa, assetlinks, config) => [...checkAasaAppIds(aasa, config), ...checkAssetlinks(assetlinks, config)];
4
+ //# sourceMappingURL=checkRemote.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkRemote.js","sourceRoot":"","sources":["../../src/checks/checkRemote.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,IAAe,EACf,UAA4B,EAC5B,MAAkB,EACP,EAAE,CAAC,CAAC,GAAG,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,GAAG,eAAe,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Finding, LinkConfig } from '../types.js';
2
+ export declare const WEB_SCHEMES: string[];
3
+ export declare const checkScheme: (config: LinkConfig) => Finding[];
@@ -0,0 +1,13 @@
1
+ import { generateFinding } from '../lib/generateFinding.js';
2
+ export const WEB_SCHEMES = ['http', 'https'];
3
+ export const checkScheme = (config) => {
4
+ const hasNoSchemes = config.schemes.length === 0;
5
+ const hasCustomScheme = config.android.intentFilters.some(({ data }) => data.some((d) => d.scheme !== undefined && !WEB_SCHEMES.includes(d.scheme.toLowerCase())));
6
+ if (hasNoSchemes && hasCustomScheme) {
7
+ return [
8
+ generateFinding('DL003', 'A custom-scheme deep link is configured but no scheme is declared; set "scheme" in your Expo config'),
9
+ ];
10
+ }
11
+ return [];
12
+ };
13
+ //# sourceMappingURL=checkScheme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkScheme.js","sourceRoot":"","sources":["../../src/checks/checkScheme.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAG5D,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE7C,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAAkB,EAAa,EAAE;IAC3D,MAAM,YAAY,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC;IACjD,MAAM,eAAe,GAAG,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CACrE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAC1F,CAAC;IACF,IAAI,YAAY,IAAI,eAAe,EAAE,CAAC;QACpC,OAAO;YACL,eAAe,CACb,OAAO,EACP,qGAAqG,CACtG;SACF,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { Finding, LinkTarget, PathData } from '../types.js';
2
+ export declare const checkUnreachableRoutes: (routes: PathData[], targets: LinkTarget[]) => Finding[];
@@ -0,0 +1,11 @@
1
+ import { generateFinding } from '../lib/generateFinding.js';
2
+ import { isLinkableRoute } from '../routes/parseRoutePath.js';
3
+ import { targetMatchesRoute } from './targetMatchesRoute.js';
4
+ export const checkUnreachableRoutes = (routes, targets) => {
5
+ const linkableRoutes = routes.filter((route) => isLinkableRoute(route));
6
+ const unreachableRoutes = linkableRoutes.filter((route) => targets.every((target) => !targetMatchesRoute(route, target)));
7
+ return unreachableRoutes.map((route) => generateFinding('DL002', `Route "${route.pathname}" is not reachable by any configured link`, {
8
+ route: route.pathname,
9
+ }));
10
+ };
11
+ //# sourceMappingURL=checkUnreachableRoutes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkUnreachableRoutes.js","sourceRoot":"","sources":["../../src/checks/checkUnreachableRoutes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,MAAkB,EAAE,OAAqB,EAAa,EAAE;IAC7F,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC;IAExE,MAAM,iBAAiB,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CACxD,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAC9D,CAAC;IAEF,OAAO,iBAAiB,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CACrC,eAAe,CAAC,OAAO,EAAE,UAAU,KAAK,CAAC,QAAQ,2CAA2C,EAAE;QAC5F,KAAK,EAAE,KAAK,CAAC,QAAQ;KACtB,CAAC,CACH,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { LinkTarget, PathData, Finding } from '../types.js';
2
+ export declare const runChecks: (routes: PathData[], targets: LinkTarget[]) => Finding[];
@@ -0,0 +1,8 @@
1
+ import { checkDeadLinks } from './checkDeadLinks.js';
2
+ import { checkUnreachableRoutes } from './checkUnreachableRoutes.js';
3
+ export const runChecks = (routes, targets) => {
4
+ const deadLinks = checkDeadLinks(routes, targets);
5
+ const unreachableRoutes = checkUnreachableRoutes(routes, targets);
6
+ return [...deadLinks, ...unreachableRoutes];
7
+ };
8
+ //# sourceMappingURL=runChecks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runChecks.js","sourceRoot":"","sources":["../../src/checks/runChecks.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAErE,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,MAAkB,EAAE,OAAqB,EAAa,EAAE;IAChF,MAAM,SAAS,GAAG,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClD,MAAM,iBAAiB,GAAG,sBAAsB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAElE,OAAO,CAAC,GAAG,SAAS,EAAE,GAAG,iBAAiB,CAAC,CAAC;AAC9C,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { PathData, LinkTarget } from '../types.js';
2
+ export declare const targetMatchesRoute: (route: PathData, target: LinkTarget) => boolean;
@@ -0,0 +1,14 @@
1
+ import { routeHandlesPath } from '../routes/routeHandlesPath.js';
2
+ import { routeHandlesPrefix } from '../routes/routeHandlesPrefix.js';
3
+ export const targetMatchesRoute = (route, target) => {
4
+ if (target.kind === 'exact') {
5
+ return routeHandlesPath(route, target.segments);
6
+ }
7
+ if (target.kind === 'prefix') {
8
+ return routeHandlesPrefix(route, target.segments);
9
+ }
10
+ const globPatternIndex = target.segments.findIndex((s) => s.includes('.') || s.includes('*'));
11
+ const truncatedSegments = globPatternIndex === -1 ? target.segments : target.segments.slice(0, globPatternIndex);
12
+ return routeHandlesPrefix(route, truncatedSegments);
13
+ };
14
+ //# sourceMappingURL=targetMatchesRoute.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"targetMatchesRoute.js","sourceRoot":"","sources":["../../src/checks/targetMatchesRoute.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAGrE,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,KAAe,EAAE,MAAkB,EAAW,EAAE;IACjF,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAC5B,OAAO,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IAClD,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IACpD,CAAC;IAED,MAAM,gBAAgB,GAAG,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;IAE9F,MAAM,iBAAiB,GACrB,gBAAgB,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC;IAEzF,OAAO,kBAAkB,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAC;AACtD,CAAC,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { Associations, LinkConfig, PathData, SuppressionRule } from '../types.js';
2
+ type CheckDeps = {
3
+ readRoutes: (root: string) => PathData[];
4
+ readLinkConfig: (root: string) => LinkConfig;
5
+ readSuppressionConfig: (root: string, configPath?: string) => SuppressionRule[];
6
+ loadAssociations: (domain: string) => Promise<Associations>;
7
+ };
8
+ type CheckOptions = {
9
+ strict?: boolean;
10
+ json?: boolean;
11
+ remote?: boolean;
12
+ domain?: string;
13
+ config?: string;
14
+ silent?: boolean;
15
+ explain?: boolean;
16
+ };
17
+ export declare const runCheck: (root: string, deps: CheckDeps, options: CheckOptions) => Promise<{
18
+ report: string;
19
+ exitCode: 0 | 1 | 2;
20
+ }>;
21
+ export {};
@@ -0,0 +1,42 @@
1
+ import { calculateExitCode } from '../checks/calculateExitCode.js';
2
+ import { checkConfig } from '../checks/checkConfig.js';
3
+ import { checkRemote } from '../checks/checkRemote.js';
4
+ import { runChecks } from '../checks/runChecks.js';
5
+ import { extractDomains } from '../links/extractDomains.js';
6
+ import { extractLinkTargets } from '../links/extractLinkTargets.js';
7
+ import * as human from '../report/human.js';
8
+ import * as json from '../report/json.js';
9
+ import { applySuppressions } from '../suppressions/applySuppressions.js';
10
+ const remoteFindings = async (config, options, loadAssociations) => {
11
+ if (!options.remote) {
12
+ return [];
13
+ }
14
+ const domains = options.domain ? [options.domain] : extractDomains(config);
15
+ const associations = await Promise.all(domains.map((domain) => loadAssociations(domain)));
16
+ return associations.flatMap((association) => [
17
+ ...association.findings,
18
+ ...checkRemote(association.aasa, association.assetlinks, config),
19
+ ]);
20
+ };
21
+ export const runCheck = async (root, deps, options) => {
22
+ try {
23
+ const config = deps.readLinkConfig(root);
24
+ const targets = extractLinkTargets(config);
25
+ const findings = [
26
+ ...runChecks(deps.readRoutes(root), targets),
27
+ ...checkConfig(config),
28
+ ...(await remoteFindings(config, options, deps.loadAssociations)),
29
+ ];
30
+ const { active, suppressed } = applySuppressions(findings, deps.readSuppressionConfig(root, options.config));
31
+ const silent = options.silent === true && options.strict !== true;
32
+ const reported = silent ? active.filter((finding) => finding.severity === 'error') : active;
33
+ const report = options.json
34
+ ? json.renderFindings(reported, suppressed)
35
+ : human.renderFindings(reported, suppressed, options.explain);
36
+ return { report, exitCode: calculateExitCode(reported, { strict: options.strict ?? false }) };
37
+ }
38
+ catch (error) {
39
+ return { report: error instanceof Error ? error.message : String(error), exitCode: 2 };
40
+ }
41
+ };
42
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { FindingCode } from '../types.js';
2
+ export declare const FINDING_EXPLANATIONS: Record<FindingCode, string>;
@@ -0,0 +1,19 @@
1
+ // Long-form help per finding code, surfaced by `check --explain`. Record<FindingCode, _>
2
+ // is exhaustive — a new code can't ship without an explanation. Prose drafted to
3
+ // say what the code means, why it matters, and how to fix it.
4
+ export const FINDING_EXPLANATIONS = {
5
+ DL001: 'A path advertised by your native config (an Android intentFilter path/pathPrefix/pathPattern, or a custom-scheme link) matches no route in your app/ tree. The link opens the app but lands nowhere. Fix: add a route that handles the path, or correct/remove the intentFilter entry.',
6
+ DL002: 'A deep-linkable route exists but no configured link pattern can reach it. This is often intentional (admin or internal-only screens) and is the most false-positive-prone check by design — suppress those in deeplink.config.json. Otherwise add an intentFilter path or associatedDomain so the screen is reachable by a link.',
7
+ DL003: 'A custom-scheme deep link is configured but no `scheme` is declared in your Expo config. Without a scheme, custom-scheme links cannot resolve. Fix: set `scheme` in app.json/app.config.',
8
+ DL101: 'An `ios.associatedDomains` entry is missing its service prefix (e.g. `applinks:`). iOS silently ignores malformed entries, so the domain is never associated. Fix: prefix the entry, e.g. `applinks:example.com`.',
9
+ DL102: 'iOS deep links are configured (associatedDomains present) but `ios.bundleIdentifier` is missing. Universal Links cannot be associated without it. Fix: set `ios.bundleIdentifier`.',
10
+ DL103: 'Android intent filters are configured but `android.package` is missing. App Links cannot be verified without the package name. Fix: set `android.package`.',
11
+ DL104: 'An intentFilter has `autoVerify: true` but no `data.host` entries, so Android has nothing to verify. Fix: add a `data.host`, or drop `autoVerify` if the filter is scheme-only.',
12
+ DL201: 'A hosted association file (AASA or assetlinks.json) is unreachable, not served as JSON, or served via a redirect. Apple and Google reject all three. Fix: serve the file at the exact /.well-known path, as application/json, over HTTPS, with no redirect.',
13
+ DL202: 'The hosted AASA lists no appID matching your `ios.bundleIdentifier` (an appID is `<teamId>.<bundleIdentifier>`). iOS will not associate the domain with your app. Fix: add your team + bundle to applinks.details[].appID(s) in the hosted AASA.',
14
+ DL203: 'The hosted assetlinks.json has no statement for your `android.package`. Android will not verify your App Links. Fix: add a statement whose target.package_name is your package.',
15
+ DL204: 'The matched assetlinks statement has an empty sha256_cert_fingerprints array, so Android has no signing certificate to verify against. Fix: add your app’s release signing SHA-256 fingerprint(s).',
16
+ DL901: 'A suppression rule is missing a `reason` and/or `owner`. Suppressions are governed — every ignore carries accountability so it can be revisited. Fix: add `reason` and `owner` to the rule in deeplink.config.json.',
17
+ DL902: 'A suppression rule targets a code this tool never emits — almost always a typo (e.g. DL2O2 instead of DL202). Fix: correct the code, or remove the stale rule.',
18
+ };
19
+ //# sourceMappingURL=findingExplanations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findingExplanations.js","sourceRoot":"","sources":["../../src/config/findingExplanations.ts"],"names":[],"mappings":"AAEA,yFAAyF;AACzF,iFAAiF;AACjF,8DAA8D;AAC9D,MAAM,CAAC,MAAM,oBAAoB,GAAgC;IAC/D,KAAK,EACH,wRAAwR;IAC1R,KAAK,EACH,kUAAkU;IACpU,KAAK,EACH,0LAA0L;IAC5L,KAAK,EACH,mNAAmN;IACrN,KAAK,EACH,oLAAoL;IACtL,KAAK,EACH,4JAA4J;IAC9J,KAAK,EACH,iLAAiL;IACnL,KAAK,EACH,6PAA6P;IAC/P,KAAK,EACH,kPAAkP;IACpP,KAAK,EACH,iLAAiL;IACnL,KAAK,EACH,oMAAoM;IACtM,KAAK,EACH,qNAAqN;IACvN,KAAK,EACH,gKAAgK;CACnK,CAAC"}