@hraness/ghostget 0.18.14 → 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/CHANGELOG.md +9 -0
- 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-g6na3p6k.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/providers/x-web-runtime.ts +11 -11
- package/src/providers/x-web.ts +36 -35
- package/src/storage.ts +8 -25
- package/src/usage.ts +2 -0
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ Historical entries retain their original delivery coordinates.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.18.15
|
|
11
|
+
|
|
12
|
+
- Refresh the X web descriptor evidence for the 2026-09-17 authenticated
|
|
13
|
+
client-web drop. X rotated most GraphQL query IDs and chunk names, so exact
|
|
14
|
+
profile, timeline, and Article operations resume instead of reporting
|
|
15
|
+
contract drift. The three retired DM search descriptors are removed, and a
|
|
16
|
+
deleted or renamed profile target now reports `target-unavailable` rather
|
|
17
|
+
than contract drift when X returns its empty-data result.
|
|
18
|
+
|
|
10
19
|
## 0.18.14
|
|
11
20
|
|
|
12
21
|
- Pin `@hraness/site-footer` v0.13.0 so every public page's shared footer
|
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";
|