@ggui-ai/cli 0.1.0-rc.1
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/LICENSE +201 -0
- package/README.md +96 -0
- package/dist/agent-resolution.d.ts +41 -0
- package/dist/agent-resolution.d.ts.map +1 -0
- package/dist/agent-resolution.js +80 -0
- package/dist/api-client.d.ts +59 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +145 -0
- package/dist/auth-keys.d.ts +10 -0
- package/dist/auth-keys.d.ts.map +1 -0
- package/dist/auth-keys.js +382 -0
- package/dist/auth-login.d.ts +11 -0
- package/dist/auth-login.d.ts.map +1 -0
- package/dist/auth-login.js +231 -0
- package/dist/auth-logout.d.ts +3 -0
- package/dist/auth-logout.d.ts.map +1 -0
- package/dist/auth-logout.js +32 -0
- package/dist/auth-store.d.ts +67 -0
- package/dist/auth-store.d.ts.map +1 -0
- package/dist/auth-store.js +150 -0
- package/dist/auth-whoami.d.ts +3 -0
- package/dist/auth-whoami.d.ts.map +1 -0
- package/dist/auth-whoami.js +52 -0
- package/dist/blueprint-command.d.ts +3 -0
- package/dist/blueprint-command.d.ts.map +1 -0
- package/dist/blueprint-command.js +203 -0
- package/dist/blueprint-create.d.ts +71 -0
- package/dist/blueprint-create.d.ts.map +1 -0
- package/dist/blueprint-create.js +377 -0
- package/dist/byok-resolver.d.ts +141 -0
- package/dist/byok-resolver.d.ts.map +1 -0
- package/dist/byok-resolver.js +89 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1066 -0
- package/dist/dev-command.d.ts +169 -0
- package/dist/dev-command.d.ts.map +1 -0
- package/dist/dev-command.js +222 -0
- package/dist/gadget-command.d.ts +3 -0
- package/dist/gadget-command.d.ts.map +1 -0
- package/dist/gadget-command.js +181 -0
- package/dist/gadget-create.d.ts +106 -0
- package/dist/gadget-create.d.ts.map +1 -0
- package/dist/gadget-create.js +492 -0
- package/dist/generation-probe.d.ts +135 -0
- package/dist/generation-probe.d.ts.map +1 -0
- package/dist/generation-probe.js +132 -0
- package/dist/internal/artifact-install.d.ts +150 -0
- package/dist/internal/artifact-install.d.ts.map +1 -0
- package/dist/internal/artifact-install.js +1019 -0
- package/dist/internal/artifact-publish.d.ts +204 -0
- package/dist/internal/artifact-publish.d.ts.map +1 -0
- package/dist/internal/artifact-publish.js +1141 -0
- package/dist/internal/artifact-search.d.ts +155 -0
- package/dist/internal/artifact-search.d.ts.map +1 -0
- package/dist/internal/artifact-search.js +539 -0
- package/dist/internal/artifact-uninstall.d.ts +30 -0
- package/dist/internal/artifact-uninstall.d.ts.map +1 -0
- package/dist/internal/artifact-uninstall.js +205 -0
- package/dist/internal/auth-cache.d.ts +40 -0
- package/dist/internal/auth-cache.d.ts.map +1 -0
- package/dist/internal/auth-cache.js +133 -0
- package/dist/internal/auth-strategy.d.ts +89 -0
- package/dist/internal/auth-strategy.d.ts.map +1 -0
- package/dist/internal/auth-strategy.js +132 -0
- package/dist/internal/ggui-json.d.ts +53 -0
- package/dist/internal/ggui-json.d.ts.map +1 -0
- package/dist/internal/ggui-json.js +92 -0
- package/dist/internal/key-store.d.ts +43 -0
- package/dist/internal/key-store.d.ts.map +1 -0
- package/dist/internal/key-store.js +128 -0
- package/dist/internal/oidc-token.d.ts +52 -0
- package/dist/internal/oidc-token.d.ts.map +1 -0
- package/dist/internal/oidc-token.js +351 -0
- package/dist/internal/register-author-key.d.ts +44 -0
- package/dist/internal/register-author-key.d.ts.map +1 -0
- package/dist/internal/register-author-key.js +212 -0
- package/dist/local-keys.d.ts +47 -0
- package/dist/local-keys.d.ts.map +1 -0
- package/dist/local-keys.js +115 -0
- package/dist/mcp-backend.d.ts +356 -0
- package/dist/mcp-backend.d.ts.map +1 -0
- package/dist/mcp-backend.js +668 -0
- package/dist/paths.d.ts +77 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +119 -0
- package/dist/persistent-secrets.d.ts +28 -0
- package/dist/persistent-secrets.d.ts.map +1 -0
- package/dist/persistent-secrets.js +94 -0
- package/dist/persistent-short-code-index.d.ts +14 -0
- package/dist/persistent-short-code-index.d.ts.map +1 -0
- package/dist/persistent-short-code-index.js +39 -0
- package/dist/persistent-stores.d.ts +12 -0
- package/dist/persistent-stores.d.ts.map +1 -0
- package/dist/persistent-stores.js +42 -0
- package/dist/serve-command.d.ts +500 -0
- package/dist/serve-command.d.ts.map +1 -0
- package/dist/serve-command.js +616 -0
- package/dist/serve-fallback.d.ts +64 -0
- package/dist/serve-fallback.d.ts.map +1 -0
- package/dist/serve-fallback.js +122 -0
- package/dist/spawn-cloudflared.d.ts +61 -0
- package/dist/spawn-cloudflared.d.ts.map +1 -0
- package/dist/spawn-cloudflared.js +167 -0
- package/dist/theme-command.d.ts +4 -0
- package/dist/theme-command.d.ts.map +1 -0
- package/dist/theme-command.js +154 -0
- package/dist/theme-file-uploader.d.ts +8 -0
- package/dist/theme-file-uploader.d.ts.map +1 -0
- package/dist/theme-file-uploader.js +58 -0
- package/dist/theme-writer.d.ts +8 -0
- package/dist/theme-writer.d.ts.map +1 -0
- package/dist/theme-writer.js +103 -0
- package/dist/tunnel-provider.d.ts +273 -0
- package/dist/tunnel-provider.d.ts.map +1 -0
- package/dist/tunnel-provider.js +142 -0
- package/package.json +81 -0
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return the OSS CLI config directory.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order:
|
|
5
|
+
* 1. `GGUI_CONFIG_DIR` env (if set + non-empty) — tests + clean-room.
|
|
6
|
+
* 2. `~/.ggui` (`os.homedir() + DIRNAME`) — operator default.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getConfigDir(): string;
|
|
9
|
+
/**
|
|
10
|
+
* Path to the BYOK credentials file. Written + read by the
|
|
11
|
+
* `PlaintextFileProviderKeyStore` from `@ggui-ai/mcp-server-core/plaintext`
|
|
12
|
+
* at the global app-scope (single-user personal-mode default).
|
|
13
|
+
*
|
|
14
|
+
* On-disk shape is `PlaintextFileProviderKeyStore`'s v1 document. Mode
|
|
15
|
+
* 0o600 is enforced by the store on every write.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getCredentialsFile(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Path to the CLI auth-session file. Written by `ggui login` after a
|
|
20
|
+
* successful device-flow exchange against `api.ggui.ai`; read by every
|
|
21
|
+
* authenticated command (`whoami`, `keys *`).
|
|
22
|
+
*
|
|
23
|
+
* On-disk shape is `AuthSessionDocument` from `./lib/auth-store.ts`.
|
|
24
|
+
* Mode 0o600 is enforced by the store on every write — the access +
|
|
25
|
+
* refresh tokens are bearer secrets equivalent to a short-lived
|
|
26
|
+
* password.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getAuthFile(): string;
|
|
29
|
+
/**
|
|
30
|
+
* Path to the local-embedding model cache. Resolution order matches
|
|
31
|
+
* the OSS full-generation port plan (§2.B + §7.2):
|
|
32
|
+
*
|
|
33
|
+
* 1. `GGUI_EMBEDDING_CACHE_DIR` env — operator override (CI, custom
|
|
34
|
+
* mounts, shared NFS).
|
|
35
|
+
* 2. `~/.ggui/models` — first-run default. Owned by ggui to avoid
|
|
36
|
+
* polluting the operator's `~/.cache/huggingface` cache.
|
|
37
|
+
*
|
|
38
|
+
* Returned even when the directory does not exist on disk — the
|
|
39
|
+
* embedding bootstrap creates it on first warmup.
|
|
40
|
+
*/
|
|
41
|
+
export declare function getEmbeddingCacheDir(): string;
|
|
42
|
+
/**
|
|
43
|
+
* Path to the content-addressable componentCode cache. Used by
|
|
44
|
+
* `FileSystemCodeStore` when wired as the OSS dev default.
|
|
45
|
+
*
|
|
46
|
+
* Resolution order:
|
|
47
|
+
* 1. `GGUI_CODE_CACHE_DIR` env — operator override.
|
|
48
|
+
* 2. `~/.ggui/code-cache` — first-run default.
|
|
49
|
+
*
|
|
50
|
+
* The store creates the directory lazily on first put; the path is
|
|
51
|
+
* safe to return even when nothing has been written yet. Operators
|
|
52
|
+
* who want to reset the cache can `rm -rf` this directory at any
|
|
53
|
+
* time — every entry is content-addressable + immutable, so a
|
|
54
|
+
* re-fetch repopulates from upstream code on next push.
|
|
55
|
+
*/
|
|
56
|
+
export declare function getCodeCacheDir(): string;
|
|
57
|
+
/**
|
|
58
|
+
* Directory backing the `--persistent` bundle (sessions.sqlite,
|
|
59
|
+
* short-codes.sqlite, bootstrap-secret.hex, render-signer-secret.hex,
|
|
60
|
+
* keys.json …). Survives `ggui serve` restarts so claude.ai chat-history
|
|
61
|
+
* revisits can rehydrate the iframe instead of seeing four cascading
|
|
62
|
+
* failures (HMAC + shortCode + session + pairing).
|
|
63
|
+
*
|
|
64
|
+
* Resolution order:
|
|
65
|
+
* 1. `GGUI_PERSISTENT_DIR` env — operator override (tests, clean-room,
|
|
66
|
+
* shared mount).
|
|
67
|
+
* 2. `<projectRoot>/.ggui/persistent` — when the CLI resolved a
|
|
68
|
+
* ggui.json. Per-project isolation; different projects get
|
|
69
|
+
* separate sessions even on the same machine.
|
|
70
|
+
* 3. `~/.ggui/persistent` — fallback when there's no manifest
|
|
71
|
+
* (`ggui serve` from a bare directory).
|
|
72
|
+
*
|
|
73
|
+
* Returned even when the directory doesn't exist on disk — the
|
|
74
|
+
* persistence wiring creates parents on first write.
|
|
75
|
+
*/
|
|
76
|
+
export declare function getPersistentDir(projectRoot?: string | null): string;
|
|
77
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAkBA;;;;;;GAMG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAIrC;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAI7C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAIxC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAOpE"}
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized path helpers for the open `@ggui-ai/cli` (`ggui`).
|
|
3
|
+
*
|
|
4
|
+
* The OSS dirstub lives at `~/.ggui/`. All CLI-managed state
|
|
5
|
+
* (credentials, embedding cache, code cache, persistent bundle) is
|
|
6
|
+
* rooted under this single directory.
|
|
7
|
+
*
|
|
8
|
+
* Override the dir via `GGUI_CONFIG_DIR` env so tests + clean-room
|
|
9
|
+
* harnesses can scope the store to a temp dir without touching the
|
|
10
|
+
* operator's HOME. The override is read on every call (no module-level
|
|
11
|
+
* cache) so process-level env mutations between calls take effect —
|
|
12
|
+
* matters for vitest suites that mutate `process.env` per test.
|
|
13
|
+
*/
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
const DIRNAME = '.ggui';
|
|
17
|
+
/**
|
|
18
|
+
* Return the OSS CLI config directory.
|
|
19
|
+
*
|
|
20
|
+
* Resolution order:
|
|
21
|
+
* 1. `GGUI_CONFIG_DIR` env (if set + non-empty) — tests + clean-room.
|
|
22
|
+
* 2. `~/.ggui` (`os.homedir() + DIRNAME`) — operator default.
|
|
23
|
+
*/
|
|
24
|
+
export function getConfigDir() {
|
|
25
|
+
const override = process.env['GGUI_CONFIG_DIR'];
|
|
26
|
+
if (override && override.length > 0)
|
|
27
|
+
return override;
|
|
28
|
+
return join(homedir(), DIRNAME);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Path to the BYOK credentials file. Written + read by the
|
|
32
|
+
* `PlaintextFileProviderKeyStore` from `@ggui-ai/mcp-server-core/plaintext`
|
|
33
|
+
* at the global app-scope (single-user personal-mode default).
|
|
34
|
+
*
|
|
35
|
+
* On-disk shape is `PlaintextFileProviderKeyStore`'s v1 document. Mode
|
|
36
|
+
* 0o600 is enforced by the store on every write.
|
|
37
|
+
*/
|
|
38
|
+
export function getCredentialsFile() {
|
|
39
|
+
return join(getConfigDir(), 'credentials.json');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Path to the CLI auth-session file. Written by `ggui login` after a
|
|
43
|
+
* successful device-flow exchange against `api.ggui.ai`; read by every
|
|
44
|
+
* authenticated command (`whoami`, `keys *`).
|
|
45
|
+
*
|
|
46
|
+
* On-disk shape is `AuthSessionDocument` from `./lib/auth-store.ts`.
|
|
47
|
+
* Mode 0o600 is enforced by the store on every write — the access +
|
|
48
|
+
* refresh tokens are bearer secrets equivalent to a short-lived
|
|
49
|
+
* password.
|
|
50
|
+
*/
|
|
51
|
+
export function getAuthFile() {
|
|
52
|
+
return join(getConfigDir(), 'auth.json');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Path to the local-embedding model cache. Resolution order matches
|
|
56
|
+
* the OSS full-generation port plan (§2.B + §7.2):
|
|
57
|
+
*
|
|
58
|
+
* 1. `GGUI_EMBEDDING_CACHE_DIR` env — operator override (CI, custom
|
|
59
|
+
* mounts, shared NFS).
|
|
60
|
+
* 2. `~/.ggui/models` — first-run default. Owned by ggui to avoid
|
|
61
|
+
* polluting the operator's `~/.cache/huggingface` cache.
|
|
62
|
+
*
|
|
63
|
+
* Returned even when the directory does not exist on disk — the
|
|
64
|
+
* embedding bootstrap creates it on first warmup.
|
|
65
|
+
*/
|
|
66
|
+
export function getEmbeddingCacheDir() {
|
|
67
|
+
const override = process.env['GGUI_EMBEDDING_CACHE_DIR'];
|
|
68
|
+
if (override && override.length > 0)
|
|
69
|
+
return override;
|
|
70
|
+
return join(getConfigDir(), 'models');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Path to the content-addressable componentCode cache. Used by
|
|
74
|
+
* `FileSystemCodeStore` when wired as the OSS dev default.
|
|
75
|
+
*
|
|
76
|
+
* Resolution order:
|
|
77
|
+
* 1. `GGUI_CODE_CACHE_DIR` env — operator override.
|
|
78
|
+
* 2. `~/.ggui/code-cache` — first-run default.
|
|
79
|
+
*
|
|
80
|
+
* The store creates the directory lazily on first put; the path is
|
|
81
|
+
* safe to return even when nothing has been written yet. Operators
|
|
82
|
+
* who want to reset the cache can `rm -rf` this directory at any
|
|
83
|
+
* time — every entry is content-addressable + immutable, so a
|
|
84
|
+
* re-fetch repopulates from upstream code on next push.
|
|
85
|
+
*/
|
|
86
|
+
export function getCodeCacheDir() {
|
|
87
|
+
const override = process.env['GGUI_CODE_CACHE_DIR'];
|
|
88
|
+
if (override && override.length > 0)
|
|
89
|
+
return override;
|
|
90
|
+
return join(getConfigDir(), 'code-cache');
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Directory backing the `--persistent` bundle (sessions.sqlite,
|
|
94
|
+
* short-codes.sqlite, bootstrap-secret.hex, render-signer-secret.hex,
|
|
95
|
+
* keys.json …). Survives `ggui serve` restarts so claude.ai chat-history
|
|
96
|
+
* revisits can rehydrate the iframe instead of seeing four cascading
|
|
97
|
+
* failures (HMAC + shortCode + session + pairing).
|
|
98
|
+
*
|
|
99
|
+
* Resolution order:
|
|
100
|
+
* 1. `GGUI_PERSISTENT_DIR` env — operator override (tests, clean-room,
|
|
101
|
+
* shared mount).
|
|
102
|
+
* 2. `<projectRoot>/.ggui/persistent` — when the CLI resolved a
|
|
103
|
+
* ggui.json. Per-project isolation; different projects get
|
|
104
|
+
* separate sessions even on the same machine.
|
|
105
|
+
* 3. `~/.ggui/persistent` — fallback when there's no manifest
|
|
106
|
+
* (`ggui serve` from a bare directory).
|
|
107
|
+
*
|
|
108
|
+
* Returned even when the directory doesn't exist on disk — the
|
|
109
|
+
* persistence wiring creates parents on first write.
|
|
110
|
+
*/
|
|
111
|
+
export function getPersistentDir(projectRoot) {
|
|
112
|
+
const override = process.env['GGUI_PERSISTENT_DIR'];
|
|
113
|
+
if (override && override.length > 0)
|
|
114
|
+
return override;
|
|
115
|
+
if (projectRoot && projectRoot.length > 0) {
|
|
116
|
+
return join(projectRoot, DIRNAME, 'persistent');
|
|
117
|
+
}
|
|
118
|
+
return join(getConfigDir(), 'persistent');
|
|
119
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a hex-encoded HMAC secret from `filename`. If the file doesn't
|
|
3
|
+
* exist (ENOENT), mint a fresh 32-byte secret, write it with mode 0600,
|
|
4
|
+
* and return the hex string.
|
|
5
|
+
*
|
|
6
|
+
* Concurrency: the write is atomic — a fresh secret is written to a
|
|
7
|
+
* sibling temp file with `O_CREAT | O_WRONLY` and then `rename(2)`'d
|
|
8
|
+
* into place. POSIX guarantees `rename` is atomic on the same
|
|
9
|
+
* filesystem, so two `ggui serve` processes racing on the same
|
|
10
|
+
* persistent dir can't tear the file at a byte boundary; the loser's
|
|
11
|
+
* rename overwrites the winner's. Both processes then re-read the
|
|
12
|
+
* same surviving value on next boot — last-writer-wins on the secret
|
|
13
|
+
* itself, never partial-write corruption. (Two processes simultaneously
|
|
14
|
+
* minting fresh secrets is already a degenerate case for a per-user
|
|
15
|
+
* persistent dir.)
|
|
16
|
+
*
|
|
17
|
+
* Throws on:
|
|
18
|
+
* - read errors other than ENOENT (permission denied, EISDIR, etc.)
|
|
19
|
+
* - write errors (mkdir / writeFile / rename)
|
|
20
|
+
* - on-disk content that isn't a 64-char hex string (corrupt secret)
|
|
21
|
+
*
|
|
22
|
+
* The throw-on-corrupt branch is deliberate: silently re-minting would
|
|
23
|
+
* invalidate every outstanding token without telling the operator.
|
|
24
|
+
* Better to fail loud so the operator can diagnose (manual edit?) and
|
|
25
|
+
* decide whether to delete and accept the cost.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readOrMintHexSecret(filename: string): string;
|
|
28
|
+
//# sourceMappingURL=persistent-secrets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistent-secrets.d.ts","sourceRoot":"","sources":["../src/persistent-secrets.ts"],"names":[],"mappings":"AAiCA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA4B5D"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-or-mint helpers for HMAC secrets that MUST survive a `ggui serve`
|
|
3
|
+
* restart.
|
|
4
|
+
*
|
|
5
|
+
* Background: `createGguiServer` mints two HMAC secrets at boot when the
|
|
6
|
+
* caller doesn't pass them — `bootstrapSecret` (signs MCP Apps `_meta.
|
|
7
|
+
* ggui.bootstrap.token` so iframes can subscribe to the live-channel WS)
|
|
8
|
+
* and the render-URL signer secret (signs `/r/<code>?sig=&exp=`). Both
|
|
9
|
+
* are process-local `randomBytes(32)` by default — fine for a single
|
|
10
|
+
* process lifetime, fatal for "revisit a claude.ai chat history after a
|
|
11
|
+
* restart": the cached bootstrap token + cached render URL fail HMAC
|
|
12
|
+
* verify against the freshly-minted secret.
|
|
13
|
+
*
|
|
14
|
+
* Persisting the secrets to a 0600-mode file makes those tokens valid
|
|
15
|
+
* across restarts, which is the precondition for any rehydrate path
|
|
16
|
+
* working at all. The file format is intentionally tiny: 64 chars of
|
|
17
|
+
* lowercase hex (32 bytes), no trailing newline, no JSON wrapper. Easy
|
|
18
|
+
* to inspect, easy to rotate (delete the file, restart, fresh secret).
|
|
19
|
+
*
|
|
20
|
+
* Security posture: these secrets are a session-token signing key, NOT
|
|
21
|
+
* a long-lived credential like an OAuth client secret. Losing them
|
|
22
|
+
* means an attacker can mint MCP Apps bootstrap tokens for arbitrary
|
|
23
|
+
* `(sessionId, appId)` pairs — same blast radius as before, just
|
|
24
|
+
* surviving restart. Mode 0600 on the file matches `~/.ggui/credentials
|
|
25
|
+
* .json` (BYOK keys) which has the same threat model.
|
|
26
|
+
*
|
|
27
|
+
* Caller composes the path; this module is pure I/O over a given
|
|
28
|
+
* filename so tests can scope to a tmpdir.
|
|
29
|
+
*/
|
|
30
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
31
|
+
import { randomBytes } from 'node:crypto';
|
|
32
|
+
import { dirname, join } from 'node:path';
|
|
33
|
+
/**
|
|
34
|
+
* Read a hex-encoded HMAC secret from `filename`. If the file doesn't
|
|
35
|
+
* exist (ENOENT), mint a fresh 32-byte secret, write it with mode 0600,
|
|
36
|
+
* and return the hex string.
|
|
37
|
+
*
|
|
38
|
+
* Concurrency: the write is atomic — a fresh secret is written to a
|
|
39
|
+
* sibling temp file with `O_CREAT | O_WRONLY` and then `rename(2)`'d
|
|
40
|
+
* into place. POSIX guarantees `rename` is atomic on the same
|
|
41
|
+
* filesystem, so two `ggui serve` processes racing on the same
|
|
42
|
+
* persistent dir can't tear the file at a byte boundary; the loser's
|
|
43
|
+
* rename overwrites the winner's. Both processes then re-read the
|
|
44
|
+
* same surviving value on next boot — last-writer-wins on the secret
|
|
45
|
+
* itself, never partial-write corruption. (Two processes simultaneously
|
|
46
|
+
* minting fresh secrets is already a degenerate case for a per-user
|
|
47
|
+
* persistent dir.)
|
|
48
|
+
*
|
|
49
|
+
* Throws on:
|
|
50
|
+
* - read errors other than ENOENT (permission denied, EISDIR, etc.)
|
|
51
|
+
* - write errors (mkdir / writeFile / rename)
|
|
52
|
+
* - on-disk content that isn't a 64-char hex string (corrupt secret)
|
|
53
|
+
*
|
|
54
|
+
* The throw-on-corrupt branch is deliberate: silently re-minting would
|
|
55
|
+
* invalidate every outstanding token without telling the operator.
|
|
56
|
+
* Better to fail loud so the operator can diagnose (manual edit?) and
|
|
57
|
+
* decide whether to delete and accept the cost.
|
|
58
|
+
*/
|
|
59
|
+
export function readOrMintHexSecret(filename) {
|
|
60
|
+
let existing = null;
|
|
61
|
+
try {
|
|
62
|
+
existing = readFileSync(filename, 'utf8').trim();
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
if (!isEnoent(err))
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
if (existing !== null) {
|
|
69
|
+
if (!HEX_64.test(existing)) {
|
|
70
|
+
throw new Error(`[ggui-cli] persistent secret at ${filename} is corrupt — expected ` +
|
|
71
|
+
`64 lowercase hex chars (32 bytes), got ${existing.length} chars. ` +
|
|
72
|
+
`Delete the file to re-mint a fresh secret (this will invalidate ` +
|
|
73
|
+
`every outstanding token).`);
|
|
74
|
+
}
|
|
75
|
+
return existing;
|
|
76
|
+
}
|
|
77
|
+
const minted = randomBytes(32).toString('hex');
|
|
78
|
+
const parent = dirname(filename);
|
|
79
|
+
mkdirSync(parent, { recursive: true });
|
|
80
|
+
// Atomic write: pid-suffixed temp file in the same dir, then rename
|
|
81
|
+
// into place. Same-filesystem rename is atomic on POSIX, so a racing
|
|
82
|
+
// boot from another process can't observe a half-written secret.
|
|
83
|
+
const tmp = join(parent, `${filename.split('/').pop()}.tmp-${process.pid}`);
|
|
84
|
+
writeFileSync(tmp, minted, { mode: 0o600, encoding: 'utf8' });
|
|
85
|
+
renameSync(tmp, filename);
|
|
86
|
+
return minted;
|
|
87
|
+
}
|
|
88
|
+
const HEX_64 = /^[0-9a-f]{64}$/;
|
|
89
|
+
function isEnoent(err) {
|
|
90
|
+
return (typeof err === 'object'
|
|
91
|
+
&& err !== null
|
|
92
|
+
&& 'code' in err
|
|
93
|
+
&& err.code === 'ENOENT');
|
|
94
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ShortCodeIndex } from '@ggui-ai/mcp-server-core';
|
|
2
|
+
/**
|
|
3
|
+
* Construct the persistent ShortCodeIndex backed by `<persistentDir>/
|
|
4
|
+
* short-codes.sqlite`. Creates the parent dir if missing (the persistent
|
|
5
|
+
* dir always exists by the time this is called — the secrets helper
|
|
6
|
+
* already mkdir'd it — but mkdir is idempotent and cheap).
|
|
7
|
+
*
|
|
8
|
+
* Throws if `better-sqlite3` can't be loaded (e.g. native binding
|
|
9
|
+
* missing for the current platform). The CLI surfaces this as a fatal
|
|
10
|
+
* boot error so the operator can either install the binding or pass
|
|
11
|
+
* `--ephemeral`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createPersistentShortCodeIndex(persistentDir: string): Promise<ShortCodeIndex>;
|
|
14
|
+
//# sourceMappingURL=persistent-short-code-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistent-short-code-index.d.ts","sourceRoot":"","sources":["../src/persistent-short-code-index.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE/D;;;;;;;;;;GAUG;AACH,wBAAsB,8BAA8B,CAClD,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,cAAc,CAAC,CAQzB"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a persistent `ShortCodeIndex` for `ggui serve`.
|
|
3
|
+
*
|
|
4
|
+
* The OSS default for `shortCodeIndex` is `InMemoryShortCodeIndex` —
|
|
5
|
+
* the mapping lives in process memory and a restart drops every
|
|
6
|
+
* binding, so `/r/<code>` + `/api/bootstrap/<code>` 404 on the cached
|
|
7
|
+
* envelopes claude.ai stored before the restart.
|
|
8
|
+
*
|
|
9
|
+
* When the CLI is running in persistent mode (no `--ephemeral`), this
|
|
10
|
+
* helper constructs a `SqliteShortCodeIndex` against
|
|
11
|
+
* `<persistentDir>/short-codes.sqlite` so the same lookups survive
|
|
12
|
+
* restart. better-sqlite3 is dynamic-imported (same pattern as
|
|
13
|
+
* `@ggui-ai/mcp-server::resolveStorageFromConfig`) so consumers that
|
|
14
|
+
* stay ephemeral don't pay the N-API binding load on boot.
|
|
15
|
+
*
|
|
16
|
+
* Caller composes the path; this module is pure construction over a
|
|
17
|
+
* given dir + better-sqlite3 dynamic-import, so tests can scope to a
|
|
18
|
+
* tmpdir.
|
|
19
|
+
*/
|
|
20
|
+
import { mkdirSync } from 'node:fs';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
/**
|
|
23
|
+
* Construct the persistent ShortCodeIndex backed by `<persistentDir>/
|
|
24
|
+
* short-codes.sqlite`. Creates the parent dir if missing (the persistent
|
|
25
|
+
* dir always exists by the time this is called — the secrets helper
|
|
26
|
+
* already mkdir'd it — but mkdir is idempotent and cheap).
|
|
27
|
+
*
|
|
28
|
+
* Throws if `better-sqlite3` can't be loaded (e.g. native binding
|
|
29
|
+
* missing for the current platform). The CLI surfaces this as a fatal
|
|
30
|
+
* boot error so the operator can either install the binding or pass
|
|
31
|
+
* `--ephemeral`.
|
|
32
|
+
*/
|
|
33
|
+
export async function createPersistentShortCodeIndex(persistentDir) {
|
|
34
|
+
mkdirSync(persistentDir, { recursive: true });
|
|
35
|
+
const { SqliteShortCodeIndex } = await import('@ggui-ai/mcp-server-core/sqlite');
|
|
36
|
+
return new SqliteShortCodeIndex({
|
|
37
|
+
filename: join(persistentDir, 'short-codes.sqlite'),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SessionStore, VectorStore } from '@ggui-ai/mcp-server-core';
|
|
2
|
+
export declare function createPersistentSessionStore(persistentDir: string): Promise<SessionStore>;
|
|
3
|
+
export declare function createPersistentVectorStore(persistentDir: string): Promise<VectorStore>;
|
|
4
|
+
/**
|
|
5
|
+
* Compute the auto-default `keysFile` path. Returns `undefined` when
|
|
6
|
+
* the caller wants ephemeral pairing (the operator already passed
|
|
7
|
+
* `--keys-file` explicitly, or `--ephemeral` is on). The CLI uses
|
|
8
|
+
* this to honour explicit operator paths while still providing a
|
|
9
|
+
* survives-restart default in the common case.
|
|
10
|
+
*/
|
|
11
|
+
export declare function defaultKeysFile(persistentDir: string): string;
|
|
12
|
+
//# sourceMappingURL=persistent-stores.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistent-stores.d.ts","sourceRoot":"","sources":["../src/persistent-stores.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EACV,YAAY,EACZ,WAAW,EACZ,MAAM,0BAA0B,CAAC;AAElC,wBAAsB,4BAA4B,CAChD,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,YAAY,CAAC,CAQvB;AAED,wBAAsB,2BAA2B,CAC/C,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,WAAW,CAAC,CAQtB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE7D"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default-on persistent stores under `<persistentDir>/`. Used when the
|
|
3
|
+
* operator hasn't declared `storage.sessions` / `storage.vectors` in
|
|
4
|
+
* `ggui.json` AND hasn't passed `--ephemeral`. Closes the SessionStore
|
|
5
|
+
* leg of the rehydrate problem: without this, a restart drops every
|
|
6
|
+
* session row even though the HMAC secret + ShortCodeIndex now persist.
|
|
7
|
+
*
|
|
8
|
+
* Override precedence (caller enforces):
|
|
9
|
+
* 1. Explicit `ggui.json#storage.sessions` / `.vectors` (user wins)
|
|
10
|
+
* 2. These defaults (operator-friendly)
|
|
11
|
+
* 3. In-memory (`createGguiServer`'s internal fallback) (last resort)
|
|
12
|
+
*
|
|
13
|
+
* `better-sqlite3` is dynamic-imported (same pattern as the storage
|
|
14
|
+
* resolver in `@ggui-ai/mcp-server`) so `--ephemeral` runs don't pay
|
|
15
|
+
* the N-API binding load.
|
|
16
|
+
*/
|
|
17
|
+
import { mkdirSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
export async function createPersistentSessionStore(persistentDir) {
|
|
20
|
+
mkdirSync(persistentDir, { recursive: true });
|
|
21
|
+
const { SqliteSessionStore } = await import('@ggui-ai/mcp-server-core/sqlite');
|
|
22
|
+
return new SqliteSessionStore({
|
|
23
|
+
filename: join(persistentDir, 'sessions.sqlite'),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export async function createPersistentVectorStore(persistentDir) {
|
|
27
|
+
mkdirSync(persistentDir, { recursive: true });
|
|
28
|
+
const { SqliteVectorStore } = await import('@ggui-ai/mcp-server-core/sqlite');
|
|
29
|
+
return new SqliteVectorStore({
|
|
30
|
+
filename: join(persistentDir, 'vectors.sqlite'),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Compute the auto-default `keysFile` path. Returns `undefined` when
|
|
35
|
+
* the caller wants ephemeral pairing (the operator already passed
|
|
36
|
+
* `--keys-file` explicitly, or `--ephemeral` is on). The CLI uses
|
|
37
|
+
* this to honour explicit operator paths while still providing a
|
|
38
|
+
* survives-restart default in the common case.
|
|
39
|
+
*/
|
|
40
|
+
export function defaultKeysFile(persistentDir) {
|
|
41
|
+
return join(persistentDir, 'keys.json');
|
|
42
|
+
}
|