sliftutils 1.7.124 → 1.7.126

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.
Files changed (50) hide show
  1. package/CLAUDE.md +3 -1
  2. package/bin/derivekey.js +10 -0
  3. package/bin/portsecuredaemon.js +13 -0
  4. package/bin/securessh.js +10 -0
  5. package/bin/setupnotify.js +9 -0
  6. package/bin/signfiles.js +10 -0
  7. package/bin/unrevoke.js +9 -0
  8. package/package.json +14 -3
  9. package/security/README.md +141 -0
  10. package/security/authorizedKeys/authorizedKeys.ts +66 -0
  11. package/security/authorizedKeys/daemon/authLog.ts +117 -0
  12. package/security/authorizedKeys/daemon/daemon.ts +308 -0
  13. package/security/authorizedKeys/daemon/git.ts +119 -0
  14. package/security/authorizedKeys/daemon/notify.ts +25 -0
  15. package/security/authorizedKeys/daemon/paths.ts +26 -0
  16. package/security/authorizedKeys/daemon/portsecure.service +19 -0
  17. package/security/authorizedKeys/daemon/revocation.ts +306 -0
  18. package/security/authorizedKeys/daemon/rootKeys.ts +135 -0
  19. package/security/authorizedKeys/daemon/sshdConfig.ts +85 -0
  20. package/security/authorizedKeys/daemon/state.ts +108 -0
  21. package/security/authorizedKeys/daemon/trust.ts +291 -0
  22. package/security/authorizedKeys/daemon/userKeys.ts +76 -0
  23. package/security/authorizedKeys/dist/authorizedKeys.ts.cache +73 -0
  24. package/security/authorizedKeys/dist/revokeSource.ts.cache +44 -0
  25. package/security/authorizedKeys/dist/secureSSH.ts.cache +552 -0
  26. package/security/authorizedKeys/dist/sources.ts.cache +24 -0
  27. package/security/authorizedKeys/dist/unrevoke.ts.cache +145 -0
  28. package/security/authorizedKeys/revokeSource.ts +40 -0
  29. package/security/authorizedKeys/secureSSH.ts +613 -0
  30. package/security/authorizedKeys/sources.ts +20 -0
  31. package/security/authorizedKeys/unrevoke.ts +149 -0
  32. package/security/helpers/dist/paths.ts.cache +28 -0
  33. package/security/helpers/dist/remoteSSH.ts.cache +90 -0
  34. package/security/helpers/dist/spawn.ts.cache +34 -0
  35. package/security/helpers/paths.ts +20 -0
  36. package/security/helpers/remoteSSH.ts +95 -0
  37. package/security/helpers/spawn.ts +36 -0
  38. package/security/keys/deriveKey.ts +72 -0
  39. package/security/keys/dist/deriveKey.ts.cache +72 -0
  40. package/security/keys/dist/sshKeyFile.ts.cache +153 -0
  41. package/security/keys/sshKeyFile.ts +156 -0
  42. package/security/notifications/discord.ts +190 -0
  43. package/security/notifications/dist/discord.ts.cache +180 -0
  44. package/security/notifications/remoteWebhook.ts +85 -0
  45. package/security/notifications/setupNotify.ts +29 -0
  46. package/security/signedFiles/dist/manifest.ts.cache +68 -0
  47. package/security/signedFiles/dist/signFiles.ts.cache +146 -0
  48. package/security/signedFiles/manifest.ts +69 -0
  49. package/security/signedFiles/signFiles.ts +151 -0
  50. package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +17 -20
package/CLAUDE.md CHANGED
@@ -8,10 +8,12 @@ from `.cursor/rules/*.mdc` so Claude reads them automatically.
8
8
  - The code automatically updates on save, so do not ever run commands to rerun the site.
9
9
  - Don't run shell commands when you need to create or move small code files. Use tool calls. Use tool calls to make files within folders — you don't need to make the folder, just make the file, the folder will be created automatically.
10
10
  - If you need to add a dependency, don't just edit `package.json`. Use `yarn add` so you get the latest version, unless the user specifies a version.
11
- - Use tool calls to read files and directories instead of running `ls`, `dir`, etc.
11
+ - Use tool calls to read and write files and directories, as opposed to shell commands running `ls`, `dir`, etc.
12
+
12
13
 
13
14
  ## Coding styles
14
15
 
16
+ - Do not use synchronous file IO, or synchronous child_process functions.
15
17
  - Times should almost always be in milliseconds; assume milliseconds if not told otherwise.
16
18
  - Don't make functions that will never be reused and are short. If under 5 lines and not reused, don't create it unless explicitly told to.
17
19
  - Comments are used sparingly and only when required to explain what's being done. A comment that just restates the function name is forbidden.
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // Derives a second ed25519 key from an existing one, so one key can stand behind several
4
+ // identities without any of them being stored.
5
+ require("typenode");
6
+
7
+ require("../security/keys/deriveKey").main().catch(e => {
8
+ console.error(`${e}`);
9
+ process.exitCode = 1;
10
+ }).finally(() => process.exit());
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // The daemon that keeps a machine's root authorized_keys equal to its key repos. Installed and
4
+ // started by securessh, and normally only run by systemd.
5
+ //
6
+ // Deliberately without the usual finally(process.exit) of the other entry points: this one is
7
+ // meant to keep running after main resolves, and exiting there would stop it dead on startup.
8
+ require("typenode");
9
+
10
+ require("../security/authorizedKeys/daemon/daemon").main().catch(e => {
11
+ console.error(`portsecure: failed to start. ${e && e.stack || e}`);
12
+ process.exit(1);
13
+ });
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // Manages which repos a host takes its root authorized_keys from, and installs the daemon that
4
+ // keeps them applied.
5
+ require("typenode");
6
+
7
+ require("../security/authorizedKeys/secureSSH").main().catch(e => {
8
+ console.error(`${e}`);
9
+ process.exitCode = 1;
10
+ }).finally(() => process.exit());
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // Sets a Discord webhook up on a remote host, so anything on it can raise notifications.
4
+ require("typenode");
5
+
6
+ require("../security/notifications/setupNotify").main().catch(e => {
7
+ console.error(`${e}`);
8
+ process.exitCode = 1;
9
+ }).finally(() => process.exit());
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // Signs the files of the repo in the current directory, so a machine pulling it can tell who
4
+ // published what it is about to trust.
5
+ require("typenode");
6
+
7
+ require("../security/signedFiles/signFiles").main().catch(e => {
8
+ console.error(`${e}`);
9
+ process.exitCode = 1;
10
+ }).finally(() => process.exit());
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // Undoes revocations, by writing a file into the keys repo naming the ones to undo.
4
+ require("typenode");
5
+
6
+ require("../security/authorizedKeys/unrevoke").main().catch(e => {
7
+ console.error(`${e}`);
8
+ process.exitCode = 1;
9
+ }).finally(() => process.exit());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliftutils",
3
- "version": "1.7.124",
3
+ "version": "1.7.126",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -28,7 +28,12 @@
28
28
  "notes": "mobx, preact, socket-function, typenode SHOULD be peerDependencies. But we want to use yarn (better dependency deduplication), so we can't use peerDependencies (as they aren't installed by default, which makes them a nightmare to use). If you want to override the versions, feel free to use overrides/resolutions.",
29
29
  "test": "typenode ./test.ts",
30
30
  "filehoster": "node ./bin/filehoster.js",
31
- "autohost": "node ./bin/autohost.js"
31
+ "autohost": "node ./bin/autohost.js",
32
+ "setupnotify": "node ./bin/setupnotify.js",
33
+ "securessh": "node ./bin/securessh.js",
34
+ "signfiles": "node ./bin/signfiles.js",
35
+ "derivekey": "node ./bin/derivekey.js",
36
+ "unrevoke": "node ./bin/unrevoke.js"
32
37
  },
33
38
  "bin": {
34
39
  "filehoster": "./bin/filehoster.js",
@@ -45,7 +50,13 @@
45
50
  "slift-watch": "./builders/watchRun.js",
46
51
  "sliftwatch": "./builders/watchRun.js",
47
52
  "slift-setup": "./builders/setupRun.js",
48
- "sliftsetup": "./builders/setupRun.js"
53
+ "sliftsetup": "./builders/setupRun.js",
54
+ "setupnotify": "./bin/setupnotify.js",
55
+ "securessh": "./bin/securessh.js",
56
+ "signfiles": "./bin/signfiles.js",
57
+ "derivekey": "./bin/derivekey.js",
58
+ "portsecuredaemon": "./bin/portsecuredaemon.js",
59
+ "unrevoke": "./bin/unrevoke.js"
49
60
  },
50
61
  "dependencies": {
51
62
  "@types/chrome": "^0.0.237",
@@ -0,0 +1,141 @@
1
+ # security
2
+
3
+ Tools for locking down machines. One folder per kind of security concern.
4
+
5
+ ## notifications
6
+
7
+ Discord notifications, so a machine can raise the alarm somewhere a person will see it.
8
+
9
+ The webhook lives on the machine that sends, in `/etc/portsecure/discord-webhook`, and nothing
10
+ starts without one. `configureDiscordNotifications` loads it and aborts the process if it is
11
+ missing, then watches the file and warns the old webhook if it is ever swapped.
12
+
13
+ yarn setupnotify <host> <discord-webhook-url> [replace]
14
+
15
+ `replace` is required to overwrite a host's existing webhook with a different one.
16
+
17
+ ## authorizedKeys
18
+
19
+ Keeps root's `authorized_keys` equal to the merged contents of one or more git repos, and turns
20
+ off every other way in.
21
+
22
+ yarn securessh <host> add <repo-private-key> [repo-url]
23
+ yarn securessh <host> remove [repo-url]
24
+ yarn securessh <host> list
25
+ yarn securessh <host> update
26
+
27
+ With no repo url, the repo you are standing in is used, as long as it holds keys and has an
28
+ origin to clone from.
29
+
30
+ The daemon runs on the host, so `update` is how a host picks up a newer build of it. It changes
31
+ nothing about which keys that host trusts.
32
+
33
+ `update` installs the daemon from this checkout, not from anything the host fetches, so it pulls
34
+ this checkout first and stops if that pull does not fast forward. It never installs code it
35
+ cannot account for.
36
+
37
+ Each source repo keeps its own deploy key. Both verbs refuse to run if the key you log in with
38
+ would not survive the change, since that would lock you out of the host permanently.
39
+
40
+ `daemon/portsecureDaemon.js` is what actually runs on the host, under systemd. It is plain
41
+ JavaScript on the Node built-ins alone so a target machine needs nothing installed. It:
42
+
43
+ - rewrites root's `authorized_keys` from the merged sources, archiving whatever it replaces
44
+ - reverts and reports edits made outside it, every 60 seconds
45
+ - reports changes to any other account's `authorized_keys`, without touching them
46
+ - polls every source every 5 minutes, and reports a repo whose history was rewritten
47
+ - disables password authentication, after checking `sshd` accepts the config
48
+
49
+ With no sources, or none readable, it leaves `authorized_keys` exactly as it is rather than
50
+ locking everyone out.
51
+
52
+ The daemon carries hand ports of several TypeScript files here, because it cannot import them.
53
+ Both copies are marked `PORTED CODE` and have to be changed together.
54
+
55
+ ## signedFiles
56
+
57
+ Signs everything in a repo, so a machine pulling it can tell who published what it is about to
58
+ trust. Run it inside the repo you want signed.
59
+
60
+ yarn signfiles [signing-key] [git]
61
+
62
+ `signedfiles.json` lists every non-ignored file with its size and sha256, and
63
+ `signedfiles.json.sig` is an ssh signature over it. With no key given, a hardware backed
64
+ `ed25519-sk` key at `~/.ssh/signfiles_ed25519_sk` is used, and created if missing - a key on disk
65
+ is compromised the moment the machine is, so the hardware key is the point. `git` also commits
66
+ and pushes, after signing, so a failed push never costs a second touch of the key.
67
+
68
+ ### How authorizedKeys uses it
69
+
70
+ The daemon records the signer it last accepted for each source, and the keys it accepted from
71
+ them, on disk. Those recorded values are the reference, not the repo, because the repo is what an
72
+ attacker would be rewriting.
73
+
74
+ When a source starts being signed by a different key, the daemon warns on Discord and keeps
75
+ applying the keys it last accepted. It applies the new ones only after 24 hours of that same new
76
+ signer. Any different signer restarts the wait, so publishing twice in a row gains nothing, and a
77
+ return to the accepted signer cancels it. Losing a signature entirely counts as a change too, so
78
+ stripping it does not get anything through faster. Going the other way, from unsigned to signed,
79
+ is only ever an improvement and applies right away.
80
+
81
+ Every one of those messages names the public key, in the same `type base64` form `signfiles`
82
+ prints, or `<no public key>` when there is none.
83
+
84
+ A signature that does not verify, or a manifest that does not match the files on disk, is never
85
+ treated as an identity - that content is ignored and the last accepted keys stay. Which of the
86
+ two it is gets reported:
87
+
88
+ - the signature is byte for byte the one we already accepted, so the repo changed and nobody
89
+ re-signed it. The changes are ignored until someone runs `signfiles` again.
90
+ - the signature did change and does not hold up, so it is corrupt.
91
+
92
+ Either way it is reported once, not every time it is polled.
93
+
94
+ ## keys
95
+
96
+ Derives a second ed25519 key from an existing one, by mixing a label into the source key's secret.
97
+ The same label and source always give the same key, so a derived key is something you can work out
98
+ again rather than something you have to keep a backup of.
99
+
100
+ yarn derivekey <label> <source-key> <derived-key>
101
+ yarn derivekey revokegithubkey ~/authorized_keys_access/id_ed25519 ~/authorized_keys_access/id_ed25519_revoke
102
+
103
+ It writes an ordinary OpenSSH key pair, so the public key can be handed to anything that takes one.
104
+ `deriveEd25519Key` in `deriveKey.ts` is the part to import elsewhere, and `sshKeyFile.ts` reads and
105
+ writes the OpenSSH private key container that node itself cannot.
106
+
107
+ ### Revoking keys
108
+
109
+ A key that is used from an address its `from=` restriction does not allow is revoked everywhere,
110
+ not just refused. The daemon reads sshd's log for exactly that refusal, takes the fingerprint sshd
111
+ names for the same connection, and writes a revocation to the source's revoke repo.
112
+
113
+ The revoke repo is derived from the source: `…/authorized_keys.git` gets `…/authorized_keys_revoked.git`,
114
+ reached with a key derived from the source's deploy key under the label `revokegithubkey`. Github
115
+ will not take one public key on two repos, which is why it is derived rather than reused, and it
116
+ means nothing extra has to be configured or uploaded - anything holding the source key can work it
117
+ out. `securessh add` checks that repo exists and is writable, and prints the deploy key to add if
118
+ it is not.
119
+
120
+ - One revocation per key, ever. The file is named after the fingerprint, and the key is checked
121
+ against local state before any network work, so a flood of unknown keys cannot become a flood of
122
+ commits.
123
+ - A revocation is sticky once seen. Deleting it from the repo does not bring the key back: the key
124
+ that writes revocations is on every server, so whoever stole one could otherwise erase the record
125
+ that locked them out. Recovering from that means making a new key, which you would want anyway.
126
+ - Each machine says so on Discord when a revoked key actually leaves its authorized_keys, once.
127
+
128
+ yarn unrevoke [keys-repo]
129
+
130
+ Run in the keys repo, or name one. It uses whatever git credentials the machine already has, since
131
+ the derived deploy key is for servers rather than for people. It reads the revoke repo and writes
132
+ one file under `unrevoked/` naming the revocations to undo, which then needs signing and pushing. Machines report when they see it, hold
133
+ it for an hour, then report again when it takes effect - so a signing key that was itself stolen
134
+ cannot instantly undo the revocation that shut it out.
135
+
136
+ Deleting a revoked key from the repo is usually the right answer instead. Both `signfiles` and
137
+ `securessh` refuse while a repo still holds a revoked key, and say which one it is.
138
+
139
+ ## helpers
140
+
141
+ Shared plumbing: running commands over ssh, spawning child processes, and expanding `~`.
@@ -0,0 +1,66 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+
5
+ // PORTED CODE: security/authorizedKeys/daemon/portsecureDaemon.js contains a plain JS port of normalizeKeys,
6
+ // summarizeKey and readRepoKeys, so it can resolve the same keys with no dependencies. The two
7
+ // must agree on which keys a repo produces - if you change one, make the matching change in the
8
+ // other.
9
+
10
+ export function normalizeKeys(contents: string) {
11
+ return contents.split("\n").map(line => line.trim()).filter(line => line && !line.startsWith("#"));
12
+ }
13
+
14
+ /** The fingerprint ssh itself reports for a key, which is what the sshd log names and therefore
15
+ what a revocation is keyed by. Returns "" for a line that holds no key. */
16
+ export function keyFingerprint(keyLine: string) {
17
+ let parts = keyLine.trim().split(/\s+/);
18
+ let typeIndex = parts.findIndex(part => /^(ssh-|ecdsa-|sk-)/.test(part));
19
+ let blob = typeIndex >= 0 && parts[typeIndex + 1] || "";
20
+ if (!blob) {
21
+ return "";
22
+ }
23
+ return "SHA256:" + crypto.createHash("sha256").update(Buffer.from(blob, "base64")).digest("base64").replace(/=+$/, "");
24
+ }
25
+
26
+ /** The addresses a key may be used from, which is the part of an authorized_keys line that
27
+ decides how much a stolen key is worth. A key with no restriction says so loudly. */
28
+ export function keyRestriction(keyLine: string) {
29
+ let match = keyLine.match(/from="([^"]*)"/);
30
+ if (!match) {
31
+ return "ANY ADDRESS (no from= restriction)";
32
+ }
33
+ return match[1];
34
+ }
35
+
36
+ /** Enough to recognise whose key this is without printing the whole blob. */
37
+ export function summarizeKey(keyLine: string) {
38
+ let parts = keyLine.trim().split(/\s+/);
39
+ let typeIndex = parts.findIndex(part => /^(ssh-|ecdsa-|sk-)/.test(part));
40
+ if (typeIndex < 0) {
41
+ return keyLine.slice(0, 60);
42
+ }
43
+ let type = parts[typeIndex];
44
+ let blob = parts[typeIndex + 1] || "";
45
+ let comment = parts.slice(typeIndex + 2).join(" ");
46
+ return `${type} ...${blob.slice(-12)}${comment && ` ${comment}` || ""}`;
47
+ }
48
+
49
+ /** Reads the authorized keys a repo checkout wants applied. Prefers a top level authorized_keys
50
+ file and otherwise concatenates every .pub at the top level. */
51
+ export async function readRepoKeys(repoPath: string) {
52
+ let combinedPath = path.join(repoPath, "authorized_keys");
53
+ let entries = await fs.readdir(repoPath);
54
+ if (entries.includes("authorized_keys")) {
55
+ return normalizeKeys(await fs.readFile(combinedPath, "utf8"));
56
+ }
57
+ let pubFiles = entries.filter(name => name.endsWith(".pub")).sort();
58
+ if (!pubFiles.length) {
59
+ throw new Error(`Expected authorized_keys or at least one .pub file in ${repoPath}, found neither`);
60
+ }
61
+ let keys: string[] = [];
62
+ for (let name of pubFiles) {
63
+ keys.push(...normalizeKeys(await fs.readFile(path.join(repoPath, name), "utf8")));
64
+ }
65
+ return keys;
66
+ }
@@ -0,0 +1,117 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs/promises";
3
+ import { spawnPromise } from "../../helpers/spawn";
4
+ import { AUTH_LOG_PATH } from "./paths";
5
+ import { getState, saveState } from "./state";
6
+ import { Attempt } from "./revocation";
7
+
8
+ // sshd says an attempt was refused in one line and names the key in another, both for the same
9
+ // connection, so they are tied together by the process id the log puts on every line.
10
+ const REFUSED = /Authentication tried for (\S+) with correct key but not from a permitted host \(host=([^,]*), ip=([^,]*), required=([^)]*)\)/;
11
+ const FAILED_KEY = /Failed publickey for (\S+) from (\S+) port (\d+) ssh2: \S+ (SHA256:[A-Za-z0-9+/=]+)/;
12
+ const PROCESS_ID = /(?:sshd|sshd-session)\[(\d+)\]/;
13
+ // Enough of the head of the file to notice it was rotated out from under us.
14
+ const SIGNATURE_LENGTH = 512;
15
+
16
+ export type RefusedAttempt = { fingerprint: string; attempt: Attempt };
17
+
18
+ /** Pairs each refusal with the fingerprint sshd logged for the same connection. A refusal we
19
+ cannot tie to a key is dropped: revoking the wrong key would lock out the wrong person. */
20
+ export function parseAuthLog(contents: string) {
21
+ let refusals = new Map<string, { user: string; ip: string; required: string; line: string }[]>();
22
+ let fingerprints = new Map<string, { fingerprint: string; port: string }>();
23
+ for (let line of contents.split("\n")) {
24
+ let processMatch = line.match(PROCESS_ID);
25
+ if (!processMatch) {
26
+ continue;
27
+ }
28
+ let processId = processMatch[1];
29
+ let refused = line.match(REFUSED);
30
+ if (refused) {
31
+ let existing = refusals.get(processId) || [];
32
+ existing.push({ user: refused[1], ip: refused[3], required: refused[4], line: line.trim() });
33
+ refusals.set(processId, existing);
34
+ continue;
35
+ }
36
+ let failed = line.match(FAILED_KEY);
37
+ if (failed) {
38
+ fingerprints.set(processId, { fingerprint: failed[4], port: failed[3] });
39
+ }
40
+ }
41
+
42
+ let attempts: RefusedAttempt[] = [];
43
+ for (let [processId, entries] of refusals) {
44
+ let key = fingerprints.get(processId);
45
+ if (!key) {
46
+ console.log(`A refused attempt named no key, so nothing is being revoked for it: ${entries[0].line}`);
47
+ continue;
48
+ }
49
+ for (let entry of entries) {
50
+ attempts.push({
51
+ fingerprint: key.fingerprint,
52
+ attempt: {
53
+ ip: entry.ip,
54
+ user: entry.user,
55
+ port: key.port,
56
+ required: entry.required,
57
+ line: entry.line,
58
+ },
59
+ });
60
+ }
61
+ }
62
+ return attempts;
63
+ }
64
+
65
+ /** Only what has been written since last time. A rotated file starts again from the beginning,
66
+ and a file that only grew is read from where we stopped. */
67
+ export async function readNewAuthLog() {
68
+ let state = getState();
69
+ let handle;
70
+ try {
71
+ handle = await fs.open(AUTH_LOG_PATH, "r");
72
+ } catch (e) {
73
+ // Some machines only keep this in the journal.
74
+ let result = await spawnPromise({
75
+ command: "journalctl",
76
+ args: ["-u", "ssh", "-u", "sshd", "--no-pager", "--since", "-10min"],
77
+ });
78
+ if (result.status !== 0) {
79
+ console.log(`No auth log to read: ${AUTH_LOG_PATH} is unreadable and journalctl exited ${result.status}`);
80
+ return "";
81
+ }
82
+ return result.stdout;
83
+ }
84
+ try {
85
+ let stats = await handle.stat();
86
+ let head = Buffer.alloc(Math.min(SIGNATURE_LENGTH, stats.size));
87
+ await handle.read(head, 0, head.length, 0);
88
+ let signature = crypto.createHash("sha256").update(head).digest("hex");
89
+
90
+ if (!state.authLogSignature) {
91
+ // First time we have ever looked. Start from the end: the log holds history from
92
+ // before this machine watched it, and revoking keys over refusals nobody was watching
93
+ // for could take away access that is still in use.
94
+ state.authLogOffset = stats.size;
95
+ state.authLogSignature = signature;
96
+ await saveState();
97
+ console.log(`Watching ${AUTH_LOG_PATH} from its current end, ${stats.size} bytes in`);
98
+ return "";
99
+ }
100
+ let offset = state.authLogOffset;
101
+ if (signature !== state.authLogSignature || stats.size < offset) {
102
+ // Rotated, or replaced. Everything in the new file is new.
103
+ offset = 0;
104
+ }
105
+ if (stats.size === offset) {
106
+ return "";
107
+ }
108
+ let contents = Buffer.alloc(stats.size - offset);
109
+ await handle.read(contents, 0, contents.length, offset);
110
+ state.authLogOffset = stats.size;
111
+ state.authLogSignature = signature;
112
+ await saveState();
113
+ return contents.toString("utf8");
114
+ } finally {
115
+ await handle.close();
116
+ }
117
+ }