@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.
Files changed (133) hide show
  1. package/dist/src/actions.d.ts +22 -0
  2. package/dist/src/actions.js +44 -0
  3. package/dist/src/actions.js.map +1 -0
  4. package/dist/src/agent/agent.d.ts +89 -0
  5. package/dist/src/agent/agent.js +22 -0
  6. package/dist/src/agent/agent.js.map +1 -0
  7. package/dist/src/agent/claude.d.ts +70 -0
  8. package/dist/src/agent/claude.js +404 -0
  9. package/dist/src/agent/claude.js.map +1 -0
  10. package/dist/src/agent/codex.d.ts +44 -0
  11. package/dist/src/agent/codex.js +303 -0
  12. package/dist/src/agent/codex.js.map +1 -0
  13. package/dist/src/agent/mock.d.ts +31 -0
  14. package/dist/src/agent/mock.js +155 -0
  15. package/dist/src/agent/mock.js.map +1 -0
  16. package/dist/src/agent/model-error.d.ts +8 -0
  17. package/dist/src/agent/model-error.js +25 -0
  18. package/dist/src/agent/model-error.js.map +1 -0
  19. package/dist/src/agent/openrouter.d.ts +38 -0
  20. package/dist/src/agent/openrouter.js +60 -0
  21. package/dist/src/agent/openrouter.js.map +1 -0
  22. package/dist/src/agent/workspace-scope.d.ts +25 -0
  23. package/dist/src/agent/workspace-scope.js +71 -0
  24. package/dist/src/agent/workspace-scope.js.map +1 -0
  25. package/dist/src/allowlist.d.ts +56 -0
  26. package/dist/src/allowlist.js +141 -0
  27. package/dist/src/allowlist.js.map +1 -0
  28. package/dist/src/chatlog.d.ts +50 -0
  29. package/dist/src/chatlog.js +179 -0
  30. package/dist/src/chatlog.js.map +1 -0
  31. package/dist/src/cli/paths.d.ts +40 -0
  32. package/dist/src/cli/paths.js +76 -0
  33. package/dist/src/cli/paths.js.map +1 -0
  34. package/dist/src/cli/service.d.ts +20 -0
  35. package/dist/src/cli/service.js +245 -0
  36. package/dist/src/cli/service.js.map +1 -0
  37. package/dist/src/cli.d.ts +2 -0
  38. package/dist/src/cli.js +227 -0
  39. package/dist/src/cli.js.map +1 -0
  40. package/dist/src/config.d.ts +33 -0
  41. package/dist/src/config.js +122 -0
  42. package/dist/src/config.js.map +1 -0
  43. package/dist/src/credentials.d.ts +38 -0
  44. package/dist/src/credentials.js +120 -0
  45. package/dist/src/credentials.js.map +1 -0
  46. package/dist/src/crypto.d.ts +49 -0
  47. package/dist/src/crypto.js +202 -0
  48. package/dist/src/crypto.js.map +1 -0
  49. package/dist/src/daemon-identity.d.ts +31 -0
  50. package/dist/src/daemon-identity.js +97 -0
  51. package/dist/src/daemon-identity.js.map +1 -0
  52. package/dist/src/envelope.d.ts +14 -0
  53. package/dist/src/envelope.js +28 -0
  54. package/dist/src/envelope.js.map +1 -0
  55. package/dist/src/events.d.ts +40 -0
  56. package/dist/src/events.js +24 -0
  57. package/dist/src/events.js.map +1 -0
  58. package/dist/src/forward.d.ts +51 -0
  59. package/dist/src/forward.js +170 -0
  60. package/dist/src/forward.js.map +1 -0
  61. package/dist/src/fs-scope.d.ts +17 -0
  62. package/dist/src/fs-scope.js +47 -0
  63. package/dist/src/fs-scope.js.map +1 -0
  64. package/dist/src/github.d.ts +70 -0
  65. package/dist/src/github.js +178 -0
  66. package/dist/src/github.js.map +1 -0
  67. package/dist/src/gitops.d.ts +113 -0
  68. package/dist/src/gitops.js +557 -0
  69. package/dist/src/gitops.js.map +1 -0
  70. package/dist/src/hub.d.ts +50 -0
  71. package/dist/src/hub.js +92 -0
  72. package/dist/src/hub.js.map +1 -0
  73. package/dist/src/index.d.ts +22 -0
  74. package/dist/src/index.js +56 -0
  75. package/dist/src/index.js.map +1 -0
  76. package/dist/src/jobs/scheduler.d.ts +94 -0
  77. package/dist/src/jobs/scheduler.js +373 -0
  78. package/dist/src/jobs/scheduler.js.map +1 -0
  79. package/dist/src/jobs/store.d.ts +33 -0
  80. package/dist/src/jobs/store.js +111 -0
  81. package/dist/src/jobs/store.js.map +1 -0
  82. package/dist/src/log.d.ts +61 -0
  83. package/dist/src/log.js +166 -0
  84. package/dist/src/log.js.map +1 -0
  85. package/dist/src/migrate-state-dir.d.ts +1 -0
  86. package/dist/src/migrate-state-dir.js +47 -0
  87. package/dist/src/migrate-state-dir.js.map +1 -0
  88. package/dist/src/models.d.ts +36 -0
  89. package/dist/src/models.js +71 -0
  90. package/dist/src/models.js.map +1 -0
  91. package/dist/src/permission-gate.d.ts +48 -0
  92. package/dist/src/permission-gate.js +93 -0
  93. package/dist/src/permission-gate.js.map +1 -0
  94. package/dist/src/permission-scope.d.ts +77 -0
  95. package/dist/src/permission-scope.js +244 -0
  96. package/dist/src/permission-scope.js.map +1 -0
  97. package/dist/src/ports.d.ts +71 -0
  98. package/dist/src/ports.js +216 -0
  99. package/dist/src/ports.js.map +1 -0
  100. package/dist/src/projects.d.ts +83 -0
  101. package/dist/src/projects.js +205 -0
  102. package/dist/src/projects.js.map +1 -0
  103. package/dist/src/protocol.d.ts +1226 -0
  104. package/dist/src/protocol.js +16 -0
  105. package/dist/src/protocol.js.map +1 -0
  106. package/dist/src/repo-sync.d.ts +49 -0
  107. package/dist/src/repo-sync.js +83 -0
  108. package/dist/src/repo-sync.js.map +1 -0
  109. package/dist/src/server.d.ts +33 -0
  110. package/dist/src/server.js +973 -0
  111. package/dist/src/server.js.map +1 -0
  112. package/dist/src/terminal.d.ts +32 -0
  113. package/dist/src/terminal.js +92 -0
  114. package/dist/src/terminal.js.map +1 -0
  115. package/dist/src/titler.d.ts +15 -0
  116. package/dist/src/titler.js +72 -0
  117. package/dist/src/titler.js.map +1 -0
  118. package/dist/src/types.d.ts +99 -0
  119. package/dist/src/types.js +3 -0
  120. package/dist/src/types.js.map +1 -0
  121. package/dist/src/usage-limit.d.ts +49 -0
  122. package/dist/src/usage-limit.js +83 -0
  123. package/dist/src/usage-limit.js.map +1 -0
  124. package/dist/src/usage.d.ts +23 -0
  125. package/dist/src/usage.js +63 -0
  126. package/dist/src/usage.js.map +1 -0
  127. package/dist/src/workspace.d.ts +303 -0
  128. package/dist/src/workspace.js +1086 -0
  129. package/dist/src/workspace.js.map +1 -0
  130. package/dist/src/worktree.d.ts +133 -0
  131. package/dist/src/worktree.js +561 -0
  132. package/dist/src/worktree.js.map +1 -0
  133. 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
+ }