@gtc6244/immediacy-daemon 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/actions.d.ts +22 -0
- package/dist/src/actions.js +44 -0
- package/dist/src/actions.js.map +1 -0
- package/dist/src/agent/agent.d.ts +89 -0
- package/dist/src/agent/agent.js +22 -0
- package/dist/src/agent/agent.js.map +1 -0
- package/dist/src/agent/claude.d.ts +70 -0
- package/dist/src/agent/claude.js +404 -0
- package/dist/src/agent/claude.js.map +1 -0
- package/dist/src/agent/codex.d.ts +44 -0
- package/dist/src/agent/codex.js +303 -0
- package/dist/src/agent/codex.js.map +1 -0
- package/dist/src/agent/mock.d.ts +31 -0
- package/dist/src/agent/mock.js +155 -0
- package/dist/src/agent/mock.js.map +1 -0
- package/dist/src/agent/model-error.d.ts +8 -0
- package/dist/src/agent/model-error.js +25 -0
- package/dist/src/agent/model-error.js.map +1 -0
- package/dist/src/agent/openrouter.d.ts +38 -0
- package/dist/src/agent/openrouter.js +60 -0
- package/dist/src/agent/openrouter.js.map +1 -0
- package/dist/src/agent/workspace-scope.d.ts +25 -0
- package/dist/src/agent/workspace-scope.js +71 -0
- package/dist/src/agent/workspace-scope.js.map +1 -0
- package/dist/src/allowlist.d.ts +56 -0
- package/dist/src/allowlist.js +141 -0
- package/dist/src/allowlist.js.map +1 -0
- package/dist/src/chatlog.d.ts +50 -0
- package/dist/src/chatlog.js +179 -0
- package/dist/src/chatlog.js.map +1 -0
- package/dist/src/cli/paths.d.ts +40 -0
- package/dist/src/cli/paths.js +76 -0
- package/dist/src/cli/paths.js.map +1 -0
- package/dist/src/cli/service.d.ts +20 -0
- package/dist/src/cli/service.js +245 -0
- package/dist/src/cli/service.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +227 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config.d.ts +33 -0
- package/dist/src/config.js +122 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/credentials.d.ts +38 -0
- package/dist/src/credentials.js +120 -0
- package/dist/src/credentials.js.map +1 -0
- package/dist/src/crypto.d.ts +49 -0
- package/dist/src/crypto.js +202 -0
- package/dist/src/crypto.js.map +1 -0
- package/dist/src/daemon-identity.d.ts +31 -0
- package/dist/src/daemon-identity.js +97 -0
- package/dist/src/daemon-identity.js.map +1 -0
- package/dist/src/envelope.d.ts +14 -0
- package/dist/src/envelope.js +28 -0
- package/dist/src/envelope.js.map +1 -0
- package/dist/src/events.d.ts +40 -0
- package/dist/src/events.js +24 -0
- package/dist/src/events.js.map +1 -0
- package/dist/src/forward.d.ts +51 -0
- package/dist/src/forward.js +170 -0
- package/dist/src/forward.js.map +1 -0
- package/dist/src/fs-scope.d.ts +17 -0
- package/dist/src/fs-scope.js +47 -0
- package/dist/src/fs-scope.js.map +1 -0
- package/dist/src/github.d.ts +70 -0
- package/dist/src/github.js +178 -0
- package/dist/src/github.js.map +1 -0
- package/dist/src/gitops.d.ts +113 -0
- package/dist/src/gitops.js +557 -0
- package/dist/src/gitops.js.map +1 -0
- package/dist/src/hub.d.ts +50 -0
- package/dist/src/hub.js +92 -0
- package/dist/src/hub.js.map +1 -0
- package/dist/src/index.d.ts +22 -0
- package/dist/src/index.js +56 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/jobs/scheduler.d.ts +94 -0
- package/dist/src/jobs/scheduler.js +373 -0
- package/dist/src/jobs/scheduler.js.map +1 -0
- package/dist/src/jobs/store.d.ts +33 -0
- package/dist/src/jobs/store.js +111 -0
- package/dist/src/jobs/store.js.map +1 -0
- package/dist/src/log.d.ts +61 -0
- package/dist/src/log.js +166 -0
- package/dist/src/log.js.map +1 -0
- package/dist/src/migrate-state-dir.d.ts +1 -0
- package/dist/src/migrate-state-dir.js +47 -0
- package/dist/src/migrate-state-dir.js.map +1 -0
- package/dist/src/models.d.ts +36 -0
- package/dist/src/models.js +71 -0
- package/dist/src/models.js.map +1 -0
- package/dist/src/permission-gate.d.ts +48 -0
- package/dist/src/permission-gate.js +93 -0
- package/dist/src/permission-gate.js.map +1 -0
- package/dist/src/permission-scope.d.ts +77 -0
- package/dist/src/permission-scope.js +244 -0
- package/dist/src/permission-scope.js.map +1 -0
- package/dist/src/ports.d.ts +71 -0
- package/dist/src/ports.js +216 -0
- package/dist/src/ports.js.map +1 -0
- package/dist/src/projects.d.ts +83 -0
- package/dist/src/projects.js +205 -0
- package/dist/src/projects.js.map +1 -0
- package/dist/src/protocol.d.ts +1226 -0
- package/dist/src/protocol.js +16 -0
- package/dist/src/protocol.js.map +1 -0
- package/dist/src/repo-sync.d.ts +49 -0
- package/dist/src/repo-sync.js +83 -0
- package/dist/src/repo-sync.js.map +1 -0
- package/dist/src/server.d.ts +33 -0
- package/dist/src/server.js +973 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/terminal.d.ts +32 -0
- package/dist/src/terminal.js +92 -0
- package/dist/src/terminal.js.map +1 -0
- package/dist/src/titler.d.ts +15 -0
- package/dist/src/titler.js +72 -0
- package/dist/src/titler.js.map +1 -0
- package/dist/src/types.d.ts +99 -0
- package/dist/src/types.js +3 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/usage-limit.d.ts +49 -0
- package/dist/src/usage-limit.js +83 -0
- package/dist/src/usage-limit.js.map +1 -0
- package/dist/src/usage.d.ts +23 -0
- package/dist/src/usage.js +63 -0
- package/dist/src/usage.js.map +1 -0
- package/dist/src/workspace.d.ts +303 -0
- package/dist/src/workspace.js +1086 -0
- package/dist/src/workspace.js.map +1 -0
- package/dist/src/worktree.d.ts +133 -0
- package/dist/src/worktree.js +561 -0
- package/dist/src/worktree.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AppConfigStore — daemon-owned app configuration (prompts + git behavior).
|
|
3
|
+
*
|
|
4
|
+
* Holds the editable {@link AppConfig} the app surfaces in Settings: the custom
|
|
5
|
+
* prompt text injected into agent sessions/actions, and the git lifecycle
|
|
6
|
+
* behavior for workspaces. Persisted to `~/.immediacy/config.json`.
|
|
7
|
+
*
|
|
8
|
+
* Unlike credentials.ts this carries no secret material, so the file is written
|
|
9
|
+
* world-readable (0644) rather than 0600 — but it keeps the same in-memory
|
|
10
|
+
* cache + serialized atomic-rename persist() so concurrent set() calls can't
|
|
11
|
+
* tear the file. Clients read it via `settings.get` → `settings.result` and
|
|
12
|
+
* replace it wholesale via `config.set`; every change is broadcast so all
|
|
13
|
+
* clients converge (see server.ts).
|
|
14
|
+
*/
|
|
15
|
+
import type { AppConfig } from "./protocol.js";
|
|
16
|
+
/** First-run config: empty prompts, and git defaults that match today's
|
|
17
|
+
* hardcoded behavior (auto -u on push, an `immediacy/` custom prefix). */
|
|
18
|
+
export declare const DEFAULT_CONFIG: AppConfig;
|
|
19
|
+
/** Coerce arbitrary (untrusted / partial) input into a valid AppConfig, filling
|
|
20
|
+
* any missing or malformed field from {@link DEFAULT_CONFIG}. Never throws. */
|
|
21
|
+
export declare function sanitizeConfig(input: unknown): AppConfig;
|
|
22
|
+
export declare class AppConfigStore {
|
|
23
|
+
private config;
|
|
24
|
+
/** Serializes writes so two concurrent set() calls can't interleave. */
|
|
25
|
+
private writeChain;
|
|
26
|
+
load(): Promise<void>;
|
|
27
|
+
/** The current config (defensive: callers get the live object; treat as
|
|
28
|
+
* read-only — mutate only through set()). */
|
|
29
|
+
get(): AppConfig;
|
|
30
|
+
/** Replace the config wholesale, sanitizing untrusted input, then persist. */
|
|
31
|
+
set(input: unknown): Promise<AppConfig>;
|
|
32
|
+
private persist;
|
|
33
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AppConfigStore — daemon-owned app configuration (prompts + git behavior).
|
|
3
|
+
*
|
|
4
|
+
* Holds the editable {@link AppConfig} the app surfaces in Settings: the custom
|
|
5
|
+
* prompt text injected into agent sessions/actions, and the git lifecycle
|
|
6
|
+
* behavior for workspaces. Persisted to `~/.immediacy/config.json`.
|
|
7
|
+
*
|
|
8
|
+
* Unlike credentials.ts this carries no secret material, so the file is written
|
|
9
|
+
* world-readable (0644) rather than 0600 — but it keeps the same in-memory
|
|
10
|
+
* cache + serialized atomic-rename persist() so concurrent set() calls can't
|
|
11
|
+
* tear the file. Clients read it via `settings.get` → `settings.result` and
|
|
12
|
+
* replace it wholesale via `config.set`; every change is broadcast so all
|
|
13
|
+
* clients converge (see server.ts).
|
|
14
|
+
*/
|
|
15
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
const CONFIG_FILE = process.env.CONFIG_FILE ?? join(homedir(), ".immediacy", "config.json");
|
|
19
|
+
/** A prompt/prefix a malicious client can't blow up to a multi-MB blob on disk. */
|
|
20
|
+
const MAX_PROMPT_LEN = 8192;
|
|
21
|
+
const MAX_PREFIX_LEN = 128;
|
|
22
|
+
const BRANCH_PREFIX_TYPES = ["none", "username", "custom"];
|
|
23
|
+
/** First-run config: empty prompts, and git defaults that match today's
|
|
24
|
+
* hardcoded behavior (auto -u on push, an `immediacy/` custom prefix). */
|
|
25
|
+
export const DEFAULT_CONFIG = {
|
|
26
|
+
prompts: {
|
|
27
|
+
general: "",
|
|
28
|
+
codeReview: "",
|
|
29
|
+
createPr: "",
|
|
30
|
+
fixErrors: "",
|
|
31
|
+
resolveMergeConflicts: "",
|
|
32
|
+
renameBranch: "",
|
|
33
|
+
},
|
|
34
|
+
git: {
|
|
35
|
+
deleteBranchOnArchive: false,
|
|
36
|
+
archiveOnMerge: false,
|
|
37
|
+
worktreePushAutoSetupRemote: true,
|
|
38
|
+
branchPrefixType: "custom",
|
|
39
|
+
branchPrefix: "immediacy/",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
function str(v, fallback, max) {
|
|
43
|
+
if (typeof v !== "string")
|
|
44
|
+
return fallback;
|
|
45
|
+
// Trim only the tail cap; keep interior whitespace (prompts are multi-line).
|
|
46
|
+
return v.length > max ? v.slice(0, max) : v;
|
|
47
|
+
}
|
|
48
|
+
function bool(v, fallback) {
|
|
49
|
+
return typeof v === "boolean" ? v : fallback;
|
|
50
|
+
}
|
|
51
|
+
function prefixType(v) {
|
|
52
|
+
return BRANCH_PREFIX_TYPES.includes(v)
|
|
53
|
+
? v
|
|
54
|
+
: DEFAULT_CONFIG.git.branchPrefixType;
|
|
55
|
+
}
|
|
56
|
+
/** Coerce arbitrary (untrusted / partial) input into a valid AppConfig, filling
|
|
57
|
+
* any missing or malformed field from {@link DEFAULT_CONFIG}. Never throws. */
|
|
58
|
+
export function sanitizeConfig(input) {
|
|
59
|
+
const obj = (input ?? {});
|
|
60
|
+
const p = (obj.prompts ?? {});
|
|
61
|
+
const g = (obj.git ?? {});
|
|
62
|
+
const D = DEFAULT_CONFIG;
|
|
63
|
+
return {
|
|
64
|
+
prompts: {
|
|
65
|
+
general: str(p.general, D.prompts.general, MAX_PROMPT_LEN),
|
|
66
|
+
codeReview: str(p.codeReview, D.prompts.codeReview, MAX_PROMPT_LEN),
|
|
67
|
+
createPr: str(p.createPr, D.prompts.createPr, MAX_PROMPT_LEN),
|
|
68
|
+
fixErrors: str(p.fixErrors, D.prompts.fixErrors, MAX_PROMPT_LEN),
|
|
69
|
+
resolveMergeConflicts: str(p.resolveMergeConflicts, D.prompts.resolveMergeConflicts, MAX_PROMPT_LEN),
|
|
70
|
+
renameBranch: str(p.renameBranch, D.prompts.renameBranch, MAX_PROMPT_LEN),
|
|
71
|
+
},
|
|
72
|
+
git: {
|
|
73
|
+
deleteBranchOnArchive: bool(g.deleteBranchOnArchive, D.git.deleteBranchOnArchive),
|
|
74
|
+
archiveOnMerge: bool(g.archiveOnMerge, D.git.archiveOnMerge),
|
|
75
|
+
worktreePushAutoSetupRemote: bool(g.worktreePushAutoSetupRemote, D.git.worktreePushAutoSetupRemote),
|
|
76
|
+
branchPrefixType: prefixType(g.branchPrefixType),
|
|
77
|
+
branchPrefix: str(g.branchPrefix, D.git.branchPrefix, MAX_PREFIX_LEN),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export class AppConfigStore {
|
|
82
|
+
config = sanitizeConfig(DEFAULT_CONFIG);
|
|
83
|
+
/** Serializes writes so two concurrent set() calls can't interleave. */
|
|
84
|
+
writeChain = Promise.resolve();
|
|
85
|
+
async load() {
|
|
86
|
+
try {
|
|
87
|
+
const raw = await readFile(CONFIG_FILE, "utf8");
|
|
88
|
+
this.config = sanitizeConfig(JSON.parse(raw));
|
|
89
|
+
console.log("[config] restored app config");
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
/* no prior state — keep defaults */
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** The current config (defensive: callers get the live object; treat as
|
|
96
|
+
* read-only — mutate only through set()). */
|
|
97
|
+
get() {
|
|
98
|
+
return this.config;
|
|
99
|
+
}
|
|
100
|
+
/** Replace the config wholesale, sanitizing untrusted input, then persist. */
|
|
101
|
+
async set(input) {
|
|
102
|
+
this.config = sanitizeConfig(input);
|
|
103
|
+
await this.persist();
|
|
104
|
+
return this.config;
|
|
105
|
+
}
|
|
106
|
+
persist() {
|
|
107
|
+
// Snapshot synchronously so a later set() can't race this write, then chain
|
|
108
|
+
// onto any in-flight write so concurrent set() calls serialize instead of
|
|
109
|
+
// interleaving (mirrors credentials.ts). No secrets here → default 0644.
|
|
110
|
+
const snapshot = JSON.stringify(this.config, null, 2);
|
|
111
|
+
this.writeChain = this.writeChain
|
|
112
|
+
.catch(() => { })
|
|
113
|
+
.then(async () => {
|
|
114
|
+
await mkdir(dirname(CONFIG_FILE), { recursive: true });
|
|
115
|
+
const tmp = `${CONFIG_FILE}.${process.pid}.tmp`;
|
|
116
|
+
await writeFile(tmp, snapshot, "utf8");
|
|
117
|
+
await rename(tmp, CONFIG_FILE);
|
|
118
|
+
});
|
|
119
|
+
return this.writeChain;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CredentialsStore — provider API keys stored on the daemon.
|
|
3
|
+
*
|
|
4
|
+
* Holds the OpenRouter / Anthropic / OpenAI API keys the agent backends bill
|
|
5
|
+
* against, persisted to `~/.immediacy/credentials.json` (chmod 0600, like the
|
|
6
|
+
* NaCl keypair in crypto.ts — never world-readable). The macOS client manages
|
|
7
|
+
* them over the wire (`settings.set`); every client can read the presence
|
|
8
|
+
* indicator (`settings.get` → `settings.result`), which carries no raw key.
|
|
9
|
+
*
|
|
10
|
+
* Precedence when resolving a key for a session: the stored value wins, and the
|
|
11
|
+
* matching environment variable (ANTHROPIC_API_KEY / OPENAI_API_KEY /
|
|
12
|
+
* OPENROUTER_API_KEY) is the fallback — so a daemon launched with env-configured
|
|
13
|
+
* keys keeps working, and a key typed into the app overrides it.
|
|
14
|
+
*
|
|
15
|
+
* SSO fallback: Anthropic and OpenAI have no stored/env key → sessions use the
|
|
16
|
+
* CLI/SDK's own cached subscription login (Claude Code login / Codex ChatGPT
|
|
17
|
+
* login). OpenRouter has no login concept, so with no key it is simply unset.
|
|
18
|
+
*/
|
|
19
|
+
import type { CredentialInfo, CredentialProvider } from "./protocol.js";
|
|
20
|
+
export declare class CredentialsStore {
|
|
21
|
+
private stored;
|
|
22
|
+
/** Serializes writes so two concurrent setKey() calls can't interleave. */
|
|
23
|
+
private writeChain;
|
|
24
|
+
load(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* The effective API key for a provider: the stored value, else the env
|
|
27
|
+
* fallback. Empty string is treated as unset. Undefined means "no key — use
|
|
28
|
+
* the SSO/login path (or leave unset for OpenRouter)".
|
|
29
|
+
*/
|
|
30
|
+
getKey(provider: CredentialProvider): string | undefined;
|
|
31
|
+
/** Set (non-empty) or clear (empty) a provider's stored key, then persist. */
|
|
32
|
+
setKey(provider: CredentialProvider, apiKey: string): Promise<void>;
|
|
33
|
+
/** Per-provider status for `settings.result` — never carries a raw key. */
|
|
34
|
+
status(): CredentialInfo[];
|
|
35
|
+
/** True when `p` is a known provider (guards `settings.set` input). */
|
|
36
|
+
static isProvider(p: unknown): p is CredentialProvider;
|
|
37
|
+
private persist;
|
|
38
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CredentialsStore — provider API keys stored on the daemon.
|
|
3
|
+
*
|
|
4
|
+
* Holds the OpenRouter / Anthropic / OpenAI API keys the agent backends bill
|
|
5
|
+
* against, persisted to `~/.immediacy/credentials.json` (chmod 0600, like the
|
|
6
|
+
* NaCl keypair in crypto.ts — never world-readable). The macOS client manages
|
|
7
|
+
* them over the wire (`settings.set`); every client can read the presence
|
|
8
|
+
* indicator (`settings.get` → `settings.result`), which carries no raw key.
|
|
9
|
+
*
|
|
10
|
+
* Precedence when resolving a key for a session: the stored value wins, and the
|
|
11
|
+
* matching environment variable (ANTHROPIC_API_KEY / OPENAI_API_KEY /
|
|
12
|
+
* OPENROUTER_API_KEY) is the fallback — so a daemon launched with env-configured
|
|
13
|
+
* keys keeps working, and a key typed into the app overrides it.
|
|
14
|
+
*
|
|
15
|
+
* SSO fallback: Anthropic and OpenAI have no stored/env key → sessions use the
|
|
16
|
+
* CLI/SDK's own cached subscription login (Claude Code login / Codex ChatGPT
|
|
17
|
+
* login). OpenRouter has no login concept, so with no key it is simply unset.
|
|
18
|
+
*/
|
|
19
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
const CREDENTIALS_FILE = process.env.CREDENTIALS_FILE ??
|
|
23
|
+
join(homedir(), ".immediacy", "credentials.json");
|
|
24
|
+
const PROVIDERS = ["anthropic", "openai", "openrouter"];
|
|
25
|
+
/** The env var that seeds each provider when no key is stored. */
|
|
26
|
+
const ENV_VAR = {
|
|
27
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
28
|
+
openai: "OPENAI_API_KEY",
|
|
29
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
30
|
+
};
|
|
31
|
+
/** Providers that can fall back to a subscription/CLI login when no key is set. */
|
|
32
|
+
const HAS_SSO_FALLBACK = {
|
|
33
|
+
anthropic: true,
|
|
34
|
+
openai: true,
|
|
35
|
+
openrouter: false,
|
|
36
|
+
};
|
|
37
|
+
/** Last 4 chars behind an ellipsis, e.g. "…6f2a"; a bare "…" for a short/absent
|
|
38
|
+
* key so a key of 4 or fewer chars is never shown in full. */
|
|
39
|
+
function mask(key) {
|
|
40
|
+
if (!key)
|
|
41
|
+
return null;
|
|
42
|
+
return key.length > 4 ? `…${key.slice(-4)}` : "…";
|
|
43
|
+
}
|
|
44
|
+
export class CredentialsStore {
|
|
45
|
+
stored = {};
|
|
46
|
+
/** Serializes writes so two concurrent setKey() calls can't interleave. */
|
|
47
|
+
writeChain = Promise.resolve();
|
|
48
|
+
async load() {
|
|
49
|
+
try {
|
|
50
|
+
const raw = await readFile(CREDENTIALS_FILE, "utf8");
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
for (const p of PROVIDERS) {
|
|
53
|
+
const v = parsed[p];
|
|
54
|
+
if (typeof v === "string" && v.trim())
|
|
55
|
+
this.stored[p] = v;
|
|
56
|
+
}
|
|
57
|
+
console.log(`[credentials] restored ${Object.keys(this.stored).length} key(s)`);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* no prior state */
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The effective API key for a provider: the stored value, else the env
|
|
65
|
+
* fallback. Empty string is treated as unset. Undefined means "no key — use
|
|
66
|
+
* the SSO/login path (or leave unset for OpenRouter)".
|
|
67
|
+
*/
|
|
68
|
+
getKey(provider) {
|
|
69
|
+
const stored = this.stored[provider]?.trim();
|
|
70
|
+
if (stored)
|
|
71
|
+
return stored;
|
|
72
|
+
const env = process.env[ENV_VAR[provider]]?.trim();
|
|
73
|
+
return env || undefined;
|
|
74
|
+
}
|
|
75
|
+
/** Set (non-empty) or clear (empty) a provider's stored key, then persist. */
|
|
76
|
+
async setKey(provider, apiKey) {
|
|
77
|
+
const trimmed = apiKey.trim();
|
|
78
|
+
if (trimmed)
|
|
79
|
+
this.stored[provider] = trimmed;
|
|
80
|
+
else
|
|
81
|
+
delete this.stored[provider];
|
|
82
|
+
await this.persist();
|
|
83
|
+
}
|
|
84
|
+
/** Per-provider status for `settings.result` — never carries a raw key. */
|
|
85
|
+
status() {
|
|
86
|
+
return PROVIDERS.map((provider) => {
|
|
87
|
+
const key = this.getKey(provider);
|
|
88
|
+
const hasKey = Boolean(key);
|
|
89
|
+
const source = hasKey
|
|
90
|
+
? "apikey"
|
|
91
|
+
: HAS_SSO_FALLBACK[provider]
|
|
92
|
+
? "sso"
|
|
93
|
+
: "none";
|
|
94
|
+
return { provider, source, hasKey, maskedKey: mask(key) };
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/** True when `p` is a known provider (guards `settings.set` input). */
|
|
98
|
+
static isProvider(p) {
|
|
99
|
+
return typeof p === "string" && PROVIDERS.includes(p);
|
|
100
|
+
}
|
|
101
|
+
persist() {
|
|
102
|
+
// Snapshot synchronously so a later mutation can't race this write, then
|
|
103
|
+
// chain onto any in-flight write so concurrent setKey() calls serialize
|
|
104
|
+
// instead of interleaving (a torn write would silently drop every key on
|
|
105
|
+
// the next load()). Write to a temp file created 0600 — never world-readable,
|
|
106
|
+
// even for the instant before a chmod — then atomically rename into place
|
|
107
|
+
// (secret-bearing, mirrors crypto.ts keys.json).
|
|
108
|
+
const snapshot = JSON.stringify(this.stored, null, 2);
|
|
109
|
+
this.writeChain = this.writeChain
|
|
110
|
+
.catch(() => { })
|
|
111
|
+
.then(async () => {
|
|
112
|
+
await mkdir(dirname(CREDENTIALS_FILE), { recursive: true });
|
|
113
|
+
const tmp = `${CREDENTIALS_FILE}.${process.pid}.tmp`;
|
|
114
|
+
await writeFile(tmp, snapshot, { mode: 0o600 });
|
|
115
|
+
await rename(tmp, CREDENTIALS_FILE);
|
|
116
|
+
});
|
|
117
|
+
return this.writeChain;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/credentials.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E crypto (slice 5) — keep the Cloudflare edge blind.
|
|
3
|
+
*
|
|
4
|
+
* Cloudflare terminates TLS and can see plaintext. We additionally seal the
|
|
5
|
+
* envelope `payload` with crypto_box (X25519 + XSalsa20-Poly1305 — NaCl box),
|
|
6
|
+
* wire-compatible with libsodium's `crypto_box_easy` on the Flutter side
|
|
7
|
+
* (sodium_libs). The outer fields (v, id, ts, ws, type) stay clear for routing.
|
|
8
|
+
*
|
|
9
|
+
* On the wire an encrypted payload is: { n: base64(nonce), c: base64(cipher) }.
|
|
10
|
+
*
|
|
11
|
+
* This is IN ADDITION to Cloudflare Access — Access does authn/authz at the
|
|
12
|
+
* edge; the box keeps payload contents private from the edge.
|
|
13
|
+
*/
|
|
14
|
+
import type { Envelope } from "./protocol.js";
|
|
15
|
+
export interface Keypair {
|
|
16
|
+
publicKey: Buffer;
|
|
17
|
+
secretKey: Buffer;
|
|
18
|
+
}
|
|
19
|
+
/** Load the daemon keypair, generating + persisting one on first run. */
|
|
20
|
+
export declare function loadOrCreateKeypair(): Promise<Keypair>;
|
|
21
|
+
/** One paired client. `pub` is base64 of its X25519 public key. */
|
|
22
|
+
export interface Peer {
|
|
23
|
+
pub: string;
|
|
24
|
+
addedAt: number;
|
|
25
|
+
label?: string;
|
|
26
|
+
}
|
|
27
|
+
/** True iff `b64` is canonical base64 of a full-length crypto_box public key. */
|
|
28
|
+
export declare function isValidPublicKeyB64(b64: string): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Every paired client. Reads `peers.json`, migrating a legacy single-peer
|
|
31
|
+
* `peer.json` (and honoring the PEER_PUBLIC_KEY dev override) on first read so
|
|
32
|
+
* an existing pairing keeps working after the multi-device upgrade.
|
|
33
|
+
*/
|
|
34
|
+
export declare function loadPeers(): Promise<Peer[]>;
|
|
35
|
+
/**
|
|
36
|
+
* Add a client public key to the paired set (idempotent) and persist. Returns
|
|
37
|
+
* the updated list. Trust-on-first-use: the caller has already cleared the
|
|
38
|
+
* Cloudflare Access gate, which is the shared pairing secret.
|
|
39
|
+
*/
|
|
40
|
+
export declare function addPeer(pub: string, label?: string): Promise<Peer[]>;
|
|
41
|
+
/** Codec abstraction so the server/hub don't care whether crypto is on. */
|
|
42
|
+
export interface Codec {
|
|
43
|
+
encode(env: Envelope): string;
|
|
44
|
+
decode(raw: string): Envelope;
|
|
45
|
+
}
|
|
46
|
+
/** Cleartext codec (slices 1–4 default, and local dev). */
|
|
47
|
+
export declare const clearCodec: Codec;
|
|
48
|
+
/** NaCl-box codec: seals/opens only the `payload` field. */
|
|
49
|
+
export declare function boxCodec(self: Keypair, peerPublicKey: Buffer): Codec;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E crypto (slice 5) — keep the Cloudflare edge blind.
|
|
3
|
+
*
|
|
4
|
+
* Cloudflare terminates TLS and can see plaintext. We additionally seal the
|
|
5
|
+
* envelope `payload` with crypto_box (X25519 + XSalsa20-Poly1305 — NaCl box),
|
|
6
|
+
* wire-compatible with libsodium's `crypto_box_easy` on the Flutter side
|
|
7
|
+
* (sodium_libs). The outer fields (v, id, ts, ws, type) stay clear for routing.
|
|
8
|
+
*
|
|
9
|
+
* On the wire an encrypted payload is: { n: base64(nonce), c: base64(cipher) }.
|
|
10
|
+
*
|
|
11
|
+
* This is IN ADDITION to Cloudflare Access — Access does authn/authz at the
|
|
12
|
+
* edge; the box keeps payload contents private from the edge.
|
|
13
|
+
*/
|
|
14
|
+
import sodium from "sodium-native";
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import { mkdir, readFile, writeFile, chmod } from "node:fs/promises";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
20
|
+
import { ProtocolError } from "./envelope.js";
|
|
21
|
+
const KEYS_FILE = process.env.KEYS_FILE ?? join(homedir(), ".immediacy", "keys.json");
|
|
22
|
+
const PEER_FILE = process.env.PEER_FILE ?? join(homedir(), ".immediacy", "peer.json");
|
|
23
|
+
// Multi-device pairing: the set of every client public key we've paired with,
|
|
24
|
+
// so encryption isn't pinned to one device (the old single PEER_FILE). Each
|
|
25
|
+
// entry is bound to its own socket at connect time (per-socket sealing).
|
|
26
|
+
const PEERS_FILE = process.env.PEERS_FILE ?? join(homedir(), ".immediacy", "peers.json");
|
|
27
|
+
/** Load the daemon keypair, generating + persisting one on first run. */
|
|
28
|
+
export async function loadOrCreateKeypair() {
|
|
29
|
+
if (existsSync(KEYS_FILE)) {
|
|
30
|
+
const j = JSON.parse(await readFile(KEYS_FILE, "utf8"));
|
|
31
|
+
return {
|
|
32
|
+
publicKey: Buffer.from(j.publicKey, "base64"),
|
|
33
|
+
secretKey: Buffer.from(j.secretKey, "base64"),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const publicKey = Buffer.alloc(sodium.crypto_box_PUBLICKEYBYTES);
|
|
37
|
+
const secretKey = Buffer.alloc(sodium.crypto_box_SECRETKEYBYTES);
|
|
38
|
+
sodium.crypto_box_keypair(publicKey, secretKey);
|
|
39
|
+
await mkdir(dirname(KEYS_FILE), { recursive: true });
|
|
40
|
+
await writeFile(KEYS_FILE, JSON.stringify({
|
|
41
|
+
publicKey: publicKey.toString("base64"),
|
|
42
|
+
secretKey: secretKey.toString("base64"),
|
|
43
|
+
}), "utf8");
|
|
44
|
+
await chmod(KEYS_FILE, 0o600); // secret key — never world-readable
|
|
45
|
+
return { publicKey, secretKey };
|
|
46
|
+
}
|
|
47
|
+
/** True iff `b64` is canonical base64 of a full-length crypto_box public key. */
|
|
48
|
+
export function isValidPublicKeyB64(b64) {
|
|
49
|
+
// Node's Buffer.from(x, "base64") never throws and silently drops invalid
|
|
50
|
+
// characters, so a decoded-length check alone lets malformed input through.
|
|
51
|
+
// Require a strict base64 alphabet up front, then confirm the value
|
|
52
|
+
// round-trips to the exact same canonical string at the expected length.
|
|
53
|
+
if (typeof b64 !== "string" || !/^[A-Za-z0-9+/]+={0,2}$/.test(b64))
|
|
54
|
+
return false;
|
|
55
|
+
const buf = Buffer.from(b64, "base64");
|
|
56
|
+
return (buf.length === sodium.crypto_box_PUBLICKEYBYTES && buf.toString("base64") === b64);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Every paired client. Reads `peers.json`, migrating a legacy single-peer
|
|
60
|
+
* `peer.json` (and honoring the PEER_PUBLIC_KEY dev override) on first read so
|
|
61
|
+
* an existing pairing keeps working after the multi-device upgrade.
|
|
62
|
+
*/
|
|
63
|
+
export async function loadPeers() {
|
|
64
|
+
const peers = [];
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
const push = (pub, label) => {
|
|
67
|
+
if (!pub || seen.has(pub) || !isValidPublicKeyB64(pub))
|
|
68
|
+
return;
|
|
69
|
+
seen.add(pub);
|
|
70
|
+
peers.push({ pub, addedAt: Date.now(), label });
|
|
71
|
+
};
|
|
72
|
+
if (existsSync(PEERS_FILE)) {
|
|
73
|
+
try {
|
|
74
|
+
const j = JSON.parse(await readFile(PEERS_FILE, "utf8"));
|
|
75
|
+
for (const p of j.peers ?? []) {
|
|
76
|
+
// Validate on the way in too: a corrupted or hand-edited peers.json must
|
|
77
|
+
// not seed invalid keys into the paired set (matches the push() helper).
|
|
78
|
+
if (p && typeof p.pub === "string" && !seen.has(p.pub) && isValidPublicKeyB64(p.pub)) {
|
|
79
|
+
seen.add(p.pub);
|
|
80
|
+
peers.push({ pub: p.pub, addedAt: p.addedAt ?? Date.now(), label: p.label });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
/* corrupt file — fall through to migration/env seeds */
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (existsSync(PEER_FILE)) {
|
|
89
|
+
// Migrate the legacy single-peer file.
|
|
90
|
+
try {
|
|
91
|
+
const j = JSON.parse(await readFile(PEER_FILE, "utf8"));
|
|
92
|
+
if (j.publicKey)
|
|
93
|
+
push(j.publicKey, "migrated");
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
/* ignore */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (process.env.PEER_PUBLIC_KEY)
|
|
100
|
+
push(process.env.PEER_PUBLIC_KEY, "env");
|
|
101
|
+
return peers;
|
|
102
|
+
}
|
|
103
|
+
// Serializes the peers.json read-modify-write so two devices pairing at the
|
|
104
|
+
// same time can't each read the old file and clobber the other's entry.
|
|
105
|
+
let addChain = Promise.resolve([]);
|
|
106
|
+
/**
|
|
107
|
+
* Add a client public key to the paired set (idempotent) and persist. Returns
|
|
108
|
+
* the updated list. Trust-on-first-use: the caller has already cleared the
|
|
109
|
+
* Cloudflare Access gate, which is the shared pairing secret.
|
|
110
|
+
*/
|
|
111
|
+
export function addPeer(pub, label) {
|
|
112
|
+
addChain = addChain.then(async () => {
|
|
113
|
+
const peers = await loadPeers();
|
|
114
|
+
if (!peers.some((p) => p.pub === pub)) {
|
|
115
|
+
peers.push({ pub, addedAt: Date.now(), label });
|
|
116
|
+
await mkdir(dirname(PEERS_FILE), { recursive: true });
|
|
117
|
+
await writeFile(PEERS_FILE, JSON.stringify({ peers }, null, 2), "utf8");
|
|
118
|
+
await chmod(PEERS_FILE, 0o600);
|
|
119
|
+
}
|
|
120
|
+
return peers;
|
|
121
|
+
});
|
|
122
|
+
return addChain;
|
|
123
|
+
}
|
|
124
|
+
/** Upper bound for one sealed payload — matches the largest legitimate frame
|
|
125
|
+
* (file contents are truncated daemon-side well below this). */
|
|
126
|
+
const MAX_SEALED_BYTES = 16 * 1024 * 1024;
|
|
127
|
+
/** Cleartext codec (slices 1–4 default, and local dev). */
|
|
128
|
+
export const clearCodec = {
|
|
129
|
+
encode: (env) => JSON.stringify(env),
|
|
130
|
+
decode: (raw) => parseEnvelope(raw),
|
|
131
|
+
};
|
|
132
|
+
/** NaCl-box codec: seals/opens only the `payload` field. */
|
|
133
|
+
export function boxCodec(self, peerPublicKey) {
|
|
134
|
+
return {
|
|
135
|
+
encode(env) {
|
|
136
|
+
const plaintext = Buffer.from(JSON.stringify(env.payload), "utf8");
|
|
137
|
+
const nonce = Buffer.alloc(sodium.crypto_box_NONCEBYTES);
|
|
138
|
+
sodium.randombytes_buf(nonce);
|
|
139
|
+
const cipher = Buffer.alloc(plaintext.length + sodium.crypto_box_MACBYTES);
|
|
140
|
+
sodium.crypto_box_easy(cipher, plaintext, nonce, peerPublicKey, self.secretKey);
|
|
141
|
+
const sealed = {
|
|
142
|
+
...env,
|
|
143
|
+
payload: { n: nonce.toString("base64"), c: cipher.toString("base64") },
|
|
144
|
+
};
|
|
145
|
+
return JSON.stringify(sealed);
|
|
146
|
+
},
|
|
147
|
+
decode(raw) {
|
|
148
|
+
const env = parseEnvelope(raw, /*allowSealed*/ true);
|
|
149
|
+
const sealed = env.payload;
|
|
150
|
+
if (typeof sealed.n !== "string" || typeof sealed.c !== "string") {
|
|
151
|
+
throw new ProtocolError("not_sealed", "payload is not a sealed box");
|
|
152
|
+
}
|
|
153
|
+
const nonce = Buffer.from(sealed.n, "base64");
|
|
154
|
+
const cipher = Buffer.from(sealed.c, "base64");
|
|
155
|
+
// Validate sizes before allocating: a short ciphertext would make the
|
|
156
|
+
// alloc below negative (RangeError), and an oversized frame would
|
|
157
|
+
// allocate unbounded memory — both should fail as protocol errors.
|
|
158
|
+
if (nonce.length !== sodium.crypto_box_NONCEBYTES) {
|
|
159
|
+
throw new ProtocolError("bad_sealed_box", "nonce has the wrong length");
|
|
160
|
+
}
|
|
161
|
+
if (cipher.length < sodium.crypto_box_MACBYTES || cipher.length > MAX_SEALED_BYTES) {
|
|
162
|
+
throw new ProtocolError("bad_sealed_box", "ciphertext length out of range");
|
|
163
|
+
}
|
|
164
|
+
const plaintext = Buffer.alloc(cipher.length - sodium.crypto_box_MACBYTES);
|
|
165
|
+
const ok = sodium.crypto_box_open_easy(plaintext, cipher, nonce, peerPublicKey, self.secretKey);
|
|
166
|
+
if (!ok)
|
|
167
|
+
throw new ProtocolError("decrypt_failed", "box authentication failed");
|
|
168
|
+
env.payload = JSON.parse(plaintext.toString("utf8"));
|
|
169
|
+
return env;
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/** Shared envelope structural check (payload shape relaxed when sealed). */
|
|
174
|
+
function parseEnvelope(raw, allowSealed = false) {
|
|
175
|
+
let parsed;
|
|
176
|
+
try {
|
|
177
|
+
parsed = JSON.parse(raw);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
throw new ProtocolError("bad_json", "frame was not valid JSON");
|
|
181
|
+
}
|
|
182
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
183
|
+
throw new ProtocolError("bad_envelope", "frame was not an object");
|
|
184
|
+
}
|
|
185
|
+
const e = parsed;
|
|
186
|
+
if (e.v !== PROTOCOL_VERSION) {
|
|
187
|
+
throw new ProtocolError("bad_version", `unsupported version ${String(e.v)}`);
|
|
188
|
+
}
|
|
189
|
+
if (typeof e.id !== "string")
|
|
190
|
+
throw new ProtocolError("bad_envelope", "missing id");
|
|
191
|
+
if (typeof e.type !== "string")
|
|
192
|
+
throw new ProtocolError("bad_envelope", "missing type");
|
|
193
|
+
if (!("ws" in e) || (e.ws !== null && typeof e.ws !== "string")) {
|
|
194
|
+
throw new ProtocolError("bad_envelope", "ws must be string or null");
|
|
195
|
+
}
|
|
196
|
+
if (typeof e.payload !== "object" || e.payload === null) {
|
|
197
|
+
throw new ProtocolError("bad_envelope", "payload must be an object");
|
|
198
|
+
}
|
|
199
|
+
void allowSealed;
|
|
200
|
+
return parsed;
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/crypto.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DaemonIdentity — this daemon's stable id + human name.
|
|
3
|
+
*
|
|
4
|
+
* A client can talk to several daemons at once (it dials each tunnel/host
|
|
5
|
+
* independently — daemons never talk to each other), and labels each by name.
|
|
6
|
+
* This store gives the daemon a stable `daemonId` (minted once, persisted) and a
|
|
7
|
+
* human `name` that defaults to the machine hostname and can be changed over the
|
|
8
|
+
* wire (`daemon.rename`). Persisted to `~/.immediacy/daemon.json`.
|
|
9
|
+
*
|
|
10
|
+
* No secret material, so the file is world-readable (0644) like config.json —
|
|
11
|
+
* but it keeps the same serialized atomic-rename persist() so a concurrent
|
|
12
|
+
* rename can't tear the file.
|
|
13
|
+
*/
|
|
14
|
+
import { type DaemonInfoPayload } from "./protocol.js";
|
|
15
|
+
export declare class DaemonIdentity {
|
|
16
|
+
private daemonId;
|
|
17
|
+
private name;
|
|
18
|
+
private readonly version;
|
|
19
|
+
/** Serializes writes so two concurrent rename() calls can't interleave. */
|
|
20
|
+
private writeChain;
|
|
21
|
+
/**
|
|
22
|
+
* Load the persisted identity, minting a fresh id (and a hostname-derived
|
|
23
|
+
* default name) on first run and writing it back so it stays stable.
|
|
24
|
+
*/
|
|
25
|
+
load(): Promise<void>;
|
|
26
|
+
/** Set the human name (trimmed, non-empty, length-capped) and persist. */
|
|
27
|
+
rename(name: string): Promise<void>;
|
|
28
|
+
/** The wire payload for `daemon.info` (pushed on connect / rename / get). */
|
|
29
|
+
info(): DaemonInfoPayload;
|
|
30
|
+
private persist;
|
|
31
|
+
}
|