@hraness/ghostget 0.18.15 → 0.18.16
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 +6 -6
- package/dist/apple-photos-client.js +1 -1
- package/dist/beeper-client.js +1 -1
- package/dist/{imessage-direct-install-5nftwc6p.js → imessage-direct-install-akfrgtpc.js} +2 -2
- package/dist/{index-kaav16fd.js → index-k2m73ke6.js} +3 -3
- package/dist/{index-4tsyncva.js → index-km75a0bm.js} +1 -1
- package/dist/{index-yq6maz71.js → index-wwtk6nnt.js} +8 -26
- package/dist/messaging-automation-api.js +2 -2
- package/dist/{messaging-automation-58s1366z.js → messaging-automation-w7n3j4sf.js} +1 -1
- package/dist/{messaging-native-install-c90nv501.js → messaging-native-install-wk10f605.js} +2 -2
- package/dist/{whatsapp-automation-runtime-ke1dx91p.js → whatsapp-automation-runtime-zp2fx46r.js} +1 -1
- package/package.json +14 -7
- package/skills/ghostget/references/install.md +5 -5
- package/src/accounts-auth.ts +188 -0
- package/src/args.ts +14 -0
- package/src/beeper-client-types.ts +1 -1
- package/src/derivation-network-guard.ts +37 -165
- package/src/derivation-network-proxy-helper.ts +27 -56
- package/src/derive.ts +36 -37
- package/src/ghostget.ts +43 -0
- package/src/storage.ts +8 -25
- package/src/usage.ts +2 -0
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://ghostget.com)
|
|
4
4
|
|
|
5
|
-
[](https://skills.sh/hraness/ghostget)
|
|
5
|
+
[](https://www.skills.sh/hraness/ghostget/ghostget)
|
|
6
6
|
|
|
7
7
|
**Give agents bounded access to pages, media, and connected accounts.**
|
|
8
8
|
|
|
@@ -79,7 +79,7 @@ The protocol and [Agent Skill](skills/ghostget/SKILL.md) describe the handoff.
|
|
|
79
79
|
|
|
80
80
|
## Built-in provider catalog
|
|
81
81
|
|
|
82
|
-
This v0.18.
|
|
82
|
+
This v0.18.16 source tree supports executable actions for 20 services: Beeper,
|
|
83
83
|
Bluesky, ClasificadosOnline, Facebook, Facebook Groups, Facebook Marketplace,
|
|
84
84
|
GitHub, Gmail, Hacker News, Instagram, iMessage, LinkedIn, Reddit, Substack,
|
|
85
85
|
Threads, TikTok, Twitch, WhatsApp, X, and YouTube.
|
|
@@ -158,9 +158,9 @@ The optional npm mirror can follow later without delaying canonical delivery.
|
|
|
158
158
|
Install the single Ghostget Agent Skill with either runner:
|
|
159
159
|
|
|
160
160
|
```sh
|
|
161
|
-
npx skills add hraness/ghostget#v0.18.
|
|
161
|
+
npx skills add hraness/ghostget#v0.18.16
|
|
162
162
|
# or
|
|
163
|
-
bunx skills add hraness/ghostget#v0.18.
|
|
163
|
+
bunx skills add hraness/ghostget#v0.18.16
|
|
164
164
|
```
|
|
165
165
|
|
|
166
166
|
The skill teaches Codex, Claude Code, Cursor, and other compatible coding
|
|
@@ -170,7 +170,7 @@ install the CLI if it is missing. Start a new agent session after installation.
|
|
|
170
170
|
After the matching immutable Release exists, install this exact canonical archive:
|
|
171
171
|
|
|
172
172
|
```sh
|
|
173
|
-
bun add --global https://github.com/hraness/ghostget/releases/download/v0.18.
|
|
173
|
+
bun add --global https://github.com/hraness/ghostget/releases/download/v0.18.16/hraness-ghostget-0.18.16.tgz
|
|
174
174
|
ghostget adapter sync-bundled --json
|
|
175
175
|
ghostget doctor
|
|
176
176
|
```
|
|
@@ -198,7 +198,7 @@ For that same released coordinate, install Ghostget in an agent or application
|
|
|
198
198
|
that owns its own model, planning, tool loop, approvals, and interface:
|
|
199
199
|
|
|
200
200
|
```sh
|
|
201
|
-
bun add https://github.com/hraness/ghostget/releases/download/v0.18.
|
|
201
|
+
bun add https://github.com/hraness/ghostget/releases/download/v0.18.16/hraness-ghostget-0.18.16.tgz
|
|
202
202
|
```
|
|
203
203
|
|
|
204
204
|
```ts
|
package/dist/beeper-client.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import {
|
|
3
3
|
ensureImsgNativeResources,
|
|
4
4
|
verifyImsgNativeResources
|
|
5
|
-
} from "./index-
|
|
5
|
+
} from "./index-k2m73ke6.js";
|
|
6
6
|
import {
|
|
7
7
|
ensurePrivateStateDirectory,
|
|
8
8
|
ghostgetStateHome
|
|
9
|
-
} from "./index-
|
|
9
|
+
} from "./index-wwtk6nnt.js";
|
|
10
10
|
import"./index-gwk7rbyj.js";
|
|
11
11
|
import"./index-z1w83f81.js";
|
|
12
12
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
ensurePrivateStateDirectory
|
|
4
|
-
} from "./index-
|
|
4
|
+
} from "./index-wwtk6nnt.js";
|
|
5
5
|
import {
|
|
6
6
|
__require
|
|
7
7
|
} from "./index-z1w83f81.js";
|
|
@@ -162,11 +162,11 @@ async function installBundledMessagingRuntime(provider, environment = process.en
|
|
|
162
162
|
await file.close();
|
|
163
163
|
}
|
|
164
164
|
if (provider === "imessage") {
|
|
165
|
-
const { installReviewedImsgBinary } = await import("./imessage-direct-install-
|
|
165
|
+
const { installReviewedImsgBinary } = await import("./imessage-direct-install-akfrgtpc.js");
|
|
166
166
|
const result = await installReviewedImsgBinary(path, environment);
|
|
167
167
|
return { version: result.version, sha256: result.executableSha256, alreadyPresent: result.alreadyPresent };
|
|
168
168
|
}
|
|
169
|
-
const { installReviewedWhatsAppAutomationBinary } = await import("./whatsapp-automation-runtime-
|
|
169
|
+
const { installReviewedWhatsAppAutomationBinary } = await import("./whatsapp-automation-runtime-zp2fx46r.js");
|
|
170
170
|
return await installReviewedWhatsAppAutomationBinary(path, environment);
|
|
171
171
|
} finally {
|
|
172
172
|
await unlink(path);
|
|
@@ -12,12 +12,12 @@ import {
|
|
|
12
12
|
mkdtempSync,
|
|
13
13
|
openSync,
|
|
14
14
|
opendirSync,
|
|
15
|
-
readSync,
|
|
16
15
|
realpathSync,
|
|
17
16
|
rmSync,
|
|
18
17
|
writeFileSync
|
|
19
18
|
} from "fs";
|
|
20
19
|
import { spawnSync } from "child_process";
|
|
20
|
+
import { readOwnedFileStableSync } from "@hraness/local-custody/private-paths";
|
|
21
21
|
import { homedir, tmpdir } from "os";
|
|
22
22
|
import { basename, dirname, isAbsolute, join, parse, relative, resolve, sep } from "path";
|
|
23
23
|
import { fileURLToPath } from "url";
|
|
@@ -368,21 +368,6 @@ function isWithinPath(root, candidate) {
|
|
|
368
368
|
function hasCode(error, code) {
|
|
369
369
|
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
370
370
|
}
|
|
371
|
-
function readDescriptorBounded(descriptor, maximumBytes) {
|
|
372
|
-
const chunks = [];
|
|
373
|
-
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maximumBytes + 1));
|
|
374
|
-
let total = 0;
|
|
375
|
-
for (;; ) {
|
|
376
|
-
const remaining = maximumBytes + 1 - total;
|
|
377
|
-
const count = readSync(descriptor, buffer, 0, Math.min(buffer.byteLength, remaining), null);
|
|
378
|
-
if (count === 0)
|
|
379
|
-
return Buffer.concat(chunks, total);
|
|
380
|
-
total += count;
|
|
381
|
-
if (total > maximumBytes)
|
|
382
|
-
throw new Error("file grew beyond its byte bound");
|
|
383
|
-
chunks.push(Buffer.from(buffer.subarray(0, count)));
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
371
|
function pathInside(root, target) {
|
|
387
372
|
const child = relative(root, target);
|
|
388
373
|
return child === "" || !isAbsolute(child) && child !== ".." && !child.startsWith(`..${sep}`);
|
|
@@ -825,25 +810,22 @@ function ownedByCurrentUser(stats) {
|
|
|
825
810
|
return currentUid === undefined || stats.uid === (typeof stats.uid === "bigint" ? BigInt(currentUid) : currentUid);
|
|
826
811
|
}
|
|
827
812
|
function readStateMarker(path) {
|
|
828
|
-
|
|
829
|
-
const nonBlocking = "O_NONBLOCK" in constants ? constants.O_NONBLOCK : 0;
|
|
830
|
-
const descriptor = openSync(path, constants.O_RDONLY | noFollow | nonBlocking);
|
|
813
|
+
let bytes;
|
|
831
814
|
try {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
815
|
+
bytes = readOwnedFileStableSync(path, 256).bytes;
|
|
816
|
+
} catch (error) {
|
|
817
|
+
throw new Error("ghostget state marker must be a private, owned regular file", { cause: error });
|
|
818
|
+
}
|
|
819
|
+
{
|
|
836
820
|
const content = new TextDecoder("utf-8", {
|
|
837
821
|
fatal: true,
|
|
838
822
|
ignoreBOM: true
|
|
839
|
-
}).decode(
|
|
823
|
+
}).decode(bytes);
|
|
840
824
|
if (content !== stateMarkerText)
|
|
841
825
|
throw new Error("ghostget state marker is malformed");
|
|
842
826
|
const value = JSON.parse(content);
|
|
843
827
|
if (typeof value !== "object" || value === null || Array.isArray(value) || Object.keys(value).sort().join(",") !== "kind,schemaVersion" || !("schemaVersion" in value) || value.schemaVersion !== 1 || !("kind" in value) || value.kind !== "io-state")
|
|
844
828
|
throw new Error("ghostget state marker is malformed");
|
|
845
|
-
} finally {
|
|
846
|
-
closeSync(descriptor);
|
|
847
829
|
}
|
|
848
830
|
}
|
|
849
831
|
function hasGhostgetPathIdentity(path) {
|
|
@@ -8,11 +8,11 @@ import {
|
|
|
8
8
|
|
|
9
9
|
// src/messaging-automation-api.ts
|
|
10
10
|
async function createMessagingAutomationHost(providers, environment) {
|
|
11
|
-
const { MessagingAutomationHost } = await import("./messaging-automation-
|
|
11
|
+
const { MessagingAutomationHost } = await import("./messaging-automation-w7n3j4sf.js");
|
|
12
12
|
return new MessagingAutomationHost(providers, environment);
|
|
13
13
|
}
|
|
14
14
|
async function installBundledMessagingRuntime(provider, environment) {
|
|
15
|
-
return (await import("./messaging-native-install-
|
|
15
|
+
return (await import("./messaging-native-install-wk10f605.js")).installBundledMessagingRuntime(provider, environment);
|
|
16
16
|
}
|
|
17
17
|
export {
|
|
18
18
|
installBundledMessagingRuntime,
|
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
installBundledMessagingRuntime,
|
|
5
5
|
readBundledMessagingAsset,
|
|
6
6
|
verifyImsgNativeResources
|
|
7
|
-
} from "./index-
|
|
8
|
-
import"./index-
|
|
7
|
+
} from "./index-k2m73ke6.js";
|
|
8
|
+
import"./index-wwtk6nnt.js";
|
|
9
9
|
import"./index-gwk7rbyj.js";
|
|
10
10
|
import"./index-z1w83f81.js";
|
|
11
11
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hraness/ghostget",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.16",
|
|
4
4
|
"description": "Open-source CLI and TypeScript SDK for precise web capabilities for AI agents: page capture, verified media archives, encrypted reads, and typed provider operations.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
"bunfig.toml",
|
|
92
92
|
"docs/imessage-direct-provider.md",
|
|
93
93
|
"docs/rental-listings.md",
|
|
94
|
+
"src/accounts-auth.ts",
|
|
94
95
|
"src/args.ts",
|
|
95
96
|
"src/apple-photos-cli.ts",
|
|
96
97
|
"src/apple-photos-client-types.ts",
|
|
@@ -621,10 +622,13 @@
|
|
|
621
622
|
"kb:check": "bunx --bun github:hraness/kb#v0.15.2 check --root kb && bunx --bun github:hraness/kb#v0.15.2 agents check --root kb --repo .",
|
|
622
623
|
"kb:catalog": "bunx --bun github:hraness/kb#v0.15.2 catalog --root kb",
|
|
623
624
|
"prepack": "bun run check",
|
|
624
|
-
"check:effect": "bun scripts/check-effect-architecture.ts"
|
|
625
|
+
"check:effect": "bun scripts/check-effect-architecture.ts",
|
|
626
|
+
"generate:og": "bun scripts/generate-og.tsx"
|
|
625
627
|
},
|
|
626
628
|
"dependencies": {
|
|
629
|
+
"@hraness/accounts-cli": "github:hraness/accounts-cli#v0.1.3",
|
|
627
630
|
"@hraness/kb": "https://github.com/hraness/kb/releases/download/v0.19.6/hraness-kb-0.19.6.tgz",
|
|
631
|
+
"@hraness/local-custody": "github:hraness/local-custody#d462900810b042a8b5f34245d0d371cfaa6ee9b3",
|
|
628
632
|
"@hraness/message-like-me": "github:hraness/textbutler#83453cc7c17b49bb53fdfd89ccb69b8b44b30af1",
|
|
629
633
|
"@hraness/support-foundation": "github:hraness/support-foundation#2d034b357680353574411217d68b02b6755b07ed",
|
|
630
634
|
"buffer-from": "1.1.2",
|
|
@@ -636,19 +640,22 @@
|
|
|
636
640
|
"@hraness/desktop-foundation": "https://github.com/hraness/desktop-foundation/releases/download/v0.7.0/hraness-desktop-foundation-0.7.0.tgz"
|
|
637
641
|
},
|
|
638
642
|
"devDependencies": {
|
|
639
|
-
"@hraness/design-kit": "github:hraness/design-kit#v0.
|
|
640
|
-
"@hraness/
|
|
643
|
+
"@hraness/design-kit": "github:hraness/design-kit#v0.10.0",
|
|
644
|
+
"@hraness/direct": "https://github.com/hraness/direct/releases/download/v0.7.21/hraness-direct-0.7.21.tgz",
|
|
645
|
+
"@hraness/site-footer": "github:hraness/site-footer#v0.15.0",
|
|
641
646
|
"@hraness/ui": "github:hraness/ui#v0.5.13",
|
|
647
|
+
"@hraness/web-discovery": "github:hraness/web-discovery#v0.6.0",
|
|
648
|
+
"@resvg/resvg-js": "2.6.2",
|
|
642
649
|
"@steipete/sweet-cookie": "0.4.3",
|
|
650
|
+
"@tanstack/react-virtual": "3.14.11",
|
|
643
651
|
"@types/bun": "^1.3.14",
|
|
644
652
|
"@types/react": "19.2.14",
|
|
645
653
|
"@types/react-dom": "19.2.3",
|
|
654
|
+
"agent-browser": "0.32.3",
|
|
646
655
|
"fast-check": "^4.8.0",
|
|
647
656
|
"react": "19.2.3",
|
|
648
657
|
"react-dom": "19.2.3",
|
|
649
|
-
"
|
|
650
|
-
"@hraness/direct": "https://github.com/hraness/direct/releases/download/v0.7.21/hraness-direct-0.7.21.tgz",
|
|
651
|
-
"agent-browser": "0.32.3"
|
|
658
|
+
"satori": "0.33.4"
|
|
652
659
|
},
|
|
653
660
|
"publishConfig": {
|
|
654
661
|
"access": "public",
|
|
@@ -17,34 +17,34 @@ If Bun is missing, stop and direct the user to the official
|
|
|
17
17
|
[Bun installation guide](https://bun.sh/docs/installation). Do not switch
|
|
18
18
|
package managers or pipe an unreviewed installer into a shell.
|
|
19
19
|
|
|
20
|
-
This reference is authored for the exact v0.18.
|
|
20
|
+
This reference is authored for the exact v0.18.16 release coordinate. Use it
|
|
21
21
|
only from the matching release-bound Agent Skill after its canonical archive and
|
|
22
22
|
immutable GitHub Release exist. If the coordinate is not public, stop instead
|
|
23
23
|
of substituting `main`, another tag, or a different package version. Install
|
|
24
24
|
that exact release and its reviewed bundled adapter manifests:
|
|
25
25
|
|
|
26
26
|
```sh
|
|
27
|
-
bun add --global https://github.com/hraness/ghostget/releases/download/v0.18.
|
|
27
|
+
bun add --global https://github.com/hraness/ghostget/releases/download/v0.18.16/hraness-ghostget-0.18.16.tgz
|
|
28
28
|
ghostget adapter sync-bundled --json
|
|
29
29
|
ghostget --help
|
|
30
30
|
ghostget doctor --json
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
The package is `@hraness/ghostget`; `@hraness/ghostget@0.18.
|
|
33
|
+
The package is `@hraness/ghostget`; `@hraness/ghostget@0.18.16` is an optional npm
|
|
34
34
|
mirror only after verified registry publication. Canonical installation does not
|
|
35
35
|
wait for registry publication.
|
|
36
36
|
|
|
37
37
|
When upgrading from Wrench, use `ghostget` for new commands and
|
|
38
38
|
`GHOSTGET_STATE_HOME` for an explicit state root. Existing state is selected in
|
|
39
39
|
place; do not rename, copy, or delete a state directory as part of the upgrade.
|
|
40
|
-
The [migration guide](https://github.com/hraness/ghostget/blob/v0.18.
|
|
40
|
+
The [migration guide](https://github.com/hraness/ghostget/blob/v0.18.16/docs/ghostget-migration.md)
|
|
41
41
|
explains the retained state aliases and durable protocol names.
|
|
42
42
|
|
|
43
43
|
Do not clone the repository merely to run the CLI. Importing the SDK is a
|
|
44
44
|
separate project dependency and does not install a global command:
|
|
45
45
|
|
|
46
46
|
```sh
|
|
47
|
-
bun add https://github.com/hraness/ghostget/releases/download/v0.18.
|
|
47
|
+
bun add https://github.com/hraness/ghostget/releases/download/v0.18.16/hraness-ghostget-0.18.16.tgz
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
`ghostget adapter sync-bundled` upgrades exact bundled baselines, including an
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createCliSession,
|
|
5
|
+
createEncryptedFileTokenStorage,
|
|
6
|
+
createKeychainTokenStorage,
|
|
7
|
+
createMemoryTokenStorage,
|
|
8
|
+
initiateDeviceLogin,
|
|
9
|
+
type CliSession,
|
|
10
|
+
type DeviceLoginResult,
|
|
11
|
+
type TokenStorage,
|
|
12
|
+
} from "@hraness/accounts-cli";
|
|
13
|
+
import { ghostgetStateHome } from "./storage";
|
|
14
|
+
|
|
15
|
+
const GHOSTGET_CLIENT_ID = "hraness:ghostget:production:v1";
|
|
16
|
+
const ACCOUNTS_ORIGIN = "https://account.hraness.com";
|
|
17
|
+
const DEVICE_AUTHORIZATION_ENDPOINT =
|
|
18
|
+
`${ACCOUNTS_ORIGIN}/api/auth/oauth2/device_authorization`;
|
|
19
|
+
const DEVICE_TOKEN_ENDPOINT =
|
|
20
|
+
`${ACCOUNTS_ORIGIN}/api/auth/oauth2/device/token`;
|
|
21
|
+
const TOKEN_ENDPOINT = `${ACCOUNTS_ORIGIN}/api/auth/oauth2/token`;
|
|
22
|
+
|
|
23
|
+
export type AccountsAuthResult =
|
|
24
|
+
| Readonly<{ kind: "success"; message: string }>
|
|
25
|
+
| Readonly<{ kind: "denied" }>
|
|
26
|
+
| Readonly<{ kind: "expired" }>
|
|
27
|
+
| Readonly<{ kind: "error"; message: string }>;
|
|
28
|
+
|
|
29
|
+
export type AccountsAuthHandlers = Readonly<{
|
|
30
|
+
onUserCode?: (userCode: string, verificationUri: string) => void;
|
|
31
|
+
onPending?: () => void;
|
|
32
|
+
onSlowDown?: (intervalMs: number) => void;
|
|
33
|
+
}>;
|
|
34
|
+
|
|
35
|
+
export function createGhostgetTokenStorage(
|
|
36
|
+
environment: Readonly<Record<string, string | undefined>>,
|
|
37
|
+
storage?: TokenStorage,
|
|
38
|
+
): TokenStorage {
|
|
39
|
+
if (storage !== undefined) return storage;
|
|
40
|
+
const keychain = createKeychainTokenStorage(
|
|
41
|
+
"ghostget",
|
|
42
|
+
"hraness-accounts",
|
|
43
|
+
);
|
|
44
|
+
const stateHome = ghostgetStateHome(environment);
|
|
45
|
+
const file = createEncryptedFileTokenStorage(
|
|
46
|
+
join(stateHome, "accounts", "refresh-token.enc"),
|
|
47
|
+
);
|
|
48
|
+
return {
|
|
49
|
+
backend: "keychain",
|
|
50
|
+
deleteRefreshToken: async () => {
|
|
51
|
+
await keychain.deleteRefreshToken();
|
|
52
|
+
await file.deleteRefreshToken();
|
|
53
|
+
},
|
|
54
|
+
loadRefreshToken: async () => {
|
|
55
|
+
const keychainToken = await keychain.loadRefreshToken();
|
|
56
|
+
if (keychainToken !== null) return keychainToken;
|
|
57
|
+
return file.loadRefreshToken();
|
|
58
|
+
},
|
|
59
|
+
saveRefreshToken: async (token) => {
|
|
60
|
+
try {
|
|
61
|
+
await keychain.saveRefreshToken(token);
|
|
62
|
+
} catch {
|
|
63
|
+
// Keychain may be unavailable in headless/CI; the encrypted file
|
|
64
|
+
// fallback still gives a machine-bound local secret.
|
|
65
|
+
}
|
|
66
|
+
await file.saveRefreshToken(token);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function runAccountsDeviceLogin(
|
|
72
|
+
environment: Readonly<Record<string, string | undefined>>,
|
|
73
|
+
handlers: AccountsAuthHandlers = {},
|
|
74
|
+
storage?: TokenStorage,
|
|
75
|
+
signal?: AbortSignal,
|
|
76
|
+
): Promise<AccountsAuthResult> {
|
|
77
|
+
if (signal?.aborted) {
|
|
78
|
+
return { kind: "error", message: "Login was cancelled." };
|
|
79
|
+
}
|
|
80
|
+
const tokenStorage = createGhostgetTokenStorage(environment, storage);
|
|
81
|
+
const session = createCliSession({
|
|
82
|
+
clientId: GHOSTGET_CLIENT_ID,
|
|
83
|
+
storage: tokenStorage,
|
|
84
|
+
tokenEndpoint: TOKEN_ENDPOINT,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const deviceHandlers: {
|
|
88
|
+
onUserCode?: (userCode: string, verificationUri: string) => void;
|
|
89
|
+
onPending?: () => void;
|
|
90
|
+
onSlowDown?: (intervalMs: number) => void;
|
|
91
|
+
} = {};
|
|
92
|
+
if (handlers.onUserCode !== undefined) {
|
|
93
|
+
deviceHandlers.onUserCode = handlers.onUserCode;
|
|
94
|
+
}
|
|
95
|
+
if (handlers.onPending !== undefined) {
|
|
96
|
+
deviceHandlers.onPending = handlers.onPending;
|
|
97
|
+
}
|
|
98
|
+
if (handlers.onSlowDown !== undefined) {
|
|
99
|
+
deviceHandlers.onSlowDown = handlers.onSlowDown;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const device = await initiateDeviceLogin(
|
|
103
|
+
{
|
|
104
|
+
deviceAuthorizationEndpoint: DEVICE_AUTHORIZATION_ENDPOINT,
|
|
105
|
+
deviceTokenEndpoint: DEVICE_TOKEN_ENDPOINT,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
clientId: GHOSTGET_CLIENT_ID,
|
|
109
|
+
scopes: ["openid", "profile", "email", "offline_access"],
|
|
110
|
+
},
|
|
111
|
+
deviceHandlers,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const abortPromise = new Promise<never>((_resolve, reject) => {
|
|
115
|
+
if (signal === undefined) return;
|
|
116
|
+
if (signal.aborted) {
|
|
117
|
+
reject(new Error("Login was cancelled."));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
signal.addEventListener("abort", () => {
|
|
121
|
+
reject(new Error("Login was cancelled."));
|
|
122
|
+
}, { once: true });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const result = (await Promise.race([device.poll(), abortPromise])) as DeviceLoginResult;
|
|
127
|
+
return handlePollResult(result, session);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return {
|
|
130
|
+
kind: "error",
|
|
131
|
+
message: error instanceof Error ? error.message : String(error),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function handlePollResult(
|
|
137
|
+
result: DeviceLoginResult,
|
|
138
|
+
session: CliSession,
|
|
139
|
+
): AccountsAuthResult {
|
|
140
|
+
switch (result.kind) {
|
|
141
|
+
case "token":
|
|
142
|
+
void session.saveRefreshToken(result.refreshToken);
|
|
143
|
+
return {
|
|
144
|
+
kind: "success",
|
|
145
|
+
message: "Signed in to Hraness Accounts. You can return to your terminal.",
|
|
146
|
+
};
|
|
147
|
+
case "access_denied":
|
|
148
|
+
return { kind: "denied" };
|
|
149
|
+
case "expired_token":
|
|
150
|
+
return { kind: "expired" };
|
|
151
|
+
case "error":
|
|
152
|
+
return {
|
|
153
|
+
kind: "error",
|
|
154
|
+
message: result.errorDescription ?? result.error,
|
|
155
|
+
};
|
|
156
|
+
default: {
|
|
157
|
+
const _exhaustive: never = result;
|
|
158
|
+
return { kind: "error", message: `unknown login result: ${_exhaustive}` };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function loadAccountsSession(
|
|
164
|
+
environment: Readonly<Record<string, string | undefined>>,
|
|
165
|
+
storage?: TokenStorage,
|
|
166
|
+
): CliSession {
|
|
167
|
+
const tokenStorage = createGhostgetTokenStorage(environment, storage);
|
|
168
|
+
return createCliSession({
|
|
169
|
+
clientId: GHOSTGET_CLIENT_ID,
|
|
170
|
+
storage: tokenStorage,
|
|
171
|
+
tokenEndpoint: TOKEN_ENDPOINT,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function runAccountsSignOut(
|
|
176
|
+
environment: Readonly<Record<string, string | undefined>>,
|
|
177
|
+
storage?: TokenStorage,
|
|
178
|
+
): Promise<void> {
|
|
179
|
+
await loadAccountsSession(environment, storage).signOut();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export const accountsAuthInternals = {
|
|
183
|
+
DEVICE_AUTHORIZATION_ENDPOINT,
|
|
184
|
+
DEVICE_TOKEN_ENDPOINT,
|
|
185
|
+
TOKEN_ENDPOINT,
|
|
186
|
+
GHOSTGET_CLIENT_ID,
|
|
187
|
+
createMemoryTokenStorage,
|
|
188
|
+
};
|
package/src/args.ts
CHANGED
|
@@ -36,6 +36,8 @@ type MessagingArguments = {
|
|
|
36
36
|
|
|
37
37
|
export type GhostgetArguments =
|
|
38
38
|
| { readonly command: "help" }
|
|
39
|
+
| { readonly command: "login"; readonly json: boolean }
|
|
40
|
+
| { readonly command: "logout"; readonly json: boolean }
|
|
39
41
|
| { readonly command: "messaging-automation-serve" }
|
|
40
42
|
| { readonly command: "whatsapp-automation-install"; readonly binary?: string; readonly json: boolean }
|
|
41
43
|
| { readonly command: "clip"; readonly arguments: readonly string[] }
|
|
@@ -777,6 +779,18 @@ export function parseGhostgetArguments(raw: readonly string[]): ParseGhostgetRes
|
|
|
777
779
|
if (raw.length > 1) return { ok: false, message: "help accepts no arguments" };
|
|
778
780
|
return { ok: true, value: { command: "help" } };
|
|
779
781
|
}
|
|
782
|
+
if (raw[0] === "login") {
|
|
783
|
+
const json = simpleJsonOptions(raw.slice(1), "login");
|
|
784
|
+
return typeof json === "boolean"
|
|
785
|
+
? { ok: true, value: { command: "login", json } }
|
|
786
|
+
: json;
|
|
787
|
+
}
|
|
788
|
+
if (raw[0] === "logout") {
|
|
789
|
+
const json = simpleJsonOptions(raw.slice(1), "logout");
|
|
790
|
+
return typeof json === "boolean"
|
|
791
|
+
? { ok: true, value: { command: "logout", json } }
|
|
792
|
+
: json;
|
|
793
|
+
}
|
|
780
794
|
const first = raw[0] ?? "";
|
|
781
795
|
if (/^https?:\/\//iu.test(first)) return { ok: true, value: { command: "clip", arguments: raw } };
|
|
782
796
|
if (first === "clip" || first === "capture") return { ok: true, value: { command: "clip", arguments: raw.slice(1) } };
|
|
@@ -96,7 +96,7 @@ export type BeeperContactInteractionExportReceipt = Readonly<{
|
|
|
96
96
|
implementation: Readonly<{
|
|
97
97
|
producer: Readonly<{
|
|
98
98
|
package: "@hraness/ghostget";
|
|
99
|
-
version: "0.18.
|
|
99
|
+
version: "0.18.16";
|
|
100
100
|
}>;
|
|
101
101
|
officialCli: Readonly<{
|
|
102
102
|
implementation: "github.com/beeper/cli";
|
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
chmodSync,
|
|
4
|
-
closeSync,
|
|
5
|
-
constants,
|
|
6
|
-
fchmodSync,
|
|
7
|
-
fstatSync,
|
|
8
|
-
fsyncSync,
|
|
9
|
-
lstatSync,
|
|
10
|
-
mkdirSync,
|
|
11
|
-
openSync,
|
|
12
|
-
readSync,
|
|
13
|
-
unlinkSync,
|
|
14
|
-
writeSync,
|
|
15
|
-
} from "node:fs";
|
|
2
|
+
import { chmodSync, mkdirSync } from "node:fs";
|
|
16
3
|
import { tmpdir } from "node:os";
|
|
17
|
-
import { join } from "node:path";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
assertOwnedPathSync,
|
|
8
|
+
readOwnedFileStableSync,
|
|
9
|
+
} from "@hraness/local-custody/private-paths";
|
|
10
|
+
import { createPrivateFileOnceSync } from "@hraness/local-custody/atomic-publish";
|
|
18
11
|
|
|
19
12
|
import { canonicalJson, sha256 } from "./canonical-json";
|
|
20
13
|
import type { ProcessOwnerIdentity } from "./process-identity";
|
|
@@ -141,11 +134,6 @@ type GuardRuntimeCase = {
|
|
|
141
134
|
readonly ruleId: number;
|
|
142
135
|
};
|
|
143
136
|
|
|
144
|
-
function currentUserOwns(uid: number | bigint): boolean {
|
|
145
|
-
const current = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
146
|
-
return current === undefined || uid === (typeof uid === "bigint" ? BigInt(current) : current);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
137
|
function escapeRegex(value: string): string {
|
|
150
138
|
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
151
139
|
}
|
|
@@ -307,14 +295,11 @@ function extensionIdFromKey(): string {
|
|
|
307
295
|
|
|
308
296
|
function inspectDirectory(path: string): GuardDirectoryIdentity {
|
|
309
297
|
try {
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|| (stats.mode & 0o777n) !== 0o700n
|
|
316
|
-
) throw new Error("unsafe");
|
|
317
|
-
return { device: stats.dev.toString(), inode: stats.ino.toString() };
|
|
298
|
+
const identity = assertOwnedPathSync(path, {
|
|
299
|
+
kind: "directory",
|
|
300
|
+
exactMode: 0o700,
|
|
301
|
+
});
|
|
302
|
+
return { device: identity.dev.toString(), inode: identity.ino.toString() };
|
|
318
303
|
} catch {
|
|
319
304
|
throw new Error("derivation network guard directory is unavailable or unsafe");
|
|
320
305
|
}
|
|
@@ -325,71 +310,18 @@ function sameIdentity(left: GuardDirectoryIdentity, right: GuardDirectoryIdentit
|
|
|
325
310
|
}
|
|
326
311
|
|
|
327
312
|
export function writeGuardPrivateFile(path: string, content: string): GuardPrivateFileEvidence {
|
|
328
|
-
let descriptor: number | null = null;
|
|
329
|
-
let openedIdentity: GuardDirectoryIdentity | null = null;
|
|
330
|
-
let completed = false;
|
|
331
313
|
try {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
);
|
|
340
|
-
const opened = fstatSync(descriptor, { bigint: true });
|
|
341
|
-
openedIdentity = { device: opened.dev.toString(), inode: opened.ino.toString() };
|
|
342
|
-
fchmodSync(descriptor, 0o600);
|
|
343
|
-
const bytes = Buffer.from(content, "utf8");
|
|
344
|
-
let offset = 0;
|
|
345
|
-
while (offset < bytes.byteLength) {
|
|
346
|
-
offset += writeSync(descriptor, bytes, offset, bytes.byteLength - offset);
|
|
347
|
-
}
|
|
348
|
-
fsyncSync(descriptor);
|
|
349
|
-
const stats = fstatSync(descriptor, { bigint: true });
|
|
350
|
-
if (
|
|
351
|
-
!stats.isFile()
|
|
352
|
-
|| !currentUserOwns(stats.uid)
|
|
353
|
-
|| (stats.mode & 0o777n) !== 0o600n
|
|
354
|
-
|| stats.size !== BigInt(bytes.byteLength)
|
|
355
|
-
) throw new Error("derivation network guard file could not be secured");
|
|
356
|
-
const evidence = {
|
|
357
|
-
device: stats.dev.toString(),
|
|
358
|
-
inode: stats.ino.toString(),
|
|
359
|
-
byteLength: bytes.byteLength,
|
|
314
|
+
const outcome = createPrivateFileOnceSync(dirname(path), basename(path), content);
|
|
315
|
+
if (outcome !== "created") throw new Error("exists");
|
|
316
|
+
const identity = assertOwnedPathSync(path, { kind: "file", exactMode: 0o600 });
|
|
317
|
+
return {
|
|
318
|
+
device: identity.dev.toString(),
|
|
319
|
+
inode: identity.ino.toString(),
|
|
320
|
+
byteLength: Buffer.byteLength(content, "utf8"),
|
|
360
321
|
sha256: sha256(content),
|
|
361
322
|
};
|
|
362
|
-
completed = true;
|
|
363
|
-
return evidence;
|
|
364
323
|
} catch {
|
|
365
324
|
throw new Error("derivation network guard file could not be secured");
|
|
366
|
-
} finally {
|
|
367
|
-
if (descriptor !== null) {
|
|
368
|
-
if (!completed && openedIdentity === null) {
|
|
369
|
-
try {
|
|
370
|
-
const stats = fstatSync(descriptor, { bigint: true });
|
|
371
|
-
openedIdentity = { device: stats.dev.toString(), inode: stats.ino.toString() };
|
|
372
|
-
} catch {
|
|
373
|
-
// Without an exact identity, preserve the path instead of unlinking.
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
try {
|
|
377
|
-
closeSync(descriptor);
|
|
378
|
-
} catch {
|
|
379
|
-
// The categorical write failure below must not expose a local path.
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
if (!completed && openedIdentity !== null) {
|
|
383
|
-
try {
|
|
384
|
-
const current = lstatSync(path, { bigint: true });
|
|
385
|
-
if (
|
|
386
|
-
current.dev.toString() === openedIdentity.device
|
|
387
|
-
&& current.ino.toString() === openedIdentity.inode
|
|
388
|
-
) unlinkSync(path);
|
|
389
|
-
} catch {
|
|
390
|
-
// A changed path is intentionally preserved.
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
325
|
}
|
|
394
326
|
}
|
|
395
327
|
|
|
@@ -397,55 +329,23 @@ export function readGuardPrivateFile(
|
|
|
397
329
|
path: string,
|
|
398
330
|
maximumBytes = 4 * 1024 * 1024,
|
|
399
331
|
): { readonly content: string; readonly evidence: GuardPrivateFileEvidence } {
|
|
400
|
-
let descriptor: number | null = null;
|
|
401
332
|
try {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
);
|
|
406
|
-
const
|
|
407
|
-
if (
|
|
408
|
-
!before.isFile()
|
|
409
|
-
|| !currentUserOwns(before.uid)
|
|
410
|
-
|| (before.mode & 0o777n) !== 0o600n
|
|
411
|
-
|| before.size < 1n
|
|
412
|
-
|| before.size > BigInt(maximumBytes)
|
|
413
|
-
) throw new Error("derivation network guard file is unavailable or unsafe");
|
|
414
|
-
const bytes = Buffer.alloc(Number(before.size));
|
|
415
|
-
let offset = 0;
|
|
416
|
-
while (offset < bytes.byteLength) {
|
|
417
|
-
const count = readSync(descriptor, bytes, offset, bytes.byteLength - offset, null);
|
|
418
|
-
if (count === 0) throw new Error("derivation network guard file changed size");
|
|
419
|
-
offset += count;
|
|
420
|
-
}
|
|
421
|
-
const after = fstatSync(descriptor, { bigint: true });
|
|
422
|
-
if (
|
|
423
|
-
before.dev !== after.dev
|
|
424
|
-
|| before.ino !== after.ino
|
|
425
|
-
|| before.size !== after.size
|
|
426
|
-
|| before.mtimeNs !== after.mtimeNs
|
|
427
|
-
|| before.ctimeNs !== after.ctimeNs
|
|
428
|
-
) throw new Error("derivation network guard file changed while reading");
|
|
429
|
-
const content = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
333
|
+
const read = readOwnedFileStableSync(path, maximumBytes, {
|
|
334
|
+
exactMode: 0o600,
|
|
335
|
+
minimumBytes: 1n,
|
|
336
|
+
});
|
|
337
|
+
const content = new TextDecoder("utf-8", { fatal: true }).decode(read.bytes);
|
|
430
338
|
return {
|
|
431
339
|
content,
|
|
432
340
|
evidence: {
|
|
433
|
-
device:
|
|
434
|
-
inode:
|
|
435
|
-
byteLength: bytes.byteLength,
|
|
341
|
+
device: read.dev.toString(),
|
|
342
|
+
inode: read.ino.toString(),
|
|
343
|
+
byteLength: read.bytes.byteLength,
|
|
436
344
|
sha256: sha256(content),
|
|
437
345
|
},
|
|
438
346
|
};
|
|
439
347
|
} catch {
|
|
440
348
|
throw new Error("derivation network guard file is unavailable or unsafe");
|
|
441
|
-
} finally {
|
|
442
|
-
if (descriptor !== null) {
|
|
443
|
-
try {
|
|
444
|
-
closeSync(descriptor);
|
|
445
|
-
} catch {
|
|
446
|
-
// Keep diagnostics categorical and path-free.
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
349
|
}
|
|
450
350
|
}
|
|
451
351
|
|
|
@@ -484,48 +384,20 @@ export function verifyGuardPrivateFile(
|
|
|
484
384
|
expected: GuardPrivateFileEvidence,
|
|
485
385
|
expectedContent: string,
|
|
486
386
|
): void {
|
|
487
|
-
let descriptor: number | null = null;
|
|
488
387
|
try {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
);
|
|
493
|
-
const before = fstatSync(descriptor, { bigint: true });
|
|
388
|
+
const read = readOwnedFileStableSync(path, expected.byteLength, {
|
|
389
|
+
exactMode: 0o600,
|
|
390
|
+
minimumBytes: 1n,
|
|
391
|
+
});
|
|
494
392
|
if (
|
|
495
|
-
|
|
496
|
-
||
|
|
497
|
-
||
|
|
498
|
-
||
|
|
499
|
-
|| before.ino.toString() !== expected.inode
|
|
500
|
-
|| before.size !== BigInt(expected.byteLength)
|
|
501
|
-
) throw new Error("derivation network guard file changed identity");
|
|
502
|
-
const bytes = Buffer.alloc(expected.byteLength);
|
|
503
|
-
let offset = 0;
|
|
504
|
-
while (offset < bytes.byteLength) {
|
|
505
|
-
const count = readSync(descriptor, bytes, offset, bytes.byteLength - offset, null);
|
|
506
|
-
if (count === 0) throw new Error("derivation network guard file changed size");
|
|
507
|
-
offset += count;
|
|
508
|
-
}
|
|
509
|
-
const after = fstatSync(descriptor, { bigint: true });
|
|
510
|
-
if (
|
|
511
|
-
before.dev !== after.dev
|
|
512
|
-
|| before.ino !== after.ino
|
|
513
|
-
|| before.size !== after.size
|
|
514
|
-
|| before.mtimeNs !== after.mtimeNs
|
|
515
|
-
|| before.ctimeNs !== after.ctimeNs
|
|
516
|
-
|| bytes.toString("utf8") !== expectedContent
|
|
393
|
+
read.dev.toString() !== expected.device
|
|
394
|
+
|| read.ino.toString() !== expected.inode
|
|
395
|
+
|| read.bytes.byteLength !== expected.byteLength
|
|
396
|
+
|| read.bytes.toString("utf8") !== expectedContent
|
|
517
397
|
|| sha256(expectedContent) !== expected.sha256
|
|
518
|
-
) throw new Error("derivation network guard file changed
|
|
398
|
+
) throw new Error("derivation network guard file changed");
|
|
519
399
|
} catch {
|
|
520
400
|
throw new Error("derivation network guard file changed or is unavailable");
|
|
521
|
-
} finally {
|
|
522
|
-
if (descriptor !== null) {
|
|
523
|
-
try {
|
|
524
|
-
closeSync(descriptor);
|
|
525
|
-
} catch {
|
|
526
|
-
// Keep diagnostics categorical and path-free.
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
401
|
}
|
|
530
402
|
}
|
|
531
403
|
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import {
|
|
3
3
|
chmodSync,
|
|
4
|
-
closeSync,
|
|
5
|
-
constants,
|
|
6
|
-
fstatSync,
|
|
7
4
|
lstatSync,
|
|
8
|
-
openSync,
|
|
9
|
-
readSync,
|
|
10
5
|
rmdirSync,
|
|
11
6
|
unlinkSync,
|
|
12
7
|
} from "node:fs";
|
|
13
8
|
import { createServer, type Server, type Socket } from "node:net";
|
|
14
9
|
|
|
10
|
+
import {
|
|
11
|
+
assertOwnedPathSync,
|
|
12
|
+
readOwnedFileStableSync,
|
|
13
|
+
} from "@hraness/local-custody/private-paths";
|
|
14
|
+
|
|
15
15
|
import {
|
|
16
16
|
DERIVATION_GUARD_PROXY_CONFIG,
|
|
17
17
|
DERIVATION_GUARD_PROXY_READY,
|
|
@@ -39,11 +39,6 @@ function injectFailureForTest(stage: HelperFailureStage): void {
|
|
|
39
39
|
) throw new Error("injected derivation proxy helper failure");
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function currentUserOwns(uid: number | bigint): boolean {
|
|
43
|
-
const current = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
44
|
-
return current === undefined || uid === (typeof uid === "bigint" ? BigInt(current) : current);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
42
|
function sameIdentity(left: GuardDirectoryIdentity, right: GuardDirectoryIdentity): boolean {
|
|
48
43
|
return left.device === right.device && left.inode === right.inode;
|
|
49
44
|
}
|
|
@@ -95,25 +90,23 @@ function provisionalReadyFileIsAbsent(): boolean {
|
|
|
95
90
|
}
|
|
96
91
|
|
|
97
92
|
function inspectPrivateDirectory(path: string): GuardDirectoryIdentity {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return { device: stats.dev.toString(), inode: stats.ino.toString() };
|
|
93
|
+
let identity: { readonly dev: number; readonly ino: number };
|
|
94
|
+
try {
|
|
95
|
+
identity = assertOwnedPathSync(path, { kind: "directory", exactMode: 0o700 });
|
|
96
|
+
} catch {
|
|
97
|
+
throw new Error("derivation proxy directory is unavailable or unsafe");
|
|
98
|
+
}
|
|
99
|
+
return { device: identity.dev.toString(), inode: identity.ino.toString() };
|
|
106
100
|
}
|
|
107
101
|
|
|
108
102
|
function inspectPrivateControlSocket(path: string): GuardDirectoryIdentity {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return { device: stats.dev.toString(), inode: stats.ino.toString() };
|
|
103
|
+
let identity: { readonly dev: number; readonly ino: number };
|
|
104
|
+
try {
|
|
105
|
+
identity = assertOwnedPathSync(path, { kind: "socket", exactMode: 0o600 });
|
|
106
|
+
} catch {
|
|
107
|
+
throw new Error("derivation proxy control socket is unavailable or unsafe");
|
|
108
|
+
}
|
|
109
|
+
return { device: identity.dev.toString(), inode: identity.ino.toString() };
|
|
117
110
|
}
|
|
118
111
|
|
|
119
112
|
function unlinkExactControlSocket(
|
|
@@ -147,38 +140,16 @@ function waitForSocketClose(socket: Socket): Promise<void> {
|
|
|
147
140
|
}
|
|
148
141
|
|
|
149
142
|
function readPrivateConfig(): unknown {
|
|
150
|
-
|
|
151
|
-
DERIVATION_GUARD_PROXY_CONFIG,
|
|
152
|
-
constants.O_RDONLY | ("O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0),
|
|
153
|
-
);
|
|
143
|
+
let read: ReturnType<typeof readOwnedFileStableSync>;
|
|
154
144
|
try {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|| before.size > BigInt(MAX_CONFIG_BYTES)
|
|
162
|
-
) throw new Error("derivation proxy config is unavailable or unsafe");
|
|
163
|
-
const bytes = Buffer.alloc(Number(before.size));
|
|
164
|
-
let offset = 0;
|
|
165
|
-
while (offset < bytes.byteLength) {
|
|
166
|
-
const count = readSync(descriptor, bytes, offset, bytes.byteLength - offset, null);
|
|
167
|
-
if (count === 0) throw new Error("derivation proxy config changed size");
|
|
168
|
-
offset += count;
|
|
169
|
-
}
|
|
170
|
-
const after = fstatSync(descriptor, { bigint: true });
|
|
171
|
-
if (
|
|
172
|
-
before.dev !== after.dev
|
|
173
|
-
|| before.ino !== after.ino
|
|
174
|
-
|| before.size !== after.size
|
|
175
|
-
|| before.mtimeNs !== after.mtimeNs
|
|
176
|
-
|| before.ctimeNs !== after.ctimeNs
|
|
177
|
-
) throw new Error("derivation proxy config changed while reading");
|
|
178
|
-
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) as unknown;
|
|
179
|
-
} finally {
|
|
180
|
-
closeSync(descriptor);
|
|
145
|
+
read = readOwnedFileStableSync(DERIVATION_GUARD_PROXY_CONFIG, MAX_CONFIG_BYTES, {
|
|
146
|
+
exactMode: 0o600,
|
|
147
|
+
minimumBytes: 1n,
|
|
148
|
+
});
|
|
149
|
+
} catch {
|
|
150
|
+
throw new Error("derivation proxy config is unavailable or unsafe");
|
|
181
151
|
}
|
|
152
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(read.bytes)) as unknown;
|
|
182
153
|
}
|
|
183
154
|
|
|
184
155
|
function exactKeys(record: Record<string, unknown>, expected: readonly string[]): boolean {
|
package/src/derive.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "@hraness/kb/clip/acquire";
|
|
23
23
|
import { isPrivateAddress, isPrivateHostname } from "@hraness/kb/clip/network";
|
|
24
24
|
import type { StrictCookie } from "@hraness/kb/clip/cookies";
|
|
25
|
+
import { assertOwnedPathSync } from "@hraness/local-custody/private-paths";
|
|
25
26
|
import type { GhostgetAuth } from "./auth";
|
|
26
27
|
import {
|
|
27
28
|
agentBrowserFailure,
|
|
@@ -234,14 +235,14 @@ function ownedByCurrentUser(uid: number | bigint): boolean {
|
|
|
234
235
|
}
|
|
235
236
|
|
|
236
237
|
function inspectDirectoryIdentity(path: string): DirectoryIdentity {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return { device:
|
|
238
|
+
let identity: { readonly dev: number; readonly ino: number };
|
|
239
|
+
try {
|
|
240
|
+
identity = assertOwnedPathSync(path, { kind: "directory", exactMode: 0o700 });
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (hasErrorCode(error, "ENOENT")) throw error;
|
|
243
|
+
throw new Error("derivation session directory is unsafe", { cause: error });
|
|
244
|
+
}
|
|
245
|
+
return { device: identity.dev.toString(), inode: identity.ino.toString() };
|
|
245
246
|
}
|
|
246
247
|
|
|
247
248
|
function parseDirectoryIdentity(value: unknown): DirectoryIdentity {
|
|
@@ -1101,25 +1102,21 @@ function inspectPrivateFile(
|
|
|
1101
1102
|
maximumBytes: number,
|
|
1102
1103
|
label: string,
|
|
1103
1104
|
): Omit<PrivateFileEvidence, "sha256"> {
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|| (stats.mode & 0o077n) !== 0n
|
|
1116
|
-
|| stats.size < 1n
|
|
1117
|
-
|| stats.size > BigInt(maximumBytes)
|
|
1118
|
-
) throw new Error(`${label} is unavailable or unsafe`);
|
|
1105
|
+
let identity: { readonly dev: number; readonly ino: number; readonly size: number };
|
|
1106
|
+
try {
|
|
1107
|
+
identity = assertOwnedPathSync(path, {
|
|
1108
|
+
kind: "file",
|
|
1109
|
+
ownerOnly: true,
|
|
1110
|
+
minimumBytes: 1n,
|
|
1111
|
+
maximumBytes: BigInt(maximumBytes),
|
|
1112
|
+
});
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
throw new Error(`${label} is unavailable or unsafe`, { cause: error });
|
|
1115
|
+
}
|
|
1119
1116
|
return {
|
|
1120
|
-
device:
|
|
1121
|
-
inode:
|
|
1122
|
-
byteLength:
|
|
1117
|
+
device: identity.dev.toString(),
|
|
1118
|
+
inode: identity.ino.toString(),
|
|
1119
|
+
byteLength: identity.size,
|
|
1123
1120
|
};
|
|
1124
1121
|
}
|
|
1125
1122
|
|
|
@@ -1444,18 +1441,20 @@ function inspectCapturedHar(path: string): {
|
|
|
1444
1441
|
readonly inode: string;
|
|
1445
1442
|
readonly byteLength: number;
|
|
1446
1443
|
} {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1444
|
+
let identity: { readonly dev: number; readonly ino: number; readonly size: number };
|
|
1445
|
+
try {
|
|
1446
|
+
identity = assertOwnedPathSync(path, {
|
|
1447
|
+
kind: "file",
|
|
1448
|
+
minimumBytes: 1n,
|
|
1449
|
+
maximumBytes: BigInt(MAX_HAR_BYTES),
|
|
1450
|
+
});
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
throw new Error("derivation review HAR is unavailable or unsafe", { cause: error });
|
|
1453
|
+
}
|
|
1455
1454
|
return {
|
|
1456
|
-
device:
|
|
1457
|
-
inode:
|
|
1458
|
-
byteLength:
|
|
1455
|
+
device: identity.dev.toString(),
|
|
1456
|
+
inode: identity.ino.toString(),
|
|
1457
|
+
byteLength: identity.size,
|
|
1459
1458
|
};
|
|
1460
1459
|
}
|
|
1461
1460
|
|
package/src/ghostget.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
saveAuth,
|
|
23
23
|
type GhostgetAuth,
|
|
24
24
|
} from "./auth";
|
|
25
|
+
import { runAccountsDeviceLogin, runAccountsSignOut } from "./accounts-auth";
|
|
25
26
|
import {
|
|
26
27
|
isPublicWebSessionInvocationAuthority,
|
|
27
28
|
type InvocationAuthority,
|
|
@@ -1983,6 +1984,48 @@ async function runCommand(
|
|
|
1983
1984
|
output.stdout(renderGhostgetUsage());
|
|
1984
1985
|
return 0;
|
|
1985
1986
|
}
|
|
1987
|
+
if (arguments_.command === "login") {
|
|
1988
|
+
const result = await runAccountsDeviceLogin(
|
|
1989
|
+
environment,
|
|
1990
|
+
{
|
|
1991
|
+
onUserCode: (userCode, verificationUri) => {
|
|
1992
|
+
output.stderr(
|
|
1993
|
+
`Open ${safe(verificationUri)} in a browser where you are already signed in to Hraness Accounts and enter code: ${safe(userCode)}\n`,
|
|
1994
|
+
);
|
|
1995
|
+
},
|
|
1996
|
+
onPending: () => {
|
|
1997
|
+
output.stderr("Waiting for browser approval...\n");
|
|
1998
|
+
},
|
|
1999
|
+
onSlowDown: () => {
|
|
2000
|
+
output.stderr("Polling more slowly...\n");
|
|
2001
|
+
},
|
|
2002
|
+
},
|
|
2003
|
+
undefined,
|
|
2004
|
+
signal,
|
|
2005
|
+
);
|
|
2006
|
+
if (arguments_.json) {
|
|
2007
|
+
output.stdout(exactTerminalJson({ ok: result.kind === "success", ...result }));
|
|
2008
|
+
} else if (result.kind === "success") {
|
|
2009
|
+
output.stdout(`${safe(result.message)}\n`);
|
|
2010
|
+
} else if (result.kind === "denied") {
|
|
2011
|
+
output.stderr("Login was denied in the browser.\n");
|
|
2012
|
+
} else if (result.kind === "expired") {
|
|
2013
|
+
output.stderr("The sign-in code expired. Run 'ghostget login' again.\n");
|
|
2014
|
+
} else {
|
|
2015
|
+
output.stderr(`Login failed: ${safe(result.message)}\n`);
|
|
2016
|
+
}
|
|
2017
|
+
return result.kind === "success" ? 0 : 3;
|
|
2018
|
+
}
|
|
2019
|
+
if (arguments_.command === "logout") {
|
|
2020
|
+
await runAccountsSignOut(environment);
|
|
2021
|
+
const message = "Signed out of Hraness Accounts.";
|
|
2022
|
+
if (arguments_.json) {
|
|
2023
|
+
output.stdout(exactTerminalJson({ ok: true, message }));
|
|
2024
|
+
} else {
|
|
2025
|
+
output.stdout(`${message}\n`);
|
|
2026
|
+
}
|
|
2027
|
+
return 0;
|
|
2028
|
+
}
|
|
1986
2029
|
if (arguments_.command === "whatsapp-automation-install") {
|
|
1987
2030
|
const { installReviewedWhatsAppAutomationBinary } = await import("./providers/whatsapp-automation-runtime");
|
|
1988
2031
|
const installed = arguments_.binary === undefined
|
package/src/storage.ts
CHANGED
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
mkdtempSync,
|
|
11
11
|
openSync,
|
|
12
12
|
opendirSync,
|
|
13
|
-
readSync,
|
|
14
13
|
realpathSync,
|
|
15
14
|
rmSync,
|
|
16
15
|
writeFileSync,
|
|
@@ -18,6 +17,7 @@ import {
|
|
|
18
17
|
type Dirent,
|
|
19
18
|
} from "node:fs";
|
|
20
19
|
import { spawnSync } from "node:child_process";
|
|
20
|
+
import { readOwnedFileStableSync } from "@hraness/local-custody/private-paths";
|
|
21
21
|
import { homedir, tmpdir } from "node:os";
|
|
22
22
|
import { basename, dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
23
23
|
import { fileURLToPath } from "node:url";
|
|
@@ -363,20 +363,6 @@ function hasCode(error: unknown, code: string): boolean {
|
|
|
363
363
|
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
-
function readDescriptorBounded(descriptor: number, maximumBytes: number): Buffer {
|
|
367
|
-
const chunks: Buffer[] = [];
|
|
368
|
-
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maximumBytes + 1));
|
|
369
|
-
let total = 0;
|
|
370
|
-
for (;;) {
|
|
371
|
-
const remaining = maximumBytes + 1 - total;
|
|
372
|
-
const count = readSync(descriptor, buffer, 0, Math.min(buffer.byteLength, remaining), null);
|
|
373
|
-
if (count === 0) return Buffer.concat(chunks, total);
|
|
374
|
-
total += count;
|
|
375
|
-
if (total > maximumBytes) throw new Error("file grew beyond its byte bound");
|
|
376
|
-
chunks.push(Buffer.from(buffer.subarray(0, count)));
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
366
|
function pathInside(root: string, target: string): boolean {
|
|
381
367
|
const child = relative(root, target);
|
|
382
368
|
return child === "" || (!isAbsolute(child) && child !== ".." && !child.startsWith(`..${sep}`));
|
|
@@ -1037,18 +1023,17 @@ function ownedByCurrentUser(stats: { readonly uid: number | bigint }): boolean {
|
|
|
1037
1023
|
}
|
|
1038
1024
|
|
|
1039
1025
|
function readStateMarker(path: string): void {
|
|
1040
|
-
|
|
1041
|
-
const nonBlocking = "O_NONBLOCK" in constants ? constants.O_NONBLOCK : 0;
|
|
1042
|
-
const descriptor = openSync(path, constants.O_RDONLY | noFollow | nonBlocking);
|
|
1026
|
+
let bytes: Buffer;
|
|
1043
1027
|
try {
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1028
|
+
bytes = readOwnedFileStableSync(path, 256).bytes;
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
throw new Error("ghostget state marker must be a private, owned regular file", { cause: error });
|
|
1031
|
+
}
|
|
1032
|
+
{
|
|
1048
1033
|
const content = new TextDecoder("utf-8", {
|
|
1049
1034
|
fatal: true,
|
|
1050
1035
|
ignoreBOM: true,
|
|
1051
|
-
}).decode(
|
|
1036
|
+
}).decode(bytes);
|
|
1052
1037
|
if (content !== stateMarkerText) throw new Error("ghostget state marker is malformed");
|
|
1053
1038
|
const value = JSON.parse(content) as unknown;
|
|
1054
1039
|
if (
|
|
@@ -1061,8 +1046,6 @@ function readStateMarker(path: string): void {
|
|
|
1061
1046
|
|| !("kind" in value)
|
|
1062
1047
|
|| value.kind !== "io-state"
|
|
1063
1048
|
) throw new Error("ghostget state marker is malformed");
|
|
1064
|
-
} finally {
|
|
1065
|
-
closeSync(descriptor);
|
|
1066
1049
|
}
|
|
1067
1050
|
}
|
|
1068
1051
|
|
package/src/usage.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* optional provider runtime is broken.
|
|
7
7
|
*/
|
|
8
8
|
export const ghostgetUsage = `Usage:
|
|
9
|
+
ghostget login [--json] Sign in to Hraness Accounts
|
|
10
|
+
ghostget logout [--json] Sign out of Hraness Accounts
|
|
9
11
|
ghostget --version Print the exact Ghostget release version
|
|
10
12
|
ghostget support [--json] View optional product updates and support
|
|
11
13
|
ghostget support protocol --json Read the optional agent closeout protocol
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Canonical immutable Ghostget package release identity. */
|
|
2
|
-
export const GHOSTGET_VERSION = "0.18.
|
|
2
|
+
export const GHOSTGET_VERSION = "0.18.16" as const;
|