diff --git a/CHANGELOG.md b/CHANGELOG.md index a3fd73f..f851d03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- `strait doctor` and `strait test`: on a link domain before Dashboard → Apps & stores is filled in (Strait answers 404 `{"error":"not configured"}`), the finding is `app.none` with "Fill in Dashboard → Apps & stores (package name and SHA-256 fingerprint / Team ID and bundle ID), wait a minute, then check again", the same step `strait init` gives, not `host.file_missing` ("use your Strait link domain as endpoint"). Same wording as `doctor()` in the SDKs (51 shared vectors, shared-spec ADR 0002 amendment). + ## 0.3.0 (7 Nov 2026) - `strait events add`: the Flutter snippet shows `eventId` (strait_sdk 0.10.0+ has it); the React Native one says `eventId` needs 0.11.0+. diff --git a/README.md b/README.md index 9d0d18a..6486927 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Exit codes: `0` done, `2` usage problem (message on stderr), `1` unexpected erro npx @straitlink/cli doctor --host acme.strait.link --android com.acme.shop --ios com.acme.shop ``` -Runs the same checks, with the same finding ids and wording, as `doctor()` in the Strait SDKs (React Native 0.9.3, Swift 0.9.4, Android 0.8.4, Flutter 0.9.4; contract B23, the 43 shared cases in `test/doctor-vectors.json`). It fetches `https:///.well-known/assetlinks.json` and `apple-app-site-association` once each and tells you whether your package, bundle ID and (with `--cert`) signing certificate are listed, with the fix for each problem: +Runs the same checks, with the same finding ids and wording, as `doctor()` in the Strait SDKs (React Native, Flutter, iOS, Android and Unity; contract B23, the shared cases in `test/doctor-vectors.json`). It fetches `https:///.well-known/assetlinks.json` and `apple-app-site-association` once each and tells you whether your package, bundle ID and (with `--cert`) signing certificate are listed, with the fix for each problem: ``` Android (assetlinks.json) diff --git a/src/doctor.mjs b/src/doctor.mjs index 474b321..afa8df4 100644 --- a/src/doctor.mjs +++ b/src/doctor.mjs @@ -1,6 +1,6 @@ // `strait doctor`: the SDK setup check (contract B23, shared-spec docs/adr/0002-sdk-doctor.md) from a // terminal. `doctorFindings` is a line-for-line port of strait-sdk-react-native 0.9.3 src/doctor.ts: -// same finding ids, severities and wording as every SDK (test/doctor-vectors.json, 43 shared cases). +// same finding ids, severities and wording as every SDK (test/doctor-vectors.json, the shared cases). /** The endpoint's host when it is an `https://` URL with a host, else null. */ export function doctorHost(endpoint) { @@ -61,14 +61,22 @@ export function doctorFindings(input) { return { ok: !findings.some((f) => f.severity === 'error'), findings }; } +/** + * A Strait link domain answers 404 {"error":"not configured"} until Apps & stores has this platform's app. + * Compared byte for byte (what the engine's well-known route sends), so every SDK agrees on any other body. + */ +const NOT_CONFIGURED = '{"error":"not configured"}'; +const notConfigured = (file) => file.status === 404 && file.body === NOT_CONFIGURED; + function hostFindings(input, host, file) { const android = input.platform === 'android'; if (file.status === undefined) return [err('host.unreachable', `Can't reach ${host}`, 'Check that endpoint is your link domain (Dashboard → Get started) and that this device is online.')]; const apps = file.status === 200 ? listedApps(input.platform, file.body ?? '') : null; - if (!apps) + if (!apps && !notConfigured(file)) return [err('host.file_missing', `${host} doesn't serve ${android ? 'assetlinks.json' : 'apple-app-site-association'}`, 'Use your Strait link domain as endpoint (Dashboard → Get started); Strait serves this file there.')]; - if (!apps.size) return [err('app.none', `No ${android ? 'Android' : 'iOS'} app is set up for ${host}`, 'Add your app in Dashboard → Apps & stores.')]; + if (!apps?.size) + return [err('app.none', `No ${android ? 'Android' : 'iOS'} app is set up for ${host}`, `Fill in Dashboard → Apps & stores (${android ? 'package name and SHA-256 fingerprint' : 'Team ID and bundle ID'}), wait a minute, then check again. Already done? Make sure ${host} is your link domain from Dashboard → Get started.`)]; const listed = [...apps.keys()].join(', '); const app = input.appId?.trim(); diff --git a/test/doctor-vectors.json b/test/doctor-vectors.json index 39b5116..dfe932e 100644 --- a/test/doctor-vectors.json +++ b/test/doctor-vectors.json @@ -542,6 +542,238 @@ } ] }, + { + "name": "app.none android: a Strait link domain before Apps & stores is filled in (404 not configured)", + "input": { + "platform": "android", + "publishableKey": "st_pub_live_x", + "endpoint": "https://acme.strait.link", + "appId": "com.acme.app", + "certSha256": [], + "file": { + "status": 404, + "body": "{\"error\":\"not configured\"}" + } + }, + "ok": false, + "findings": [ + { + "id": "key.ok", + "severity": "ok", + "title": "Publishable key is set", + "fix": "" + }, + { + "id": "app.none", + "severity": "error", + "title": "No Android app is set up for acme.strait.link", + "fix": "Fill in Dashboard → Apps & stores (package name and SHA-256 fingerprint), wait a minute, then check again. Already done? Make sure acme.strait.link is your link domain from Dashboard → Get started." + } + ] + }, + { + "name": "app.none ios: a Strait link domain before Apps & stores is filled in (404 not configured)", + "input": { + "platform": "ios", + "publishableKey": "st_pub_live_x", + "endpoint": "https://acme.strait.link", + "appId": "com.acme.app", + "certSha256": [], + "file": { + "status": 404, + "body": "{\"error\":\"not configured\"}" + } + }, + "ok": false, + "findings": [ + { + "id": "key.ok", + "severity": "ok", + "title": "Publishable key is set", + "fix": "" + }, + { + "id": "app.none", + "severity": "error", + "title": "No iOS app is set up for acme.strait.link", + "fix": "Fill in Dashboard → Apps & stores (Team ID and bundle ID), wait a minute, then check again. Already done? Make sure acme.strait.link is your link domain from Dashboard → Get started." + } + ] + }, + { + "name": "app.none ios: not configured, app id unknown", + "input": { + "platform": "ios", + "publishableKey": "st_pub_live_x", + "endpoint": "https://acme.strait.link", + "appId": null, + "certSha256": [], + "file": { + "status": 404, + "body": "{\"error\":\"not configured\"}" + } + }, + "ok": false, + "findings": [ + { + "id": "key.ok", + "severity": "ok", + "title": "Publishable key is set", + "fix": "" + }, + { + "id": "app.none", + "severity": "error", + "title": "No iOS app is set up for acme.strait.link", + "fix": "Fill in Dashboard → Apps & stores (Team ID and bundle ID), wait a minute, then check again. Already done? Make sure acme.strait.link is your link domain from Dashboard → Get started." + } + ] + }, + { + "name": "host.file_missing android 404 with another JSON error", + "input": { + "platform": "android", + "publishableKey": "st_pub_live_x", + "endpoint": "https://acme.strait.link", + "appId": "com.acme.app", + "certSha256": [], + "file": { + "status": 404, + "body": "{\"error\":\"not found\"}" + } + }, + "ok": false, + "findings": [ + { + "id": "key.ok", + "severity": "ok", + "title": "Publishable key is set", + "fix": "" + }, + { + "id": "host.file_missing", + "severity": "error", + "title": "acme.strait.link doesn't serve assetlinks.json", + "fix": "Use your Strait link domain as endpoint (Dashboard → Get started); Strait serves this file there." + } + ] + }, + { + "name": "host.file_missing android 404 with a JSON string body", + "input": { + "platform": "android", + "publishableKey": "st_pub_live_x", + "endpoint": "https://acme.strait.link", + "appId": "com.acme.app", + "certSha256": [], + "file": { + "status": 404, + "body": "\"not configured\"" + } + }, + "ok": false, + "findings": [ + { + "id": "key.ok", + "severity": "ok", + "title": "Publishable key is set", + "fix": "" + }, + { + "id": "host.file_missing", + "severity": "error", + "title": "acme.strait.link doesn't serve assetlinks.json", + "fix": "Use your Strait link domain as endpoint (Dashboard → Get started); Strait serves this file there." + } + ] + }, + { + "name": "host.file_missing android 404 not configured with spaces (only the exact engine body counts)", + "input": { + "platform": "android", + "publishableKey": "st_pub_live_x", + "endpoint": "https://acme.strait.link", + "appId": "com.acme.app", + "certSha256": [], + "file": { + "status": 404, + "body": "{ \"error\": \"not configured\" }" + } + }, + "ok": false, + "findings": [ + { + "id": "key.ok", + "severity": "ok", + "title": "Publishable key is set", + "fix": "" + }, + { + "id": "host.file_missing", + "severity": "error", + "title": "acme.strait.link doesn't serve assetlinks.json", + "fix": "Use your Strait link domain as endpoint (Dashboard → Get started); Strait serves this file there." + } + ] + }, + { + "name": "host.file_missing android 404 not configured in single quotes", + "input": { + "platform": "android", + "publishableKey": "st_pub_live_x", + "endpoint": "https://acme.strait.link", + "appId": "com.acme.app", + "certSha256": [], + "file": { + "status": 404, + "body": "{'error':'not configured'}" + } + }, + "ok": false, + "findings": [ + { + "id": "key.ok", + "severity": "ok", + "title": "Publishable key is set", + "fix": "" + }, + { + "id": "host.file_missing", + "severity": "error", + "title": "acme.strait.link doesn't serve assetlinks.json", + "fix": "Use your Strait link domain as endpoint (Dashboard → Get started); Strait serves this file there." + } + ] + }, + { + "name": "host.file_missing ios 500 with the not configured body", + "input": { + "platform": "ios", + "publishableKey": "st_pub_live_x", + "endpoint": "https://acme.strait.link", + "appId": "com.acme.app", + "certSha256": [], + "file": { + "status": 500, + "body": "{\"error\":\"not configured\"}" + } + }, + "ok": false, + "findings": [ + { + "id": "key.ok", + "severity": "ok", + "title": "Publishable key is set", + "fix": "" + }, + { + "id": "host.file_missing", + "severity": "error", + "title": "acme.strait.link doesn't serve apple-app-site-association", + "fix": "Use your Strait link domain as endpoint (Dashboard → Get started); Strait serves this file there." + } + ] + }, { "name": "app.none android empty array", "input": { @@ -567,7 +799,7 @@ "id": "app.none", "severity": "error", "title": "No Android app is set up for acme.strait.link", - "fix": "Add your app in Dashboard → Apps & stores." + "fix": "Fill in Dashboard → Apps & stores (package name and SHA-256 fingerprint), wait a minute, then check again. Already done? Make sure acme.strait.link is your link domain from Dashboard → Get started." } ] }, @@ -596,7 +828,7 @@ "id": "app.none", "severity": "error", "title": "No Android app is set up for acme.strait.link", - "fix": "Add your app in Dashboard → Apps & stores." + "fix": "Fill in Dashboard → Apps & stores (package name and SHA-256 fingerprint), wait a minute, then check again. Already done? Make sure acme.strait.link is your link domain from Dashboard → Get started." } ] }, @@ -625,7 +857,7 @@ "id": "app.none", "severity": "error", "title": "No iOS app is set up for acme.strait.link", - "fix": "Add your app in Dashboard → Apps & stores." + "fix": "Fill in Dashboard → Apps & stores (Team ID and bundle ID), wait a minute, then check again. Already done? Make sure acme.strait.link is your link domain from Dashboard → Get started." } ] }, diff --git a/test/doctor.test.mjs b/test/doctor.test.mjs index b41e9c6..cf21175 100644 --- a/test/doctor.test.mjs +++ b/test/doctor.test.mjs @@ -11,8 +11,8 @@ import { HOST, KEY, sink } from './helpers.mjs'; const VECTORS = new URL('./doctor-vectors.json', import.meta.url); const { vectors } = JSON.parse(readFileSync(VECTORS, 'utf8')); -test('the 43 shared doctor vectors: same ids, severities and wording as the SDKs', () => { - assert.ok(vectors.length >= 43); +test('the shared doctor vectors: same ids, severities and wording as the SDKs', () => { + assert.ok(vectors.length >= 51); for (const v of vectors) { const { file, appId, ...rest } = v.input; const report = doctorFindings({ @@ -106,6 +106,21 @@ test('doctor: unreachable host and missing file use the SDK wording', async () = assert.match(r.out, /error\s+host\.file_missing\s+acme\.strait\.link doesn't serve apple-app-site-association/); }); +// User-lens audit item 10: the most common first-time state. The link domain is right, but Apps & stores is +// empty, so Strait answers 404 {"error":"not configured"}. The fix is Apps & stores, as `strait init` says. +test('doctor before Apps & stores is filled in: app.none with the Apps & stores fix, not "use your link domain"', async () => { + const notConfigured = [404, '{"error":"not configured"}']; + const r = await run(['doctor', '--host', HOST, '--android', 'com.acme.shop', '--ios', 'com.acme.shop'], { + fetch: fakeFetch({ 'assetlinks.json': notConfigured, 'apple-app-site-association': notConfigured }), + }); + assert.equal(r.code, 1); + assert.doesNotMatch(r.out, /host\.file_missing|Use your Strait link domain as endpoint/); + assert.match(r.out, /error\s+app\.none\s+No Android app is set up for acme\.strait\.link/); + assert.match(r.out, /Fix: Fill in Dashboard → Apps & stores \(package name and SHA-256 fingerprint\), wait a minute, then check again\./); + assert.match(r.out, /error\s+app\.none\s+No iOS app is set up for acme\.strait\.link/); + assert.match(r.out, /Fix: Fill in Dashboard → Apps & stores \(Team ID and bundle ID\), wait a minute, then check again\./); +}); + test('doctor: a secret key is flagged and never printed', async () => { const secret = 'st_live_abcdef0123456789abcdef0123456789'; const r = await run(['doctor', '--host', HOST, '--key', secret]);