breakaway 1.4.0-main.60 → 1.4.0-main.62
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/package.json +1 -1
- package/scripts/tasks/install-check.js +54 -0
- package/scripts/tasks.mjs +49 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakaway",
|
|
3
|
-
"version": "1.4.0-main.
|
|
3
|
+
"version": "1.4.0-main.62",
|
|
4
4
|
"description": "The task board for you and your coding agents: a Cloudflare Worker, its web app, Taskwarrior sync, and the CLI (npx breakaway).",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
* repos remove) name them from the checkout's breakaway.config.json. In breakaway's own checkout that's the template's
|
|
4
4
|
* defaults, and under npx in another repository the package's, so before writing, the CLI checks the config is the
|
|
5
5
|
* board's own install, as the board's health reports it (BRK-95). Pure, so it's tested without a board.
|
|
6
|
+
*
|
|
7
|
+
* Asking the board takes the token. When the owner has lost it, rotate-token can't ask, so the owner names the install
|
|
8
|
+
* instead (CLI-5): the Worker's name, or the Secrets Store's ID, has to match the config before anything is written.
|
|
6
9
|
*/
|
|
7
10
|
|
|
11
|
+
import { secretName } from '../../src/install.js';
|
|
12
|
+
|
|
8
13
|
/** What the CLI compares, and how each is named when they differ. @type {[string, (v: any) => string][]} */
|
|
9
14
|
const FIELDS = [
|
|
10
15
|
['worker', (v) => `the Worker ${v}`],
|
|
@@ -39,3 +44,52 @@ export function checkInstall(local, board, { command, configFile }) {
|
|
|
39
44
|
message: `${configFile} isn't this board's install: it says ${describe(local)}, and the board is ${describe(board)}. Nothing was written. Run npx breakaway ${command} from ${from}.`,
|
|
40
45
|
};
|
|
41
46
|
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Where rotate-token writes the new token, as the checkout's config says: the Secrets Store's secret, or the Worker's.
|
|
50
|
+
*
|
|
51
|
+
* @param {{ worker: string, secretsPrefix: string, secretsStore: string | null }} local
|
|
52
|
+
*/
|
|
53
|
+
export function tokenTarget(local) {
|
|
54
|
+
return local.secretsStore
|
|
55
|
+
? `${secretName(local, 'API_TOKEN')} in the Secrets Store ${local.secretsStore}, for the Worker ${local.worker}`
|
|
56
|
+
: `the secret TASKS_API_TOKEN on the Worker ${local.worker}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* What rotate-token says when the board refused the token, or there's none (CLI-5): it can't ask the board which
|
|
61
|
+
* install it is, so it says where it would write and what the owner confirms.
|
|
62
|
+
*
|
|
63
|
+
* @param {{ worker: string, secretsPrefix: string, secretsStore: string | null }} local
|
|
64
|
+
* @param {{ configFile: string, token: boolean }} where `token`: whether there was a token the board refused
|
|
65
|
+
* @returns {string}
|
|
66
|
+
*/
|
|
67
|
+
export function unverifiedInstall(local, { configFile, token }) {
|
|
68
|
+
return [
|
|
69
|
+
token
|
|
70
|
+
? "The board refused this machine's token, so rotate-token can't ask it which install it is."
|
|
71
|
+
: "There's no token on this machine, so rotate-token can't ask the board which install it is.",
|
|
72
|
+
`${configFile} says the new token goes in ${tokenTarget(local)}.`,
|
|
73
|
+
`If that's this board, confirm with the Worker's name${local.secretsStore ? " or the Secrets Store's ID" : ''}; if it isn't, stop and run this from the checkout the board deploys from.`,
|
|
74
|
+
].join('\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Whether the owner's answer names the install the config describes: the Worker's name, or with a Secrets Store, its ID.
|
|
79
|
+
* Without a match nothing is written.
|
|
80
|
+
*
|
|
81
|
+
* @param {{ worker: string, secretsStore: string | null }} local
|
|
82
|
+
* @param {string | null | undefined} answer
|
|
83
|
+
* @param {{ configFile: string }} where
|
|
84
|
+
* @returns {{ ok: boolean, message?: string }}
|
|
85
|
+
*/
|
|
86
|
+
export function confirmInstall(local, answer, { configFile }) {
|
|
87
|
+
const given = String(answer ?? '').trim();
|
|
88
|
+
if (given && (given === local.worker || (local.secretsStore && given === local.secretsStore))) return { ok: true };
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
message: given
|
|
92
|
+
? `"${given.slice(0, 64)}" isn't the Worker ${local.worker}${local.secretsStore ? ` or the Secrets Store ${local.secretsStore}` : ''} that ${configFile} names, so nothing was written. Run npx breakaway rotate-token from the checkout the board deploys from.`
|
|
93
|
+
: `rotate-token needs the Worker's name to confirm, so nothing was written.`,
|
|
94
|
+
};
|
|
95
|
+
}
|
package/scripts/tasks.mjs
CHANGED
|
@@ -45,7 +45,7 @@ import { hookFailure, sessionProxy, routeThroughSessionProxy } from './tasks/pro
|
|
|
45
45
|
import { githubFromRemote, inRepo, pickRepo } from './tasks/repo.js';
|
|
46
46
|
import { appInPlace } from './tasks/github-connect.js';
|
|
47
47
|
import { unsupportedSystem, wranglerFailure } from './tasks/platform.js';
|
|
48
|
-
import { checkInstall } from './tasks/install-check.js';
|
|
48
|
+
import { checkInstall, confirmInstall, tokenTarget, unverifiedInstall } from './tasks/install-check.js';
|
|
49
49
|
import { NO_TERMINAL, ask as askIn } from './tasks/ask.js';
|
|
50
50
|
import {
|
|
51
51
|
CLI_PACKAGE,
|
|
@@ -145,6 +145,28 @@ async function ensureBoardInstall(command, health = null) {
|
|
|
145
145
|
if (result.warning) console.error(`tasks: ${result.warning}`);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/**
|
|
149
|
+
* rotate-token without a token the board accepts (CLI-5): it can't ask the board which install it is, so it says
|
|
150
|
+
* where it would write, and the owner names the Worker (or the Secrets Store) with --worker or when asked.
|
|
151
|
+
*/
|
|
152
|
+
async function confirmUnverifiedInstall(hadToken) {
|
|
153
|
+
const local = installConfig();
|
|
154
|
+
const where = { configFile: tildePath(CONFIG_FILE, homedir()) };
|
|
155
|
+
console.log(unverifiedInstall(local, { ...where, token: hadToken }));
|
|
156
|
+
let answer = opts.worker;
|
|
157
|
+
if (answer === undefined) {
|
|
158
|
+
if (!process.stdin.isTTY) fail(`${NO_TERMINAL}, or pass --worker <name>.`);
|
|
159
|
+
const { createInterface } = await import('node:readline');
|
|
160
|
+
answer = await askIn("The Worker's name: ", {
|
|
161
|
+
input: process.stdin,
|
|
162
|
+
output: process.stdout,
|
|
163
|
+
createInterface,
|
|
164
|
+
}).catch((error) => fail(error.message));
|
|
165
|
+
}
|
|
166
|
+
const confirmed = confirmInstall(local, answer, where);
|
|
167
|
+
if (!confirmed.ok) fail(confirmed.message);
|
|
168
|
+
}
|
|
169
|
+
|
|
148
170
|
/** Stops `command` before it does anything on a system the install doesn't support: Windows outside WSL (CLI-3). */
|
|
149
171
|
function ensureSupportedSystem(command) {
|
|
150
172
|
const message = unsupportedSystem(command, process.platform);
|
|
@@ -304,8 +326,9 @@ Working
|
|
|
304
326
|
|
|
305
327
|
Setup (owner)
|
|
306
328
|
setup connect this machine's Taskwarrior (writes taskrc in this machine's folder for the board, with every repository's report and context)
|
|
307
|
-
rotate-sync new client ID and sync secret; the server re-encrypts its history
|
|
308
|
-
rotate-token new API token; every browser is signed out
|
|
329
|
+
rotate-sync new client ID and sync secret; the server re-encrypts its history (the old secret isn't needed)
|
|
330
|
+
rotate-token new API token; every browser is signed out. Works without the old one: you confirm the install
|
|
331
|
+
by the Worker's name, asked for or given with --worker <name>
|
|
309
332
|
github-connect <code> store the GitHub App's keys (the board's GitHub view gives the code); refuses when the board
|
|
310
333
|
already has an App, unless --replace
|
|
311
334
|
agents-connect store the agent routine's URL and token (docs/tasks.md#cloud-agents-from-the-board)
|
|
@@ -1643,12 +1666,17 @@ const commands = {
|
|
|
1643
1666
|
/**
|
|
1644
1667
|
* Owner: new client ID and sync secret. The server re-encrypts its whole history under the new
|
|
1645
1668
|
* key in one step and keeps every version ID, so replicas carry on once they have the new
|
|
1646
|
-
* values. The new values are saved before the server switches, so they can't be lost.
|
|
1669
|
+
* values. The new values are saved before the server switches, so they can't be lost. The old
|
|
1670
|
+
* secret isn't needed: the server re-encrypts with the key it holds, so this is also how an
|
|
1671
|
+
* owner who lost the secret everywhere gets Taskwarrior sync back (CLI-5).
|
|
1647
1672
|
*/
|
|
1648
1673
|
async 'rotate-sync'() {
|
|
1649
1674
|
ensureSupportedSystem('rotate-sync');
|
|
1650
1675
|
const env = readEnvFile();
|
|
1651
|
-
if (!readSetting('SECRET', { file: env }))
|
|
1676
|
+
if (!readSetting('SECRET', { file: env }))
|
|
1677
|
+
console.log(
|
|
1678
|
+
`${ENV_FILE} has no sync secret. That's fine: the board re-encrypts its history with the key it holds, and this writes new values.`,
|
|
1679
|
+
);
|
|
1652
1680
|
const health = await call('GET', 'health');
|
|
1653
1681
|
if (!health.ok) fail(`the server can't read its history (${health.replicaError}); fix that first (docs/tasks.md).`);
|
|
1654
1682
|
await ensureBoardInstall('rotate-sync', health);
|
|
@@ -1679,10 +1707,23 @@ const commands = {
|
|
|
1679
1707
|
].join('\n'),
|
|
1680
1708
|
);
|
|
1681
1709
|
},
|
|
1682
|
-
/**
|
|
1710
|
+
/**
|
|
1711
|
+
* Owner: a new API token. Every browser is signed out; cloud environments need the new one. With the
|
|
1712
|
+
* old token lost, or refused, the board can't say which install it is, so the owner confirms the
|
|
1713
|
+
* one the checkout's config names (CLI-5): --worker <name>, or typed when asked.
|
|
1714
|
+
*/
|
|
1683
1715
|
async 'rotate-token'() {
|
|
1684
1716
|
ensureSupportedSystem('rotate-token');
|
|
1685
|
-
|
|
1717
|
+
const old = setting('TOKEN');
|
|
1718
|
+
let res;
|
|
1719
|
+
try {
|
|
1720
|
+
res = await fetch(`${BASE}/api/health`, { headers: old ? { Authorization: `Bearer ${old}` } : {} });
|
|
1721
|
+
} catch (error) {
|
|
1722
|
+
fail(`can't reach ${BASE} (${reasonOf(error)}), so nothing was changed.`);
|
|
1723
|
+
}
|
|
1724
|
+
if (res.status === 401) await confirmUnverifiedInstall(Boolean(old));
|
|
1725
|
+
else if (!res.ok) fail(`the board answered HTTP ${res.status} to health, so nothing was changed.`);
|
|
1726
|
+
else await ensureBoardInstall('rotate-token', await res.json());
|
|
1686
1727
|
const env = readEnvFile();
|
|
1687
1728
|
const token = randomBytes(32).toString('base64url');
|
|
1688
1729
|
writePrivate(`${ENV_FILE}.next`, envFile({ ...env, [envName('TOKEN')]: token }));
|
|
@@ -1704,7 +1745,7 @@ const commands = {
|
|
|
1704
1745
|
await new Promise((r) => setTimeout(r, 2000));
|
|
1705
1746
|
}
|
|
1706
1747
|
fail(
|
|
1707
|
-
|
|
1748
|
+
`${tokenTarget(installConfig())} has the new token, but the server doesn't accept it yet. Try npx breakaway health in a minute.`,
|
|
1708
1749
|
);
|
|
1709
1750
|
},
|
|
1710
1751
|
/** Owner, once for a new board: fresh credentials in tasks.env (the Secrets Store comes next). */
|