@arcanemachine/inter-agent-opencode 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.
@@ -0,0 +1,47 @@
1
+ export declare const DEFAULT_HOST = "127.0.0.1";
2
+ export declare const DEFAULT_PORT = 16837;
3
+ export declare const DEFAULT_TOKEN_FILENAME = "token";
4
+ export declare const DEFAULT_TLS_CERT_FILENAME = "tls-cert.pem";
5
+ export type Environment = Record<string, string | undefined>;
6
+ export type ResolverOptions = {
7
+ env?: Environment;
8
+ platform?: NodeJS.Platform;
9
+ home?: string;
10
+ };
11
+ export type CoreConfig = Record<string, unknown>;
12
+ export type LoadedConfig = {
13
+ values: CoreConfig;
14
+ path: string | undefined;
15
+ };
16
+ export type EndpointResolution = {
17
+ host: string;
18
+ port: number;
19
+ dataDir: string;
20
+ configPath: string | undefined;
21
+ hostSource: "env" | "config" | "default";
22
+ portSource: "env" | "config" | "default";
23
+ dataDirSource: "env" | "config" | "default";
24
+ tls: boolean;
25
+ tlsSource: "env" | "config" | "default";
26
+ scheme: "ws" | "wss";
27
+ tlsCertPath: string;
28
+ tlsCertSource: "env" | "config" | "default";
29
+ supported: boolean;
30
+ unsupportedReason?: string;
31
+ };
32
+ export type SecretResolution = {
33
+ secret: string;
34
+ source: "env" | "config" | "token_file";
35
+ tokenPath?: string;
36
+ configPath?: string;
37
+ };
38
+ export declare function expandPath(raw: string, options?: ResolverOptions): string;
39
+ export declare function configPath(options?: ResolverOptions): string;
40
+ export declare function dataDirectory(options?: ResolverOptions, config?: LoadedConfig): string;
41
+ export declare function loadConfig(options?: ResolverOptions): LoadedConfig;
42
+ export declare function resolvesLoopback(host: string): Promise<boolean>;
43
+ export declare function resolveEndpoint(options?: ResolverOptions): Promise<EndpointResolution>;
44
+ export declare function assertSupportedEndpoint(endpoint: EndpointResolution): void;
45
+ export declare function tokenPath(options?: ResolverOptions, config?: LoadedConfig): string;
46
+ export declare function endpointUri(endpoint: Pick<EndpointResolution, "host" | "port" | "scheme">): string;
47
+ export declare function resolveSecret(options?: ResolverOptions, config?: LoadedConfig): SecretResolution;
package/dist/config.js ADDED
@@ -0,0 +1,349 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { lookup } from "node:dns/promises";
3
+ import { chmodSync, closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { isIP } from "node:net";
6
+ import { dirname, join } from "node:path";
7
+ import { ConfigError, UnsupportedEndpointError } from "./errors.js";
8
+ export const DEFAULT_HOST = "127.0.0.1";
9
+ export const DEFAULT_PORT = 16837;
10
+ export const DEFAULT_TOKEN_FILENAME = "token";
11
+ export const DEFAULT_TLS_CERT_FILENAME = "tls-cert.pem";
12
+ export function expandPath(raw, options = {}) {
13
+ const env = options.env ?? process.env;
14
+ const home = options.home ?? homedir();
15
+ let expanded = raw;
16
+ if (expanded === "~")
17
+ expanded = home;
18
+ else if (expanded.startsWith("~/"))
19
+ expanded = join(home, expanded.slice(2));
20
+ expanded = expanded.replace(/\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, braced, plain) => {
21
+ const name = braced ?? plain;
22
+ return env[name] ?? match;
23
+ });
24
+ return expanded;
25
+ }
26
+ export function configPath(options = {}) {
27
+ const env = options.env ?? process.env;
28
+ const home = options.home ?? homedir();
29
+ const platform = options.platform ?? process.platform;
30
+ const override = env.INTER_AGENT_CONFIG;
31
+ if (override)
32
+ return expandPath(override, options);
33
+ if (platform === "darwin")
34
+ return join(home, "Library", "Application Support", "inter-agent", "config.json");
35
+ if (platform.startsWith("win")) {
36
+ const appData = env.APPDATA;
37
+ if (appData)
38
+ return join(appData, "inter-agent", "config.json");
39
+ }
40
+ const xdg = env.XDG_CONFIG_HOME;
41
+ if (xdg)
42
+ return join(xdg, "inter-agent", "config.json");
43
+ return join(home, ".config", "inter-agent", "config.json");
44
+ }
45
+ export function dataDirectory(options = {}, config) {
46
+ const env = options.env ?? process.env;
47
+ const home = options.home ?? homedir();
48
+ const platform = options.platform ?? process.platform;
49
+ if (env.INTER_AGENT_DATA_DIR)
50
+ return expandPath(env.INTER_AGENT_DATA_DIR, options);
51
+ const configured = configString(config?.values, "dataDir");
52
+ if (configured)
53
+ return expandPath(configured, options);
54
+ if (platform === "darwin")
55
+ return join(home, "Library", "Application Support", "inter-agent");
56
+ if (platform.startsWith("win")) {
57
+ const local = env.LOCALAPPDATA ?? env.APPDATA;
58
+ if (local)
59
+ return join(local, "inter-agent");
60
+ }
61
+ const xdg = env.XDG_STATE_HOME;
62
+ if (xdg)
63
+ return join(xdg, "inter-agent");
64
+ return join(home, ".local", "state", "inter-agent");
65
+ }
66
+ export function loadConfig(options = {}) {
67
+ const path = configPath(options);
68
+ if (!existsSync(path))
69
+ return { values: {}, path: undefined };
70
+ let parsed;
71
+ try {
72
+ parsed = JSON.parse(readFileSync(path, "utf8"));
73
+ }
74
+ catch {
75
+ throw new ConfigError("invalid inter-agent config file");
76
+ }
77
+ if (!isRecord(parsed))
78
+ throw new ConfigError("inter-agent config root must be a JSON object");
79
+ return { values: parsed, path };
80
+ }
81
+ function isRecord(value) {
82
+ return typeof value === "object" && value !== null && !Array.isArray(value);
83
+ }
84
+ function configString(config, key) {
85
+ const value = config?.[key];
86
+ if (value === undefined || value === null)
87
+ return undefined;
88
+ if (typeof value !== "string")
89
+ throw new ConfigError(`inter-agent config key ${key} must be a string`);
90
+ return value;
91
+ }
92
+ function parsePort(value, source) {
93
+ if (typeof value === "number") {
94
+ if (!Number.isInteger(value))
95
+ throw new ConfigError(`${source} must be an integer`);
96
+ return validatePort(value, source);
97
+ }
98
+ if (typeof value !== "string" || !/^\d+$/.test(value))
99
+ throw new ConfigError(`${source} must be an integer`);
100
+ return validatePort(Number(value), source);
101
+ }
102
+ function validatePort(value, source) {
103
+ if (!Number.isSafeInteger(value) || value < 1 || value > 65535) {
104
+ throw new ConfigError(`${source} must be between 1 and 65535`);
105
+ }
106
+ return value;
107
+ }
108
+ function envPort(env) {
109
+ const value = env.INTER_AGENT_PORT;
110
+ if (value === undefined || value === "")
111
+ return undefined;
112
+ return parsePort(value, "INTER_AGENT_PORT");
113
+ }
114
+ function parseBoolean(value, source) {
115
+ if (typeof value === "boolean")
116
+ return value;
117
+ if (typeof value !== "string")
118
+ throw new ConfigError(`${source} must be a boolean`);
119
+ const normalized = value.trim().toLowerCase();
120
+ if (["1", "true", "yes", "on", "wss", "tls"].includes(normalized))
121
+ return true;
122
+ if (["0", "false", "no", "off", "ws", "plaintext"].includes(normalized))
123
+ return false;
124
+ throw new ConfigError(`${source} must be a boolean`);
125
+ }
126
+ function configBoolean(config, key) {
127
+ const value = config[key];
128
+ if (value === undefined || value === null)
129
+ return undefined;
130
+ return parseBoolean(value, `inter-agent config key ${key}`);
131
+ }
132
+ function envBoolean(env, key) {
133
+ const value = env[key];
134
+ if (value === undefined || value === "")
135
+ return undefined;
136
+ return parseBoolean(value, key);
137
+ }
138
+ function normalizedHost(host) {
139
+ const trimmed = host.trim();
140
+ return trimmed.startsWith("[") && trimmed.endsWith("]")
141
+ ? trimmed.slice(1, -1)
142
+ : trimmed;
143
+ }
144
+ function isLoopbackIp(host) {
145
+ const value = normalizedHost(host).toLowerCase();
146
+ const version = isIP(value);
147
+ if (version === 4)
148
+ return value.startsWith("127.");
149
+ if (version === 6)
150
+ return value === "::1" || value.startsWith("::ffff:127.");
151
+ return false;
152
+ }
153
+ export async function resolvesLoopback(host) {
154
+ const value = normalizedHost(host);
155
+ if (value.toLowerCase() === "localhost" || isLoopbackIp(value))
156
+ return true;
157
+ try {
158
+ const records = await lookup(value, { all: true, verbatim: true });
159
+ return (records.length > 0 &&
160
+ records.every((record) => isLoopbackIp(record.address)));
161
+ }
162
+ catch {
163
+ return false;
164
+ }
165
+ }
166
+ export async function resolveEndpoint(options = {}) {
167
+ const env = options.env ?? process.env;
168
+ const config = loadConfig(options);
169
+ const configuredHost = configString(config.values, "host");
170
+ const envHost = env.INTER_AGENT_HOST;
171
+ const host = envHost || configuredHost || DEFAULT_HOST;
172
+ const hostSource = envHost ? "env" : configuredHost ? "config" : "default";
173
+ const configuredPort = config.values.port;
174
+ const envPortValue = envPort(env);
175
+ const port = envPortValue ??
176
+ (configuredPort === undefined || configuredPort === null
177
+ ? DEFAULT_PORT
178
+ : parsePort(configuredPort, "inter-agent config key port"));
179
+ const portSource = envPortValue !== undefined
180
+ ? "env"
181
+ : configuredPort === undefined || configuredPort === null
182
+ ? "default"
183
+ : "config";
184
+ const dataDir = dataDirectory(options, config);
185
+ const configuredTls = configBoolean(config.values, "tls");
186
+ const envTls = envBoolean(env, "INTER_AGENT_TLS");
187
+ const loopback = await resolvesLoopback(host);
188
+ const tls = envTls ?? configuredTls ?? !loopback;
189
+ const tlsSource = envTls !== undefined
190
+ ? "env"
191
+ : configuredTls !== undefined
192
+ ? "config"
193
+ : "default";
194
+ const configuredCert = configString(config.values, "tlsCert");
195
+ const envCert = env.INTER_AGENT_TLS_CERT;
196
+ const tlsCertPath = envCert
197
+ ? expandPath(envCert, options)
198
+ : configuredCert
199
+ ? expandPath(configuredCert, options)
200
+ : join(dataDir, DEFAULT_TLS_CERT_FILENAME);
201
+ const tlsCertSource = envCert ? "env" : configuredCert ? "config" : "default";
202
+ const reasons = [];
203
+ if (!loopback)
204
+ reasons.push("host is not loopback");
205
+ return {
206
+ host: normalizedHost(host),
207
+ port,
208
+ dataDir,
209
+ configPath: config.path,
210
+ hostSource,
211
+ portSource,
212
+ dataDirSource: env.INTER_AGENT_DATA_DIR
213
+ ? "env"
214
+ : configString(config.values, "dataDir")
215
+ ? "config"
216
+ : "default",
217
+ tls,
218
+ tlsSource,
219
+ scheme: tls ? "wss" : "ws",
220
+ tlsCertPath,
221
+ tlsCertSource,
222
+ supported: reasons.length === 0,
223
+ ...(reasons.length ? { unsupportedReason: reasons.join("; ") } : {}),
224
+ };
225
+ }
226
+ export function assertSupportedEndpoint(endpoint) {
227
+ if (!endpoint.supported)
228
+ throw new UnsupportedEndpointError(endpoint.unsupportedReason ?? "unsupported transport");
229
+ }
230
+ export function tokenPath(options = {}, config) {
231
+ return join(dataDirectory(options, config), DEFAULT_TOKEN_FILENAME);
232
+ }
233
+ export function endpointUri(endpoint) {
234
+ const host = isIP(endpoint.host) === 6 && !endpoint.host.startsWith("[")
235
+ ? `[${endpoint.host}]`
236
+ : endpoint.host;
237
+ return `${endpoint.scheme}://${host}:${endpoint.port}`;
238
+ }
239
+ function ensurePrivateDirectory(path) {
240
+ try {
241
+ const existing = lstatSync(path);
242
+ if (existing.isSymbolicLink() || !existing.isDirectory())
243
+ throw new ConfigError("inter-agent data directory is not a private directory");
244
+ }
245
+ catch (error) {
246
+ if (error instanceof ConfigError)
247
+ throw error;
248
+ mkdirSync(path, { recursive: true, mode: 0o700 });
249
+ }
250
+ try {
251
+ chmodSync(path, 0o700);
252
+ }
253
+ catch {
254
+ // Windows may not support POSIX mode bits.
255
+ }
256
+ }
257
+ function readToken(path) {
258
+ if (!existsSync(path))
259
+ return undefined;
260
+ let stat;
261
+ try {
262
+ stat = lstatSync(path);
263
+ }
264
+ catch {
265
+ throw new ConfigError("unable to inspect inter-agent token file");
266
+ }
267
+ if (!stat.isFile())
268
+ throw new ConfigError("inter-agent token path is not a regular file");
269
+ try {
270
+ chmodSync(path, 0o600);
271
+ }
272
+ catch {
273
+ // Windows may not support POSIX mode bits.
274
+ }
275
+ let value;
276
+ try {
277
+ value = readFileSync(path, "utf8").trim();
278
+ }
279
+ catch {
280
+ throw new ConfigError("unable to read inter-agent token file");
281
+ }
282
+ return value || undefined;
283
+ }
284
+ function createToken(path) {
285
+ const token = randomBytes(32).toString("base64url");
286
+ const temp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
287
+ try {
288
+ const fd = openSync(temp, "wx", 0o600);
289
+ try {
290
+ writeFileSync(fd, `${token}\n`, { encoding: "utf8" });
291
+ }
292
+ finally {
293
+ closeSync(fd);
294
+ }
295
+ try {
296
+ chmodSync(temp, 0o600);
297
+ }
298
+ catch {
299
+ // Windows may not support POSIX mode bits.
300
+ }
301
+ renameSync(temp, path);
302
+ try {
303
+ chmodSync(path, 0o600);
304
+ }
305
+ catch {
306
+ // Windows may not support POSIX mode bits.
307
+ }
308
+ return token;
309
+ }
310
+ catch (error) {
311
+ try {
312
+ unlinkSync(temp);
313
+ }
314
+ catch {
315
+ // Best effort cleanup of this process's private temporary file.
316
+ }
317
+ const existing = readToken(path);
318
+ if (existing)
319
+ return existing;
320
+ if (error instanceof ConfigError)
321
+ throw error;
322
+ throw new ConfigError("unable to create inter-agent token file");
323
+ }
324
+ }
325
+ export function resolveSecret(options = {}, config) {
326
+ const env = options.env ?? process.env;
327
+ const loaded = config ?? loadConfig(options);
328
+ const explicitEnv = env.INTER_AGENT_SECRET;
329
+ if (explicitEnv !== undefined) {
330
+ if (!explicitEnv.trim())
331
+ throw new ConfigError("INTER_AGENT_SECRET must not be empty");
332
+ return { secret: explicitEnv, source: "env", configPath: loaded.path };
333
+ }
334
+ const explicitConfig = configString(loaded.values, "secret");
335
+ if (explicitConfig !== undefined) {
336
+ if (!explicitConfig.trim())
337
+ throw new ConfigError("inter-agent config key secret must not be empty");
338
+ return {
339
+ secret: explicitConfig,
340
+ source: "config",
341
+ configPath: loaded.path,
342
+ };
343
+ }
344
+ const path = tokenPath(options, loaded);
345
+ ensurePrivateDirectory(dirname(path));
346
+ const token = readToken(path) ?? createToken(path);
347
+ return { secret: token, source: "token_file", tokenPath: path };
348
+ }
349
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,35 @@
1
+ export type ProtocolErrorCode = "PROTOCOL_ERROR" | "AUTH_FAILED" | "TOO_MANY_CONNECTIONS" | "BAD_ROLE" | "BAD_SESSION" | "SESSION_TAKEN" | "BAD_NAME" | "BAD_LABEL" | "NAME_TAKEN" | "UNKNOWN_OP" | "BAD_TEXT" | "BAD_FROM_NAME" | "BAD_CUSTOM_TYPE" | "TEXT_TOO_LARGE" | "CUSTOM_PAYLOAD_TOO_LARGE" | "UNKNOWN_TARGET" | "AMBIGUOUS_TARGET" | "BAD_CHANNEL" | "CHANNEL_LIMIT_REACHED" | "NOT_SUBSCRIBED" | "UNKNOWN_CHANNEL" | "KICKED";
2
+ export declare class InterAgentError extends Error {
3
+ readonly kind: string;
4
+ readonly retryable: boolean;
5
+ constructor(message: string, kind: string, retryable?: boolean);
6
+ }
7
+ export declare class ConfigError extends InterAgentError {
8
+ constructor(message: string);
9
+ }
10
+ export declare class UnsupportedEndpointError extends InterAgentError {
11
+ readonly reason: string;
12
+ constructor(reason: string);
13
+ }
14
+ export declare class StateError extends InterAgentError {
15
+ constructor(message: string);
16
+ }
17
+ export declare class ProtocolError extends InterAgentError {
18
+ readonly code: ProtocolErrorCode;
19
+ constructor(message: string, code?: ProtocolErrorCode);
20
+ }
21
+ export declare class AuthenticationError extends InterAgentError {
22
+ constructor(message?: string);
23
+ }
24
+ export declare class ConnectionError extends InterAgentError {
25
+ constructor(message: string, retryable?: boolean);
26
+ }
27
+ export declare class TimeoutError extends InterAgentError {
28
+ constructor(message: string);
29
+ }
30
+ export declare class RemoteError extends InterAgentError {
31
+ readonly code: ProtocolErrorCode;
32
+ readonly remoteMessage: string;
33
+ constructor(code: ProtocolErrorCode, message: string, secret?: string);
34
+ }
35
+ export declare function isProtocolErrorCode(value: unknown): value is ProtocolErrorCode;
package/dist/errors.js ADDED
@@ -0,0 +1,95 @@
1
+ export class InterAgentError extends Error {
2
+ kind;
3
+ retryable;
4
+ constructor(message, kind, retryable = false) {
5
+ super(message);
6
+ this.name = kind;
7
+ this.kind = kind;
8
+ this.retryable = retryable;
9
+ }
10
+ }
11
+ export class ConfigError extends InterAgentError {
12
+ constructor(message) {
13
+ super(message, "ConfigError");
14
+ }
15
+ }
16
+ export class UnsupportedEndpointError extends InterAgentError {
17
+ reason;
18
+ constructor(reason) {
19
+ super(`unsupported inter-agent endpoint: ${reason}`, "UnsupportedEndpointError");
20
+ this.reason = reason;
21
+ }
22
+ }
23
+ export class StateError extends InterAgentError {
24
+ constructor(message) {
25
+ super(message, "StateError");
26
+ }
27
+ }
28
+ export class ProtocolError extends InterAgentError {
29
+ code;
30
+ constructor(message, code = "PROTOCOL_ERROR") {
31
+ super(message, "ProtocolError");
32
+ this.code = code;
33
+ }
34
+ }
35
+ export class AuthenticationError extends InterAgentError {
36
+ constructor(message = "inter-agent server authentication failed") {
37
+ super(message, "AuthenticationError");
38
+ }
39
+ }
40
+ export class ConnectionError extends InterAgentError {
41
+ constructor(message, retryable = true) {
42
+ super(message, "ConnectionError", retryable);
43
+ }
44
+ }
45
+ export class TimeoutError extends InterAgentError {
46
+ constructor(message) {
47
+ super(message, "TimeoutError", true);
48
+ }
49
+ }
50
+ function redact(value, secret) {
51
+ if (!secret || !value.includes(secret))
52
+ return value;
53
+ return value.split(secret).join("[redacted]");
54
+ }
55
+ export class RemoteError extends InterAgentError {
56
+ code;
57
+ remoteMessage;
58
+ constructor(code, message, secret) {
59
+ const safeMessage = redact(message, secret);
60
+ super(`inter-agent ${code}: ${safeMessage}`, "RemoteError", isTransientCode(code));
61
+ this.code = code;
62
+ this.remoteMessage = safeMessage;
63
+ }
64
+ }
65
+ function isTransientCode(code) {
66
+ return code === "TOO_MANY_CONNECTIONS";
67
+ }
68
+ export function isProtocolErrorCode(value) {
69
+ return (typeof value === "string" &&
70
+ [
71
+ "PROTOCOL_ERROR",
72
+ "AUTH_FAILED",
73
+ "TOO_MANY_CONNECTIONS",
74
+ "BAD_ROLE",
75
+ "BAD_SESSION",
76
+ "SESSION_TAKEN",
77
+ "BAD_NAME",
78
+ "BAD_LABEL",
79
+ "NAME_TAKEN",
80
+ "UNKNOWN_OP",
81
+ "BAD_TEXT",
82
+ "BAD_FROM_NAME",
83
+ "BAD_CUSTOM_TYPE",
84
+ "TEXT_TOO_LARGE",
85
+ "CUSTOM_PAYLOAD_TOO_LARGE",
86
+ "UNKNOWN_TARGET",
87
+ "AMBIGUOUS_TARGET",
88
+ "BAD_CHANNEL",
89
+ "CHANNEL_LIMIT_REACHED",
90
+ "NOT_SUBSCRIBED",
91
+ "UNKNOWN_CHANNEL",
92
+ "KICKED",
93
+ ].includes(value));
94
+ }
95
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,31 @@
1
+ export declare const INBOX_VERSION = 1;
2
+ export declare const INBOX_MAX_MESSAGES = 100;
3
+ export declare const INBOX_MAX_BYTES: number;
4
+ export type InboxKind = "direct" | "broadcast";
5
+ export type InboxMessage = {
6
+ id: string;
7
+ receivedAt: string;
8
+ from: string;
9
+ fromName: string;
10
+ kind: InboxKind;
11
+ to: string | null;
12
+ text: string;
13
+ notificationTruncated: boolean;
14
+ };
15
+ export type Inbox = {
16
+ version: 1;
17
+ messages: InboxMessage[];
18
+ };
19
+ export declare function emptyInbox(): Inbox;
20
+ export declare function encodedInboxSize(inbox: Inbox): number;
21
+ export declare function evictToBounds(messages: InboxMessage[]): InboxMessage[];
22
+ export declare function addMessage(inbox: Inbox, message: InboxMessage): {
23
+ inbox: Inbox;
24
+ added: boolean;
25
+ };
26
+ export declare function readInboxFile(dataDir: string, workspaceHash: string, sessionHash: string): Inbox;
27
+ export declare function writeInboxFile(dataDir: string, workspaceHash: string, sessionHash: string, inbox: Inbox): void;
28
+ export declare function recordMessage(dataDir: string, workspaceHash: string, sessionHash: string, message: InboxMessage): {
29
+ inbox: Inbox;
30
+ added: boolean;
31
+ };
package/dist/inbox.js ADDED
@@ -0,0 +1,94 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { StateError } from "./errors.js";
4
+ import { ensureSessionDir, readJsonFile, sessionDir, writeJsonAtomic, } from "./state.js";
5
+ export const INBOX_VERSION = 1;
6
+ export const INBOX_MAX_MESSAGES = 100;
7
+ export const INBOX_MAX_BYTES = 8 * 1024 * 1024;
8
+ const INBOX_FILENAME = "inbox.json";
9
+ export function emptyInbox() {
10
+ return { version: INBOX_VERSION, messages: [] };
11
+ }
12
+ export function encodedInboxSize(inbox) {
13
+ return Buffer.byteLength(JSON.stringify(inbox), "utf8");
14
+ }
15
+ // Eviction drops the oldest records until the count and encoded-size bounds
16
+ // both hold. A single record larger than the total bound is kept as the sole
17
+ // record rather than being discarded.
18
+ export function evictToBounds(messages) {
19
+ const result = messages.slice();
20
+ while (result.length > INBOX_MAX_MESSAGES ||
21
+ encodedInboxSize({ version: INBOX_VERSION, messages: result }) >
22
+ INBOX_MAX_BYTES) {
23
+ if (result.length <= 1)
24
+ break;
25
+ result.shift();
26
+ }
27
+ return result;
28
+ }
29
+ export function addMessage(inbox, message) {
30
+ if (inbox.messages.some((existing) => existing.id === message.id))
31
+ return { inbox, added: false };
32
+ const messages = [...inbox.messages, message];
33
+ return {
34
+ inbox: { version: INBOX_VERSION, messages: evictToBounds(messages) },
35
+ added: true,
36
+ };
37
+ }
38
+ function isInboxMessage(value) {
39
+ if (typeof value !== "object" || value === null || Array.isArray(value))
40
+ return false;
41
+ const record = value;
42
+ if (typeof record.id !== "string" || record.id.length === 0)
43
+ return false;
44
+ if (typeof record.receivedAt !== "string")
45
+ return false;
46
+ if (Number.isNaN(Date.parse(record.receivedAt)))
47
+ return false;
48
+ if (typeof record.from !== "string" || record.from.length === 0)
49
+ return false;
50
+ if (typeof record.fromName !== "string")
51
+ return false;
52
+ if (record.kind !== "direct" && record.kind !== "broadcast")
53
+ return false;
54
+ if (record.to !== null && typeof record.to !== "string")
55
+ return false;
56
+ if (typeof record.text !== "string")
57
+ return false;
58
+ if (typeof record.notificationTruncated !== "boolean")
59
+ return false;
60
+ return true;
61
+ }
62
+ function isInbox(value) {
63
+ if (typeof value !== "object" || value === null || Array.isArray(value))
64
+ return false;
65
+ const record = value;
66
+ if (record.version !== INBOX_VERSION)
67
+ return false;
68
+ if (!Array.isArray(record.messages))
69
+ return false;
70
+ if (!record.messages.every((message) => isInboxMessage(message)))
71
+ return false;
72
+ return true;
73
+ }
74
+ export function readInboxFile(dataDir, workspaceHash, sessionHash) {
75
+ const dir = sessionDir(dataDir, workspaceHash, sessionHash);
76
+ const path = join(dir, INBOX_FILENAME);
77
+ if (!existsSync(path))
78
+ return emptyInbox();
79
+ const record = readJsonFile(path);
80
+ if (!isInbox(record))
81
+ throw new StateError("session inbox is malformed");
82
+ return record;
83
+ }
84
+ export function writeInboxFile(dataDir, workspaceHash, sessionHash, inbox) {
85
+ writeJsonAtomic(join(ensureSessionDir(dataDir, workspaceHash, sessionHash), INBOX_FILENAME), inbox);
86
+ }
87
+ export function recordMessage(dataDir, workspaceHash, sessionHash, message) {
88
+ const current = readInboxFile(dataDir, workspaceHash, sessionHash);
89
+ const { inbox, added } = addMessage(current, message);
90
+ if (added)
91
+ writeInboxFile(dataDir, workspaceHash, sessionHash, inbox);
92
+ return { inbox, added };
93
+ }
94
+ //# sourceMappingURL=inbox.js.map