@kurtbruns/kestrel 1.13.0 → 1.15.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/CHANGELOG.md +44 -1
- package/README.md +2 -2
- package/bin/kestrel.mjs +21 -2
- package/cli/dev.mjs +6 -2
- package/cli/init.mjs +2 -1
- package/cli/reset.mjs +1 -1
- package/cli/seed.mjs +3 -3
- package/dist/public/dashboard/app.js +290 -285
- package/dist/public/dashboard/app.js.map +4 -4
- package/dist/public/dashboard/index.html +2 -2
- package/dist/public/dashboard/styles.css +74 -40
- package/dist/worker/index.js +2723 -1192
- package/docs/README.md +3 -2
- package/docs/get-started/02-deploy.md +18 -21
- package/docs/get-started/03-access.md +10 -8
- package/docs/get-started/06-go-live.md +2 -1
- package/docs/guides/01-allow-claude.md +68 -0
- package/docs/guides/02-automate.md +88 -0
- package/docs/guides/09-as-your-list-grows.md +2 -2
- package/docs/publishing/01-your-list.md +3 -3
- package/docs/publishing/03-name-your-publication.md +1 -1
- package/docs/publishing/04-write-a-post.md +15 -52
- package/docs/publishing/06-troubleshooting.md +20 -9
- package/docs/publishing/08-connect-claude.md +60 -0
- package/docs/publishing/09-share-your-subscribe-link.md +1 -1
- package/docs/publishing/10-check-your-setup.md +9 -16
- package/docs/publishing/11-email-template.md +1 -1
- package/docs/publishing/README.md +2 -2
- package/docs/reference/01-configuration.md +132 -85
- package/docs/reference/02-sending-domain-dns.md +2 -2
- package/migrations/0013_who_acted_client.sql +20 -0
- package/migrations/0014_send_acts.sql +25 -0
- package/package.json +2 -1
- package/packages/kestrel/lib/api.mjs +462 -0
- package/packages/kestrel/lib/credentials.mjs +192 -0
- package/{cli → packages/kestrel/lib}/dev-api.mjs +21 -11
- package/{cli → packages/kestrel/lib}/dev-server.mjs +2 -2
- package/packages/kestrel/lib/handoff.mjs +118 -0
- package/packages/kestrel/lib/login.mjs +155 -0
- package/packages/kestrel/lib/oauth.mjs +368 -0
- package/packages/kestrel/lib/openapi.mjs +569 -0
- package/template/README.md +2 -0
- package/template/claude/CLAUDE.md +8 -5
- package/docs/guides/01-connect-claude.md +0 -101
- package/docs/publishing/07-working-with-claude.md +0 -29
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
* is actually serving. The token comes from the dev-only `/api/dev/token` route, which
|
|
9
9
|
* exists only on a local dev server (fake transport, no Access, a loopback origin).
|
|
10
10
|
*
|
|
11
|
+
* It lives in the client package, beside `kestrel api`, because `kestrel api --local` finds and
|
|
12
|
+
* signs in to the dev server the same way; the instance package's seed and reset import it
|
|
13
|
+
* from the copy of the client it ships, so the two never disagree about the record.
|
|
14
|
+
*
|
|
11
15
|
* Seed and reset wipe the database of the server they reach, so a target they find for
|
|
12
16
|
* themselves must prove it is this directory's: the port `kestrel dev` recorded here, and a
|
|
13
|
-
* server there that reports the run ID recorded with it (
|
|
17
|
+
* server there that reports the run ID recorded with it (packages/kestrel/lib/dev-server.mjs). A target the
|
|
14
18
|
* caller names, a port or URL argument, is their choice and is used as given. `PORT` names
|
|
15
19
|
* nothing here: a shell can carry it without the caller meaning anything by it, and a
|
|
16
20
|
* target that wipes a database has to be named on purpose.
|
|
@@ -68,12 +72,18 @@ export function ownsServer(target, reportedId) {
|
|
|
68
72
|
/**
|
|
69
73
|
* The dev server to act on and a local admin token for it, `{ base, token }`, or exit saying
|
|
70
74
|
* why there is none to act on (see `chooseTarget`). `arg` is the command's port or URL
|
|
71
|
-
* argument, if any; `tag` names the command, as in `npm run <tag>`.
|
|
75
|
+
* argument, if any; `tag` names the command, as in `npm run <tag>`. `kind` is the token's
|
|
76
|
+
* principal: `service` for a script, as seed and reset are, and `human` for a person working
|
|
77
|
+
* through a client, as `kestrel api` is (SPEC §11, Who acted).
|
|
72
78
|
*/
|
|
73
|
-
export async function devTarget(arg, tag) {
|
|
79
|
+
export async function devTarget(arg, tag, kind = "service") {
|
|
74
80
|
const target = chooseTarget({ arg, record: readDevServer() });
|
|
81
|
+
const naming =
|
|
82
|
+
tag === "api"
|
|
83
|
+
? "`npx kestrel api --url http://localhost:<port> …`"
|
|
84
|
+
: `\`npm run ${tag} -- <port>\``;
|
|
75
85
|
const deliberately = [
|
|
76
|
-
`To act on a server deliberately, name its port:
|
|
86
|
+
`To act on a server deliberately, name its port: ${naming}.`,
|
|
77
87
|
...(process.env.PORT
|
|
78
88
|
? [
|
|
79
89
|
`(PORT=${process.env.PORT} is set, but PORT doesn't name a target here: a shell can carry it unawares.)`,
|
|
@@ -88,7 +98,7 @@ export async function devTarget(arg, tag) {
|
|
|
88
98
|
...deliberately,
|
|
89
99
|
);
|
|
90
100
|
}
|
|
91
|
-
const { token, runId } = await devToken(target.base, tag);
|
|
101
|
+
const { token, runId } = await devToken(target.base, tag, kind);
|
|
92
102
|
if (!ownsServer(target, runId)) {
|
|
93
103
|
fail(
|
|
94
104
|
tag,
|
|
@@ -101,7 +111,7 @@ export async function devTarget(arg, tag) {
|
|
|
101
111
|
);
|
|
102
112
|
}
|
|
103
113
|
if (target.explicit) {
|
|
104
|
-
console.
|
|
114
|
+
console.error(`[${tag}] acting on ${target.base}, as named`);
|
|
105
115
|
}
|
|
106
116
|
return { base: target.base, token };
|
|
107
117
|
}
|
|
@@ -116,13 +126,13 @@ export function fail(tag, ...lines) {
|
|
|
116
126
|
process.exit(1);
|
|
117
127
|
}
|
|
118
128
|
|
|
119
|
-
/** Mint a local admin token
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
async function devToken(base, tag) {
|
|
129
|
+
/** Mint a local admin token for a principal of `kind`, with the run ID the server reports
|
|
130
|
+
* (undefined when it reports none), as `{ token, runId }`, or exit saying why the server
|
|
131
|
+
* can't give one. */
|
|
132
|
+
async function devToken(base, tag, kind) {
|
|
123
133
|
let res;
|
|
124
134
|
try {
|
|
125
|
-
res = await fetch(`${base}/api/dev/token?kind
|
|
135
|
+
res = await fetch(`${base}/api/dev/token?kind=${kind}`);
|
|
126
136
|
} catch (err) {
|
|
127
137
|
fail(
|
|
128
138
|
tag,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* The record of this directory's dev server, shared between `kestrel dev` (cli/dev.mjs) and
|
|
2
|
+
* The record of this directory's dev server, shared between `kestrel dev` (cli/dev.mjs in @kurtbruns/kestrel) and
|
|
3
3
|
* the seed and reset commands: the port it bound and the ID of that run.
|
|
4
4
|
*
|
|
5
5
|
* Why this exists: seed and reset wipe the database of whichever server they reach, and more
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* stopped can name a port another directory's server now holds. So `kestrel dev` makes up an ID
|
|
13
13
|
* for each run, records it here with the port, and hands it to the Worker (the `DEV_RUN_ID`
|
|
14
14
|
* var), which reports it beside the dev token. Seed and reset with no target given go only to
|
|
15
|
-
* the recorded port, and only when the server there reports this record's ID (
|
|
15
|
+
* the recorded port, and only when the server there reports this record's ID (packages/kestrel/lib/dev-api.mjs).
|
|
16
16
|
*
|
|
17
17
|
* Scope: the file lives under `.wrangler/`, which is gitignored and per-directory (each worktree
|
|
18
18
|
* has its own `.wrangler/state`), so the record is scoped to this directory and never committed.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* How the client hands an instance command to the instance package, so `kestrel <command>`
|
|
3
|
+
* stays one entry point although the commands live in two packages.
|
|
4
|
+
*
|
|
5
|
+
* `init`, `dev`, `seed`, `reset`, `sync-migrations`, and `check-context` act on an instance's
|
|
6
|
+
* repository and belong to the release it runs, so they live in `@kurtbruns/kestrel`, which
|
|
7
|
+
* each instance installs and pins. The client finds that install from the directory it runs
|
|
8
|
+
* in, the way Node resolves an import from there, and runs its `kestrel` command with the same
|
|
9
|
+
* arguments. `init` alone runs where nothing is installed yet: it fetches the instance package
|
|
10
|
+
* at the client's own version through npx, so `npx kestrel@X.Y.Z init` writes an X.Y.Z
|
|
11
|
+
* instance. Plain Node, no dependency, as the whole client is.
|
|
12
|
+
*/
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
|
+
import { constants } from "node:os";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
|
|
19
|
+
/** The package that holds the instance commands. */
|
|
20
|
+
export const INSTANCE_PACKAGE = "@kurtbruns/kestrel";
|
|
21
|
+
|
|
22
|
+
/** The commands that act on an instance's repository, and so run from the instance package. */
|
|
23
|
+
export const INSTANCE_COMMANDS = [
|
|
24
|
+
"init",
|
|
25
|
+
"dev",
|
|
26
|
+
"sync-migrations",
|
|
27
|
+
"check-context",
|
|
28
|
+
"seed",
|
|
29
|
+
"reset",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The instance package as installed for `dir` (or a directory above it): `{ root, bin }`, with
|
|
34
|
+
* `bin` the path of its `kestrel` script, or undefined when the install has none; or undefined
|
|
35
|
+
* when no install is found.
|
|
36
|
+
*/
|
|
37
|
+
export function findInstance(dir = process.cwd()) {
|
|
38
|
+
let manifest;
|
|
39
|
+
try {
|
|
40
|
+
manifest = createRequire(join(dir, "package.json")).resolve(`${INSTANCE_PACKAGE}/package.json`);
|
|
41
|
+
} catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const root = dirname(manifest);
|
|
45
|
+
const { bin } = JSON.parse(readFileSync(manifest, "utf8"));
|
|
46
|
+
const script = typeof bin === "string" ? bin : bin?.kestrel;
|
|
47
|
+
return { root, bin: script ? join(root, script) : undefined };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One argument for cmd.exe, which splits on spaces and reads its own metacharacters. */
|
|
51
|
+
function quoteForCmd(arg) {
|
|
52
|
+
return /^[\w@=./:\\-]+$/.test(arg) ? arg : `"${arg.replace(/"/g, '""')}"`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* How to run instance `command`, as `{ cmd, args, shell }`, or `{ refuse }` with words for the
|
|
57
|
+
* person when there is no install to run it from. `version` is the client's, which `init`
|
|
58
|
+
* fetches when nothing is installed. `KESTREL_INSTANCE_PACKAGE` names another package spec to
|
|
59
|
+
* fetch, such as a packed tarball, which the package check uses before a release exists.
|
|
60
|
+
*/
|
|
61
|
+
export function handoff(
|
|
62
|
+
command,
|
|
63
|
+
argv,
|
|
64
|
+
{ dir = process.cwd(), version, env = process.env, platform = process.platform } = {},
|
|
65
|
+
) {
|
|
66
|
+
const found = findInstance(dir);
|
|
67
|
+
if (found?.bin) {
|
|
68
|
+
return { cmd: process.execPath, args: [found.bin, command, ...argv], shell: false };
|
|
69
|
+
}
|
|
70
|
+
if (found) {
|
|
71
|
+
return {
|
|
72
|
+
refuse: `the ${INSTANCE_PACKAGE} installed at ${found.root} has no kestrel command, so it can't run \`kestrel ${command}\`. Reinstall it with \`npm install\`.`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (command === "init") {
|
|
76
|
+
const spec = env.KESTREL_INSTANCE_PACKAGE || `${INSTANCE_PACKAGE}@${version}`;
|
|
77
|
+
const args = ["--yes", `--package=${spec}`, "--", "kestrel", command, ...argv];
|
|
78
|
+
// npx is a .cmd script on Windows, which Node spawns only through a shell, and a shell
|
|
79
|
+
// splits what it is given: so it gets one command line, each argument quoted for cmd.exe.
|
|
80
|
+
if (platform === "win32") {
|
|
81
|
+
return { cmd: ["npx.cmd", ...args].map(quoteForCmd).join(" "), args: [], shell: true };
|
|
82
|
+
}
|
|
83
|
+
return { cmd: "npx", args, shell: false };
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
refuse: `\`kestrel ${command}\` runs in an instance's repository, with ${INSTANCE_PACKAGE} installed, and none is installed here. Run it in your instance's repository after \`npm install\`, or write a new instance with \`npx kestrel init\`.`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The signals passed on to the child when they are sent to this process alone.
|
|
91
|
+
const FORWARDED = ["SIGTERM", "SIGHUP", "SIGQUIT"];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Run instance `command` through the instance package, with the terminal attached, and exit
|
|
95
|
+
* as it does. A Ctrl-C reaches the child from the terminal itself, so this process only waits
|
|
96
|
+
* for it to finish (`kestrel dev` stops its server on the way out); another signal sent to
|
|
97
|
+
* this process alone is passed on. A child ended by a signal exits 128 plus its number, as a
|
|
98
|
+
* shell reports it.
|
|
99
|
+
*/
|
|
100
|
+
export function runInstanceCommand(command, argv, version) {
|
|
101
|
+
const plan = handoff(command, argv, { version });
|
|
102
|
+
if (plan.refuse) {
|
|
103
|
+
console.error(`kestrel: ${plan.refuse}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
const child = spawn(plan.cmd, plan.args, { stdio: "inherit", shell: plan.shell });
|
|
107
|
+
process.on("SIGINT", () => {});
|
|
108
|
+
for (const signal of FORWARDED) {
|
|
109
|
+
process.on(signal, () => child.kill(signal));
|
|
110
|
+
}
|
|
111
|
+
child.on("error", (err) => {
|
|
112
|
+
console.error(`kestrel: could not run ${INSTANCE_PACKAGE}: ${err.message}`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
});
|
|
115
|
+
child.on("exit", (code, signal) => {
|
|
116
|
+
process.exit(code ?? (signal ? 128 + (constants.signals[signal] ?? 0) : 0));
|
|
117
|
+
});
|
|
118
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* `kestrel login` and `kestrel logout`: sign this computer in to a deployed instance as the
|
|
3
|
+
* person at it, through Cloudflare Access's Managed OAuth, and forget that sign-in again.
|
|
4
|
+
*
|
|
5
|
+
* The person approves the sign-in in their browser, through the same Access policy the
|
|
6
|
+
* editor's sign-in passes, so Claude Code working through `kestrel api` afterwards is that
|
|
7
|
+
* person, not a token of its own (SPEC §11). The OAuth steps are packages/kestrel/lib/oauth.mjs, and where
|
|
8
|
+
* the result is kept is packages/kestrel/lib/credentials.mjs.
|
|
9
|
+
*/
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import { randomBytes } from "node:crypto";
|
|
12
|
+
import { signedInFetch } from "./api.mjs";
|
|
13
|
+
import { credentialsFile, readSignIn, readSignIns, writeSignIn } from "./credentials.mjs";
|
|
14
|
+
import {
|
|
15
|
+
authorizeUrl,
|
|
16
|
+
DEFAULT_CLIENT,
|
|
17
|
+
discover,
|
|
18
|
+
exchangeCode,
|
|
19
|
+
instanceOrigin,
|
|
20
|
+
isLoopback,
|
|
21
|
+
listenForCode,
|
|
22
|
+
PROTECTED_PATH,
|
|
23
|
+
pkcePair,
|
|
24
|
+
registerClient,
|
|
25
|
+
revokeTokens,
|
|
26
|
+
SignInError,
|
|
27
|
+
tokenFields,
|
|
28
|
+
} from "./oauth.mjs";
|
|
29
|
+
import { unauthorizedMove } from "./openapi.mjs";
|
|
30
|
+
|
|
31
|
+
/** Open `url` in the person's browser, best effort: the URL is printed either way. */
|
|
32
|
+
function openBrowser(url) {
|
|
33
|
+
const [command, args] =
|
|
34
|
+
process.platform === "darwin"
|
|
35
|
+
? ["open", [url]]
|
|
36
|
+
: process.platform === "win32"
|
|
37
|
+
? ["cmd", ["/c", "start", '""', url.replace(/&/g, "^&")]]
|
|
38
|
+
: ["xdg-open", [url]];
|
|
39
|
+
try {
|
|
40
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
41
|
+
child.on("error", () => {});
|
|
42
|
+
child.unref();
|
|
43
|
+
} catch {
|
|
44
|
+
/* no browser to open: the printed URL still works */
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Sign in to `origin`, store the result, and answer who it signed in as. */
|
|
49
|
+
export async function signIn(origin, { browser }) {
|
|
50
|
+
const found = await discover(origin);
|
|
51
|
+
const stored = readSignIn(origin);
|
|
52
|
+
// A client registered for this computer is reused while its redirect can be served again,
|
|
53
|
+
// so a sign-in doesn't leave a new registration in Access each time.
|
|
54
|
+
const reuse =
|
|
55
|
+
stored?.client_id && stored.registration_endpoint === found.registration_endpoint
|
|
56
|
+
? stored
|
|
57
|
+
: undefined;
|
|
58
|
+
const state = randomBytes(16).toString("base64url");
|
|
59
|
+
const listener = await listenForCode({
|
|
60
|
+
port: reuse ? Number(new URL(reuse.redirect_uri).port) : undefined,
|
|
61
|
+
state,
|
|
62
|
+
});
|
|
63
|
+
try {
|
|
64
|
+
const clientId =
|
|
65
|
+
reuse && reuse.redirect_uri === listener.redirectUri
|
|
66
|
+
? reuse.client_id
|
|
67
|
+
: await registerClient(found.registration_endpoint, listener.redirectUri);
|
|
68
|
+
const entry = { ...found, client_id: clientId, redirect_uri: listener.redirectUri };
|
|
69
|
+
const { verifier, challenge } = pkcePair();
|
|
70
|
+
const url = authorizeUrl({
|
|
71
|
+
endpoint: found.authorization_endpoint,
|
|
72
|
+
clientId,
|
|
73
|
+
redirectUri: listener.redirectUri,
|
|
74
|
+
challenge,
|
|
75
|
+
state,
|
|
76
|
+
resource: found.resource,
|
|
77
|
+
});
|
|
78
|
+
console.log(`Approve the sign-in to ${origin} in your browser.`);
|
|
79
|
+
console.log(`If it didn't open, go to:\n\n ${url}\n`);
|
|
80
|
+
if (browser) {
|
|
81
|
+
openBrowser(url);
|
|
82
|
+
}
|
|
83
|
+
const code = await listener.code;
|
|
84
|
+
const fields = tokenFields(await exchangeCode(entry, { code, verifier }));
|
|
85
|
+
await writeSignIn(origin, { ...entry, ...fields });
|
|
86
|
+
} finally {
|
|
87
|
+
listener.close();
|
|
88
|
+
}
|
|
89
|
+
const res = await signedInFetch(origin, PROTECTED_PATH, { client: DEFAULT_CLIENT });
|
|
90
|
+
const body = await res.json().catch(() => null);
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
const move = unauthorizedMove(body);
|
|
93
|
+
throw new SignInError(
|
|
94
|
+
move
|
|
95
|
+
? `Signed in to Access, but ${origin}${PROTECTED_PATH} answered ${res.status} (${body.reason}). ${move}.`
|
|
96
|
+
: `Signed in to Access, but ${origin}${PROTECTED_PATH} answered ${res.status}${body?.message ? `: ${body.message}` : ""}.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return body?.principal;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** `kestrel login <url> [--no-browser]`. */
|
|
103
|
+
export async function loginCommand(argv) {
|
|
104
|
+
const browser = !argv.includes("--no-browser");
|
|
105
|
+
const urls = argv.filter((a) => a !== "--no-browser");
|
|
106
|
+
try {
|
|
107
|
+
if (urls.length !== 1) {
|
|
108
|
+
throw new SignInError("Usage: kestrel login <url> [--no-browser]");
|
|
109
|
+
}
|
|
110
|
+
const origin = instanceOrigin(urls[0]);
|
|
111
|
+
if (isLoopback(origin)) {
|
|
112
|
+
throw new SignInError(
|
|
113
|
+
"A local dev server needs no sign-in: `npx kestrel api --local …` calls it as you.",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const principal = await signIn(origin, { browser });
|
|
117
|
+
const who = principal?.email ?? principal?.kind ?? "someone";
|
|
118
|
+
console.log(`Signed in to ${origin} as ${who}.`);
|
|
119
|
+
console.log(
|
|
120
|
+
`Stored in ${credentialsFile()}. \`npx kestrel api GET /api/posts\` calls it as you.`,
|
|
121
|
+
);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.error(`kestrel login: ${err instanceof Error ? err.message : String(err)}`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** `kestrel logout [url]`: revoke and forget one sign-in, or the only one stored. */
|
|
129
|
+
export async function logoutCommand(argv) {
|
|
130
|
+
try {
|
|
131
|
+
const origins = Object.keys(readSignIns());
|
|
132
|
+
const origin = argv[0] ? instanceOrigin(argv[0]) : origins.length === 1 ? origins[0] : null;
|
|
133
|
+
if (!origin) {
|
|
134
|
+
throw new SignInError(
|
|
135
|
+
origins.length
|
|
136
|
+
? `You're signed in to ${origins.join(", ")}. Name the one to sign out of.`
|
|
137
|
+
: "You aren't signed in to any instance.",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const entry = readSignIn(origin);
|
|
141
|
+
if (!entry) {
|
|
142
|
+
throw new SignInError(`You aren't signed in to ${origin}.`);
|
|
143
|
+
}
|
|
144
|
+
const revoked = await revokeTokens(entry);
|
|
145
|
+
await writeSignIn(origin, undefined);
|
|
146
|
+
console.log(
|
|
147
|
+
revoked
|
|
148
|
+
? `Signed out of ${origin}, and Access revoked the sign-in.`
|
|
149
|
+
: `Signed out of ${origin} on this computer. Access didn't confirm revoking it; it ends with the Access application's grant session.`,
|
|
150
|
+
);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.error(`kestrel logout: ${err instanceof Error ? err.message : String(err)}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
}
|