@straitlink/cli 0.3.5 → 0.3.7
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 +5 -5
- package/package.json +1 -1
- package/src/app.mjs +4 -3
- package/src/cli.mjs +11 -8
- package/src/doctor.mjs +6 -6
- package/src/install.mjs +4 -4
- package/src/links.mjs +1 -1
- package/src/selftest.mjs +1 -1
- package/src/validate.mjs +5 -5
- package/src/version.mjs +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ From a checkout of this repo: `node bin/strait.mjs init ../my-app`, or `npm link
|
|
|
51
51
|
|
|
52
52
|
| Option | |
|
|
53
53
|
|---|---|
|
|
54
|
-
| `--key` | Publishable key (Dashboard →
|
|
54
|
+
| `--key` | Publishable key (Dashboard → Setup → Your keys). Env: `STRAIT_PUBLISHABLE_KEY` |
|
|
55
55
|
| `--host` | Link domain, e.g. `acme.strait.link`. A pasted link works too. Env: `STRAIT_LINK_HOST` |
|
|
56
56
|
| `--scheme` | Custom scheme: the one you typed in Dashboard → Apps & stores. Taken from `app.json`, or guessed from the package name (`com.acme.shop` → `acmeshop`); a guess is marked "(guessed from …)" in the output |
|
|
57
57
|
| `--framework` | `react-native`, `expo`, `flutter` or `android`, to skip detection |
|
|
@@ -83,14 +83,14 @@ Android (assetlinks.json)
|
|
|
83
83
|
| `--key` | Also check the publishable key. Env: `STRAIT_PUBLISHABLE_KEY`. Left out, the key check is skipped |
|
|
84
84
|
| `--json` | The reports as JSON, for CI and agents |
|
|
85
85
|
|
|
86
|
-
With a secret key (`STRAIT_SECRET_KEY` or `--secret-key`), `doctor` also runs [`strait app verify`](#whats-wrong-with-my-setup-strait-app-verify) and prints its checks under "Account"; `--host` is then optional. If those checks can't run (a refused or malformed key, no connection, the 6-a-minute limit) it says "Could not check"
|
|
86
|
+
With a secret key (`STRAIT_SECRET_KEY` or `--secret-key`), `doctor` also runs [`strait app verify`](#whats-wrong-with-my-setup-strait-app-verify) and prints its checks under "Account"; `--host` is then optional. If those checks can't run (a refused or malformed key, no connection, the 6-a-minute limit) it says "Could not check". They are shown but don't change the exit code, so a CI job that already sets `STRAIT_SECRET_KEY` keeps passing as before; add `--strict` to exit `1` when one fails or can't run. `--json` adds them as `verify` (its own `ok` inside); the top-level `ok` matches the exit code.
|
|
87
87
|
|
|
88
88
|
Exit codes: `0` no errors (warnings allowed), `1` a check failed, `2` usage problem. What a terminal can't see (intent filters, the Associated Domains entitlement) is checked by `doctor()` inside a debug build and by the [free tools](https://straitlink.in/tools/).
|
|
89
89
|
|
|
90
90
|
## Manage links: `strait links`
|
|
91
91
|
|
|
92
92
|
```bash
|
|
93
|
-
export STRAIT_SECRET_KEY=st_live_… # Dashboard → Settings →
|
|
93
|
+
export STRAIT_SECRET_KEY=st_live_… # Dashboard → Settings → Keys
|
|
94
94
|
npx @straitlink/cli links create summer https://acme.com/summer --campaign sale --tag email
|
|
95
95
|
npx @straitlink/cli links list --status active --limit 20
|
|
96
96
|
npx @straitlink/cli links qr summer --size 512 # writes summer.png
|
|
@@ -104,7 +104,7 @@ Exit codes: `0` done, `1` the API said no or can't be reached (its message on st
|
|
|
104
104
|
## Am I set up right? `strait test`
|
|
105
105
|
|
|
106
106
|
```bash
|
|
107
|
-
export STRAIT_SECRET_KEY=st_live_… # Dashboard → Settings →
|
|
107
|
+
export STRAIT_SECRET_KEY=st_live_… # Dashboard → Settings → Keys
|
|
108
108
|
npx @straitlink/cli test
|
|
109
109
|
```
|
|
110
110
|
|
|
@@ -126,7 +126,7 @@ One read-only pass: the engine answers (`GET /healthz`), the secret key is valid
|
|
|
126
126
|
## What's wrong with my setup? `strait app verify`
|
|
127
127
|
|
|
128
128
|
```bash
|
|
129
|
-
export STRAIT_SECRET_KEY=st_live_… # Dashboard → Settings →
|
|
129
|
+
export STRAIT_SECRET_KEY=st_live_… # Dashboard → Settings → Keys (read or full access)
|
|
130
130
|
npx @straitlink/cli app # the setup Strait has: apps, link domains and their files, SDKs seen
|
|
131
131
|
npx @straitlink/cli app verify # every check, with the fix for each problem
|
|
132
132
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@straitlink/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "Strait deep links from the terminal: init sets them up in a React Native, Expo, Flutter or Android app, doctor checks your link host, app verify checks your whole setup with a fix for each problem, links creates and lists links and saves QR codes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"strait",
|
package/src/app.mjs
CHANGED
|
@@ -99,8 +99,9 @@ function formatVerify(answer) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/** `strait doctor`'s account block: the verify answer, or why it could not run (never a silent pass). */
|
|
102
|
-
export function formatAccount(answer) {
|
|
102
|
+
export function formatAccount(answer, strict = false) {
|
|
103
103
|
const head = 'Account (strait app verify)';
|
|
104
|
-
|
|
105
|
-
return lines([head,
|
|
104
|
+
const soft = !strict && answer?.ok !== true ? ['These checks don\'t change doctor\'s exit code: add --strict to make doctor fail on them.'] : [];
|
|
105
|
+
if (answer?.error) return lines([head, ` Could not check: ${answer.error}`, ...soft]);
|
|
106
|
+
return lines([head, ...verifyLines(answer), '', verifyVerdict(answer), ...soft]);
|
|
106
107
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -31,6 +31,7 @@ const OPTIONS = {
|
|
|
31
31
|
cert: { type: 'string', multiple: true },
|
|
32
32
|
'secret-key': { type: 'string' },
|
|
33
33
|
'api-url': { type: 'string' },
|
|
34
|
+
strict: { type: 'boolean', default: false },
|
|
34
35
|
json: { type: 'boolean', default: false },
|
|
35
36
|
},
|
|
36
37
|
links: {
|
|
@@ -77,7 +78,7 @@ Usage
|
|
|
77
78
|
strait events add <name> [dir] [options] Print ready-to-paste code that tracks an event
|
|
78
79
|
|
|
79
80
|
init options
|
|
80
|
-
--key <st_pub_live_…> Publishable key (Dashboard →
|
|
81
|
+
--key <st_pub_live_…> Publishable key (Dashboard → Setup → Your keys)
|
|
81
82
|
--host <host> Your link domain, e.g. acme.strait.link
|
|
82
83
|
--scheme <scheme> Your app's custom scheme, the one in Dashboard → Apps & stores,
|
|
83
84
|
e.g. acmeshop (guessed from the package name if left out)
|
|
@@ -93,14 +94,15 @@ doctor options (the same checks and messages as doctor() in the SDKs)
|
|
|
93
94
|
--cert <sha256> Android signing certificate fingerprint (repeatable)
|
|
94
95
|
--key <st_pub_live_…> Also check the publishable key (env STRAIT_PUBLISHABLE_KEY)
|
|
95
96
|
--secret-key <st_live_…> Also run strait app verify (env STRAIT_SECRET_KEY); --host is then optional.
|
|
96
|
-
If it can't run (refused key, no connection), doctor says so
|
|
97
|
+
If it can't run (refused key, no connection), doctor says so
|
|
98
|
+
--strict Exit 1 when an account check fails or can't run (default: shown, exit unchanged)
|
|
97
99
|
--api-url <url> Engine for those checks (env STRAIT_API_URL, default https://strait.link)
|
|
98
100
|
--json Print the reports as JSON
|
|
99
101
|
With neither --android nor --ios, both files are checked and the apps they list are shown.
|
|
100
102
|
Exit 1 when a check fails.
|
|
101
103
|
|
|
102
104
|
links options
|
|
103
|
-
--secret-key <st_live_…> Secret key (Dashboard → Settings →
|
|
105
|
+
--secret-key <st_live_…> Secret key (Dashboard → Settings → Keys). Prefer the
|
|
104
106
|
env STRAIT_SECRET_KEY: flags stay in your shell history
|
|
105
107
|
--api-url <url> API base (env STRAIT_API_URL, default https://strait.link)
|
|
106
108
|
--title, --campaign, --tag <tag> (repeatable) create: link details
|
|
@@ -194,7 +196,7 @@ export async function main(argv, io = {}) {
|
|
|
194
196
|
if (canAsk && (!key || !host || !scheme)) {
|
|
195
197
|
const ask = io.ask ?? (await terminalAsker(io.stdin ?? process.stdin, out));
|
|
196
198
|
try {
|
|
197
|
-
key = key ?? (await askValid(ask, 'Publishable key (Dashboard →
|
|
199
|
+
key = key ?? (await askValid(ask, 'Publishable key (Dashboard → Setup → Your keys, st_pub_live_…): ', normalizeKey, err));
|
|
198
200
|
host = host ?? (await askValid(ask, 'Link host (e.g. acme.strait.link): ', normalizeHost, err));
|
|
199
201
|
if (!scheme) {
|
|
200
202
|
const guess = suggestScheme(detectProject(dir) ?? {});
|
|
@@ -246,7 +248,7 @@ async function terminalAsker(input, output) {
|
|
|
246
248
|
async function doctor({ values: v }, { out, env, io }) {
|
|
247
249
|
const hostIn = v.host ?? env.STRAIT_LINK_HOST;
|
|
248
250
|
const secretIn = v['secret-key'] ?? env.STRAIT_SECRET_KEY;
|
|
249
|
-
if (!hostIn && !secretIn) throw new UsageError('Missing --host: your link domain from Dashboard →
|
|
251
|
+
if (!hostIn && !secretIn) throw new UsageError('Missing --host: your link domain from Dashboard → Setup → Your keys, e.g. --host acme.strait.link. Or set STRAIT_SECRET_KEY to check your whole setup.');
|
|
250
252
|
const fetchImpl = io.fetch ?? fetch;
|
|
251
253
|
let result = { ok: true };
|
|
252
254
|
if (hostIn) {
|
|
@@ -255,7 +257,8 @@ async function doctor({ values: v }, { out, env, io }) {
|
|
|
255
257
|
result = await runDoctor({ host: normalizeHost(hostIn), key, android: v.android?.trim(), ios: v.ios?.trim(), certs: v.cert ?? [], fetch: fetchImpl });
|
|
256
258
|
}
|
|
257
259
|
if (secretIn) {
|
|
258
|
-
// A secret key adds the engine's checks (docs/adr/0003)
|
|
260
|
+
// A secret key adds the engine's checks (docs/adr/0003), always shown; if they can't run, that is shown too.
|
|
261
|
+
// They change the exit code only with --strict, so a CI job that already sets the key keeps passing as before.
|
|
259
262
|
try {
|
|
260
263
|
const call = apiClient({ apiUrl: v['api-url'] ?? env.STRAIT_API_URL ?? DEFAULT_API_URL, key: secretKey(secretIn), fetch: fetchImpl, userAgent: `strait-cli/${CLI_VERSION}` });
|
|
261
264
|
result.verify = await getVerify(call);
|
|
@@ -263,9 +266,9 @@ async function doctor({ values: v }, { out, env, io }) {
|
|
|
263
266
|
if (!(e instanceof ApiError || e instanceof UsageError)) throw e;
|
|
264
267
|
result.verify = { error: e.message };
|
|
265
268
|
}
|
|
266
|
-
if (result.verify?.ok !== true) result.ok = false;
|
|
269
|
+
if (v.strict && result.verify?.ok !== true) result.ok = false;
|
|
267
270
|
}
|
|
268
|
-
const text = v.json ? toJson(result) : [hostIn && formatDoctor(result), secretIn && formatAccount(result.verify)].filter(Boolean).join('\n\n');
|
|
271
|
+
const text = v.json ? toJson(result) : [hostIn && formatDoctor(result), secretIn && formatAccount(result.verify, v.strict)].filter(Boolean).join('\n\n');
|
|
269
272
|
out.write(text + '\n');
|
|
270
273
|
return result.ok ? 0 : 1;
|
|
271
274
|
}
|
package/src/doctor.mjs
CHANGED
|
@@ -51,12 +51,12 @@ export function doctorFindings(input) {
|
|
|
51
51
|
const key = input.publishableKey ?? '';
|
|
52
52
|
if (key.startsWith('st_pub_')) findings.push(ok('key.ok', 'Publishable key is set'));
|
|
53
53
|
else if (key.startsWith('st_live_') || key.startsWith('st_test_'))
|
|
54
|
-
findings.push(err('key.secret', 'This is a secret key, not the publishable key', 'Use the publishable key (st_pub_…) from Dashboard →
|
|
55
|
-
else findings.push(err('key.invalid', 'Not a publishable key', 'Copy the publishable key (st_pub_…) from Dashboard →
|
|
54
|
+
findings.push(err('key.secret', 'This is a secret key, not the publishable key', 'Use the publishable key (st_pub_…) from Dashboard → Setup → Your keys, and revoke this secret key in Dashboard → Settings → Keys: it has been inside your app.'));
|
|
55
|
+
else findings.push(err('key.invalid', 'Not a publishable key', 'Copy the publishable key (st_pub_…) from Dashboard → Setup → Your keys.'));
|
|
56
56
|
|
|
57
57
|
const host = doctorHost(input.endpoint ?? '');
|
|
58
58
|
if (!host) {
|
|
59
|
-
findings.push(err('endpoint.invalid', "The endpoint isn't an https link domain", 'Set endpoint to your link domain from Dashboard →
|
|
59
|
+
findings.push(err('endpoint.invalid', "The endpoint isn't an https link domain", 'Set endpoint to your link domain from Dashboard → Setup → Your keys, e.g. https://<handle>.strait.link.'));
|
|
60
60
|
} else if (input.file) findings.push(...hostFindings(input, host, input.file));
|
|
61
61
|
return { ok: !findings.some((f) => f.severity === 'error'), findings };
|
|
62
62
|
}
|
|
@@ -71,12 +71,12 @@ const notConfigured = (file) => file.status === 404 && file.body === NOT_CONFIGU
|
|
|
71
71
|
function hostFindings(input, host, file) {
|
|
72
72
|
const android = input.platform === 'android';
|
|
73
73
|
if (file.status === undefined)
|
|
74
|
-
return [err('host.unreachable', `Can't reach ${host}`, 'Check that endpoint is your link domain (Dashboard →
|
|
74
|
+
return [err('host.unreachable', `Can't reach ${host}`, 'Check that endpoint is your link domain (Dashboard → Setup → Your keys) and that this device is online.')];
|
|
75
75
|
const apps = file.status === 200 ? listedApps(input.platform, file.body ?? '') : null;
|
|
76
76
|
if (!apps && !notConfigured(file))
|
|
77
|
-
return [err('host.file_missing', `${host} doesn't serve ${android ? 'assetlinks.json' : 'apple-app-site-association'}`, 'Use your Strait link domain as endpoint (Dashboard →
|
|
77
|
+
return [err('host.file_missing', `${host} doesn't serve ${android ? 'assetlinks.json' : 'apple-app-site-association'}`, 'Use your Strait link domain as endpoint (Dashboard → Setup → Your keys); Strait serves this file there.')];
|
|
78
78
|
if (!apps?.size)
|
|
79
|
-
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 →
|
|
79
|
+
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 → Setup → Your keys.`)];
|
|
80
80
|
|
|
81
81
|
const listed = [...apps.keys()].join(', ');
|
|
82
82
|
const app = input.appId?.trim();
|
package/src/install.mjs
CHANGED
|
@@ -23,10 +23,10 @@ export const sourceFor = (src, registry) => (typeof src.source === 'string' ? sr
|
|
|
23
23
|
* `strait events add` reads them too: its iOS/Android eventId needs these SDKs.
|
|
24
24
|
*/
|
|
25
25
|
export const VERSIONS = {
|
|
26
|
-
version: '0.11.
|
|
27
|
-
rnVersion: '0.11.
|
|
28
|
-
flutterVersion: '0.10.
|
|
29
|
-
androidVersion: '0.10.
|
|
26
|
+
version: '0.11.6', // Swift (Unity is not written by this CLI)
|
|
27
|
+
rnVersion: '0.11.5',
|
|
28
|
+
flutterVersion: '0.10.8',
|
|
29
|
+
androidVersion: '0.10.6',
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
export const PACKAGES = {
|
package/src/links.mjs
CHANGED
|
@@ -14,7 +14,7 @@ export class ApiError extends Error {}
|
|
|
14
14
|
/** The secret key for the link API, or a UsageError that says where to get one. Never echoes the value. */
|
|
15
15
|
export function secretKey(value) {
|
|
16
16
|
const k = String(value ?? '').trim().replace(/^['"]+|['"]+$/g, '').replace(/^Bearer\s+/i, '');
|
|
17
|
-
const where = 'Create one in Dashboard → Settings →
|
|
17
|
+
const where = 'Create one in Dashboard → Settings → Keys (https://app.straitlink.in/settings#keys) and set STRAIT_SECRET_KEY (or pass --secret-key).';
|
|
18
18
|
if (!k) throw new UsageError(`The link API needs a secret key (st_live_…). ${where}`);
|
|
19
19
|
if (k.startsWith('st_pub_')) throw new UsageError(`That is the publishable key (st_pub_…), which only works inside apps. The link API needs a secret key (st_live_…). ${where}`);
|
|
20
20
|
if (!SECRET.test(k)) throw new UsageError(`That is not a secret key (st_live_… or st_test_…). ${where}`);
|
package/src/selftest.mjs
CHANGED
|
@@ -48,7 +48,7 @@ export async function runSelfTest({ call, apiUrl, key, fetch: fetchImpl }) {
|
|
|
48
48
|
checks.push(pass('key', 'Key', `valid ${mode} key${prefix ? ` ${prefix}…` : ''} for workspace "${workspace.name ?? '?'}"${access}`));
|
|
49
49
|
} catch (e) {
|
|
50
50
|
if (!(e instanceof ApiError)) throw e;
|
|
51
|
-
checks.push(fail('key', 'Key', e.message, 'Create a secret key in Dashboard → Settings →
|
|
51
|
+
checks.push(fail('key', 'Key', e.message, 'Create a secret key in Dashboard → Settings → Keys (https://app.straitlink.in/settings#keys) and set STRAIT_SECRET_KEY.'));
|
|
52
52
|
checks.push(...DOMAIN_CHECKS.map(([id, l]) => skip(id, l, 'needs a valid key')));
|
|
53
53
|
return done();
|
|
54
54
|
}
|
package/src/validate.mjs
CHANGED
|
@@ -15,15 +15,15 @@ const EXAMPLE_BODIES = new Set(['8fK2mQx7Lp4RtZ']);
|
|
|
15
15
|
*/
|
|
16
16
|
export function normalizeKey(value) {
|
|
17
17
|
let k = String(value ?? '').trim().replace(/^['"]+|['"]+$/g, '');
|
|
18
|
-
if (!k) throw new UsageError('A publishable key is required (--key st_pub_live_…). Find it in Dashboard →
|
|
18
|
+
if (!k) throw new UsageError('A publishable key is required (--key st_pub_live_…). Find it in Dashboard → Setup → Your keys (https://app.straitlink.in/setup#keys).');
|
|
19
19
|
if (SECRET_KEY.test(k)) {
|
|
20
20
|
throw new UsageError(
|
|
21
|
-
'That is a secret key (st_live_…). Apps must use the publishable key (st_pub_live_…) from Dashboard →
|
|
22
|
-
'If the secret key was pasted somewhere public, rotate it in Dashboard → Settings →
|
|
21
|
+
'That is a secret key (st_live_…). Apps must use the publishable key (st_pub_live_…) from Dashboard → Setup → Your keys. ' +
|
|
22
|
+
'If the secret key was pasted somewhere public, rotate it in Dashboard → Settings → Keys.',
|
|
23
23
|
);
|
|
24
24
|
}
|
|
25
25
|
const m = /^st_pub_(live|test)_(.*)$/.exec(k);
|
|
26
|
-
const where = (mode = 'live') => `Copy your real one from Dashboard →
|
|
26
|
+
const where = (mode = 'live') => `Copy your real one from Dashboard → Setup → Your keys (the Publishable key box): st_pub_${mode}_ followed by 32 letters and numbers.`;
|
|
27
27
|
if (!m) throw new UsageError(`That is not a publishable key. ${where()}`);
|
|
28
28
|
const [, mode] = m;
|
|
29
29
|
let rest = m[2].replace(/\.$/, ''); // a full stop copied from a sentence
|
|
@@ -42,7 +42,7 @@ export function normalizeKey(value) {
|
|
|
42
42
|
/** "https://Acme.strait.link/summer" → "acme.strait.link". */
|
|
43
43
|
export function normalizeHost(value) {
|
|
44
44
|
let h = String(value ?? '').trim().toLowerCase();
|
|
45
|
-
if (!h) throw new UsageError('A link host is required (--host acme.strait.link). It is your workspace link domain in Dashboard →
|
|
45
|
+
if (!h) throw new UsageError('A link host is required (--host acme.strait.link). It is your workspace link domain in Dashboard → Setup → Your keys.');
|
|
46
46
|
h = h.replace(/^[a-z][a-z0-9+.-]*:\/\//, '').replace(/[/?#].*$/, '').replace(/\.$/, '');
|
|
47
47
|
const labels = h.split('.');
|
|
48
48
|
if (h.length > 253 || labels.length < 2 || !labels.every((l) => LABEL.test(l)) || !/^[a-z]{2,}$/.test(labels.at(-1))) {
|
package/src/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Kept equal to package.json version (test/links.test.mjs checks it). Sent as the API User-Agent.
|
|
2
|
-
export const CLI_VERSION = '0.3.
|
|
2
|
+
export const CLI_VERSION = '0.3.7';
|