bikeshed-now 0.9.0 → 0.10.0
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 +4 -0
- package/package.json +1 -1
- package/src/client.mjs +35 -1
- package/src/index.mjs +75 -16
package/README.md
CHANGED
|
@@ -28,6 +28,10 @@ and run `bikeshed login --paste`. The token is shown once. In CI, set
|
|
|
28
28
|
`BIKESHED_ORIGIN` and `BIKESHED_TOKEN` instead of running `login` at all; they
|
|
29
29
|
outrank whatever `login` saved.
|
|
30
30
|
|
|
31
|
+
`bikeshed logout` signs this computer out. It revokes a token from browser
|
|
32
|
+
sign-in as well as deleting it. A pasted token is only deleted, because CI may
|
|
33
|
+
be using the same one: revoke it at `/account` once nothing needs it.
|
|
34
|
+
|
|
31
35
|
## The loop
|
|
32
36
|
|
|
33
37
|
```bash
|
package/package.json
CHANGED
package/src/client.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
import { basename } from "node:path";
|
|
6
6
|
|
|
7
|
-
export const CLIENT_NAME = "bikeshed-cli/0.
|
|
7
|
+
export const CLIENT_NAME = "bikeshed-cli/0.10.0";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Three surfaces, and the split is deliberate rather than untidy.
|
|
@@ -225,6 +225,40 @@ export async function exchangeCode(tokenEndpoint, { code, verifier, redirectUri,
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
/** Check that a token works, and say who it belongs to. */
|
|
228
|
+
/**
|
|
229
|
+
* Revoke the token being presented, and only it (SPEC §23).
|
|
230
|
+
*
|
|
231
|
+
* A 401 from /tokens means one of two things that need different advice: the
|
|
232
|
+
* token had already stopped working, or the deployment is older than a token
|
|
233
|
+
* revoking itself and still takes only a browser session there. One call with
|
|
234
|
+
* the same token to /mcp tells them apart. Returns "revoked", "invalid" or
|
|
235
|
+
* "unsupported"; a deployment that cannot be reached throws.
|
|
236
|
+
*/
|
|
237
|
+
export async function revokeOwnToken(origin, token) {
|
|
238
|
+
const res = await fetch(`${origin}/tokens`, {
|
|
239
|
+
method: "POST",
|
|
240
|
+
headers: headers(token, { "Content-Type": "application/json" }),
|
|
241
|
+
body: JSON.stringify({ op: "revoke" }),
|
|
242
|
+
});
|
|
243
|
+
if (res.ok) return "revoked";
|
|
244
|
+
if (res.status !== 401) {
|
|
245
|
+
throw new CliError(await readError(res, `the server returned ${res.status}`));
|
|
246
|
+
}
|
|
247
|
+
const probe = await fetch(`${origin}/mcp`, {
|
|
248
|
+
method: "POST",
|
|
249
|
+
headers: headers(token, { "Content-Type": "application/json" }),
|
|
250
|
+
body: JSON.stringify({
|
|
251
|
+
jsonrpc: "2.0",
|
|
252
|
+
id: 1,
|
|
253
|
+
method: "tools/call",
|
|
254
|
+
params: { name: "list_documents", arguments: { limit: 1 } },
|
|
255
|
+
}),
|
|
256
|
+
});
|
|
257
|
+
if (probe.status === 401) return "invalid";
|
|
258
|
+
if (probe.ok) return "unsupported";
|
|
259
|
+
throw new CliError(`the server returned ${probe.status}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
228
262
|
export async function whoami(origin, token) {
|
|
229
263
|
// list_documents is the cheapest authenticated call, and a token that can
|
|
230
264
|
// list is a token that works.
|
package/src/index.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
fetchSource,
|
|
17
17
|
listSkills,
|
|
18
18
|
printable,
|
|
19
|
+
revokeOwnToken,
|
|
19
20
|
uploadFile,
|
|
20
21
|
whoami,
|
|
21
22
|
} from "./client.mjs";
|
|
@@ -33,15 +34,8 @@ import {
|
|
|
33
34
|
} from "./login.mjs";
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* against production, so the difference is stated in `--help`, in the README,
|
|
39
|
-
* and here:
|
|
40
|
-
*
|
|
41
|
-
* scripts/*.mjs whoever owns the deployment. Admin credentials. Talks to
|
|
42
|
-
* Firestore and Storage directly. Provisions and withdraws.
|
|
43
|
-
* bikeshed any signed-up user. A personal access token. Talks to the
|
|
44
|
-
* deployment over HTTPS. Publishes and revises documents.
|
|
37
|
+
* A person's own client: their personal access token, over HTTPS, able to do
|
|
38
|
+
* exactly what they can do in the browser and nothing more.
|
|
45
39
|
*
|
|
46
40
|
* Every command takes `--json`, so it composes inside a script or an agent's
|
|
47
41
|
* shell without anybody parsing human-readable output.
|
|
@@ -50,6 +44,7 @@ import {
|
|
|
50
44
|
const USAGE = `bikeshed — publish and revise documents
|
|
51
45
|
|
|
52
46
|
bikeshed login sign in through your browser (--paste for a token)
|
|
47
|
+
bikeshed logout sign out (revokes a token from browser sign-in)
|
|
53
48
|
bikeshed list documents you can read
|
|
54
49
|
bikeshed publish <file> create a new document
|
|
55
50
|
bikeshed update <id> <file> publish a new version
|
|
@@ -116,10 +111,6 @@ Options
|
|
|
116
111
|
|
|
117
112
|
Environment
|
|
118
113
|
BIKESHED_ORIGIN, BIKESHED_TOKEN, BIKESHED_AGENT
|
|
119
|
-
|
|
120
|
-
This is the end-user tool. The scripts in scripts/ are the OPERATOR tools: they
|
|
121
|
-
use admin credentials, talk to the database directly, and can tear a deployment
|
|
122
|
-
down. They are not the same thing.
|
|
123
114
|
`;
|
|
124
115
|
|
|
125
116
|
function fail(message) {
|
|
@@ -275,7 +266,9 @@ async function browserLogin(flags) {
|
|
|
275
266
|
});
|
|
276
267
|
// Checked before it is stored, the same as a pasted one.
|
|
277
268
|
await whoami(where, token);
|
|
278
|
-
|
|
269
|
+
// `via` is what lets `logout` revoke this token: one made by browser sign-in
|
|
270
|
+
// belongs to this computer alone, where a pasted one may be in CI as well.
|
|
271
|
+
writeConfig({ ...readConfig(), origin: where, token, via: "browser" });
|
|
279
272
|
|
|
280
273
|
// The environment outranks the file (resolveAuth), so a variable left over
|
|
281
274
|
// from an earlier session would quietly keep being used instead.
|
|
@@ -313,13 +306,78 @@ async function pasteLogin(flags) {
|
|
|
313
306
|
// Verified before it is stored, so a typo fails here rather than on the
|
|
314
307
|
// next command with a confusing message.
|
|
315
308
|
await whoami(where, token);
|
|
316
|
-
writeConfig({ ...readConfig(), origin: where, token });
|
|
309
|
+
writeConfig({ ...readConfig(), origin: where, token, via: "paste" });
|
|
317
310
|
console.log(`\nSaved to ${CONFIG_PATH} (readable only by you).`);
|
|
318
311
|
} finally {
|
|
319
312
|
rl.close();
|
|
320
313
|
}
|
|
321
314
|
}
|
|
322
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Sign this computer out.
|
|
318
|
+
*
|
|
319
|
+
* Only the SAVED sign-in, never BIKESHED_TOKEN or --token: those belong to a
|
|
320
|
+
* shell or a job, and revoking one would sign out something nobody meant to
|
|
321
|
+
* touch. A token from browser sign-in exists for this computer alone, so it is
|
|
322
|
+
* revoked on the deployment as well as forgotten here. A pasted one may be in
|
|
323
|
+
* CI too, so it is only forgotten, as is one saved before `login` recorded
|
|
324
|
+
* which kind it was (SPEC §23).
|
|
325
|
+
*
|
|
326
|
+
* The token is forgotten whatever the deployment says, and when it cannot be
|
|
327
|
+
* reached at all: after `logout`, this computer is signed out. `revoked` in
|
|
328
|
+
* --json means the deployment confirmed the token no longer works.
|
|
329
|
+
*/
|
|
330
|
+
async function logout(flags) {
|
|
331
|
+
const config = readConfig();
|
|
332
|
+
const origin = String(config.origin || resolveAuth().origin).replace(/\/+$/, "");
|
|
333
|
+
const shadowed = () => {
|
|
334
|
+
if (process.env.BIKESHED_TOKEN) {
|
|
335
|
+
console.error("BIKESHED_TOKEN is set in this shell, so commands here still use it. Unset it to sign out here too.");
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
if (!config.token) {
|
|
340
|
+
out(flags.json, { origin, revoked: false, forgot: false }, () => console.log("Not signed in on this computer."));
|
|
341
|
+
shadowed();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let outcome = "kept";
|
|
346
|
+
let why = "You pasted it in, so something else may use it too.";
|
|
347
|
+
if (config.via === "browser") {
|
|
348
|
+
try {
|
|
349
|
+
outcome = await revokeOwnToken(origin, config.token);
|
|
350
|
+
why = `${origin} is too old to revoke a token from the command line.`;
|
|
351
|
+
} catch (e) {
|
|
352
|
+
outcome = "failed";
|
|
353
|
+
why =
|
|
354
|
+
e instanceof CliError
|
|
355
|
+
? `${origin} did not revoke it (${printable(e.message, 80)}).`
|
|
356
|
+
: `Could not reach ${origin} to revoke it (${printable(e.cause?.code ?? e.message, 80)}).`;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const kept = { ...config, origin };
|
|
361
|
+
delete kept.token;
|
|
362
|
+
delete kept.via;
|
|
363
|
+
writeConfig(kept);
|
|
364
|
+
|
|
365
|
+
const revoked = outcome === "revoked" || outcome === "invalid";
|
|
366
|
+
out(flags.json, { origin, revoked, forgot: true }, () =>
|
|
367
|
+
console.log(
|
|
368
|
+
outcome === "revoked"
|
|
369
|
+
? `Signed out. ${origin} revoked the token, and it is gone from this computer.`
|
|
370
|
+
: outcome === "invalid"
|
|
371
|
+
? `Signed out. The token had already stopped working on ${origin}, and it is gone from this computer.`
|
|
372
|
+
: "Signed out. The token is gone from this computer.",
|
|
373
|
+
),
|
|
374
|
+
);
|
|
375
|
+
if (!revoked) {
|
|
376
|
+
console.error(`${why} It still works until you revoke it at ${origin}/account, under Access tokens.`);
|
|
377
|
+
}
|
|
378
|
+
shadowed();
|
|
379
|
+
}
|
|
380
|
+
|
|
323
381
|
async function list(flags) {
|
|
324
382
|
const { origin, token } = resolveAuth(flags);
|
|
325
383
|
const page = (limit, cursor) => {
|
|
@@ -737,6 +795,7 @@ async function watchFile(file, run) {
|
|
|
737
795
|
|
|
738
796
|
const COMMANDS = {
|
|
739
797
|
login,
|
|
798
|
+
logout,
|
|
740
799
|
list,
|
|
741
800
|
publish,
|
|
742
801
|
update,
|
|
@@ -755,7 +814,7 @@ const COMMANDS = {
|
|
|
755
814
|
};
|
|
756
815
|
|
|
757
816
|
/** Commands that reach a deployment without authenticating as anybody. */
|
|
758
|
-
const NO_TOKEN = new Set(["login", "open", "skills"]);
|
|
817
|
+
const NO_TOKEN = new Set(["login", "logout", "open", "skills"]);
|
|
759
818
|
|
|
760
819
|
async function main() {
|
|
761
820
|
const { values, positionals } = parseArgs({
|