@elisym/sdk 0.7.0 → 0.9.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/README.md +31 -10
- package/dist/agent-store.cjs +5 -2
- package/dist/agent-store.cjs.map +1 -1
- package/dist/agent-store.d.cts +9 -9
- package/dist/agent-store.d.ts +9 -9
- package/dist/agent-store.js +5 -3
- package/dist/agent-store.js.map +1 -1
- package/dist/assets-CMf-v55Z.d.cts +58 -0
- package/dist/assets-CMf-v55Z.d.ts +58 -0
- package/dist/global-schema-CddHP2nk.d.cts +62 -0
- package/dist/global-schema-CddHP2nk.d.ts +62 -0
- package/dist/index.cjs +421 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +151 -9
- package/dist/index.d.ts +151 -9
- package/dist/index.js +409 -26
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +60 -0
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +21 -1
- package/dist/node.d.ts +21 -1
- package/dist/node.js +55 -1
- package/dist/node.js.map +1 -1
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +14 -0
- package/dist/runtime.d.ts +14 -0
- package/dist/runtime.js.map +1 -1
- package/dist/skills.cjs +123 -10
- package/dist/skills.cjs.map +1 -1
- package/dist/skills.d.cts +17 -4
- package/dist/skills.d.ts +17 -4
- package/dist/skills.js +123 -10
- package/dist/skills.js.map +1 -1
- package/package.json +3 -1
package/dist/node.cjs
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
var buffer = require('buffer');
|
|
4
4
|
var crypto = require('crypto');
|
|
5
|
+
var promises = require('fs/promises');
|
|
6
|
+
var YAML2 = require('yaml');
|
|
7
|
+
var path = require('path');
|
|
8
|
+
var zod = require('zod');
|
|
9
|
+
|
|
10
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
+
|
|
12
|
+
var YAML2__default = /*#__PURE__*/_interopDefault(YAML2);
|
|
5
13
|
|
|
6
14
|
// src/primitives/encryption.ts
|
|
7
15
|
var PREFIX = "encrypted:v1:";
|
|
@@ -64,9 +72,61 @@ function decryptSecret(encrypted, passphrase) {
|
|
|
64
72
|
throw new Error("Decryption failed. Wrong passphrase or corrupted data.");
|
|
65
73
|
}
|
|
66
74
|
}
|
|
75
|
+
async function writeFileAtomic(path$1, data, mode) {
|
|
76
|
+
await promises.mkdir(path.dirname(path$1), { recursive: true });
|
|
77
|
+
const tmpPath = `${path$1}.tmp.${crypto.randomBytes(6).toString("hex")}`;
|
|
78
|
+
await promises.writeFile(tmpPath, data, { mode });
|
|
79
|
+
try {
|
|
80
|
+
await promises.rename(tmpPath, path$1);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
try {
|
|
83
|
+
const { unlink } = await import('fs/promises');
|
|
84
|
+
await unlink(tmpPath);
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
throw e;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
var SessionSpendLimitEntrySchema = zod.z.object({
|
|
91
|
+
chain: zod.z.enum(["solana"]),
|
|
92
|
+
token: zod.z.string().min(1).max(16).regex(/^[a-z0-9]+$/, "token must be lowercase alphanumeric"),
|
|
93
|
+
mint: zod.z.string().min(1).max(64).optional(),
|
|
94
|
+
amount: zod.z.number().positive().finite()
|
|
95
|
+
}).strict();
|
|
96
|
+
var GlobalConfigSchema = zod.z.object({
|
|
97
|
+
session_spend_limits: zod.z.array(SessionSpendLimitEntrySchema).max(16).optional()
|
|
98
|
+
}).strict();
|
|
99
|
+
|
|
100
|
+
// src/config/global.ts
|
|
101
|
+
function isEnoent(e) {
|
|
102
|
+
return typeof e === "object" && e !== null && "code" in e && e.code === "ENOENT";
|
|
103
|
+
}
|
|
104
|
+
async function loadGlobalConfig(path) {
|
|
105
|
+
let raw;
|
|
106
|
+
try {
|
|
107
|
+
raw = await promises.readFile(path, "utf-8");
|
|
108
|
+
} catch (e) {
|
|
109
|
+
if (isEnoent(e)) {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
throw e;
|
|
113
|
+
}
|
|
114
|
+
if (raw.trim() === "") {
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
const parsed = YAML2__default.default.parse(raw);
|
|
118
|
+
return GlobalConfigSchema.parse(parsed ?? {});
|
|
119
|
+
}
|
|
120
|
+
async function writeGlobalConfig(path, config) {
|
|
121
|
+
const validated = GlobalConfigSchema.parse(config);
|
|
122
|
+
const body = YAML2__default.default.stringify(validated);
|
|
123
|
+
await writeFileAtomic(path, body, 420);
|
|
124
|
+
}
|
|
67
125
|
|
|
68
126
|
exports.decryptSecret = decryptSecret;
|
|
69
127
|
exports.encryptSecret = encryptSecret;
|
|
70
128
|
exports.isEncrypted = isEncrypted;
|
|
129
|
+
exports.loadGlobalConfig = loadGlobalConfig;
|
|
130
|
+
exports.writeGlobalConfig = writeGlobalConfig;
|
|
71
131
|
//# sourceMappingURL=node.cjs.map
|
|
72
132
|
//# sourceMappingURL=node.cjs.map
|
package/dist/node.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/primitives/encryption.ts"],"names":["randomBytes","scryptSync","createCipheriv","Buffer","createDecipheriv"],"mappings":"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","file":"node.cjs","sourcesContent":["/**\n * Secret encryption/decryption for agent config files.\n * Uses scrypt (KDF) + AES-256-GCM (cipher).\n * Format: \"encrypted:v1:\" + base64(salt[16] + iv[12] + ciphertext + tag[16])\n *\n * scrypt params: N=2^17, r=8, p=1 (~128 MB RAM per derivation).\n *\n * Node.js/Bun only - not available in browsers. Reachable only via the\n * '@elisym/sdk/node' subpath, which browser bundlers will not resolve.\n */\n\nimport { Buffer } from 'node:buffer';\nimport { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';\n\nconst PREFIX = 'encrypted:v1:';\nconst SALT_LENGTH = 16;\nconst IV_LENGTH = 12;\nconst TAG_LENGTH = 16;\nconst KEY_LENGTH = 32; // AES-256\n// v1: N=2^17 (OWASP minimum). v2 will use N=2^20 with format migration.\nconst SCRYPT_N = 2 ** 17;\nconst SCRYPT_R = 8;\nconst SCRYPT_P = 1;\nconst SCRYPT_MAXMEM = 128 * SCRYPT_N * SCRYPT_R * 2; // 2x the minimum required memory\n\n/** Check if a value is encrypted (has the encrypted:v1: prefix). */\nexport function isEncrypted(value: string): boolean {\n return value.startsWith(PREFIX);\n}\n\n/** Encrypt a plaintext secret with a passphrase. Returns \"encrypted:v1:base64...\". Node.js/Bun only. */\nexport function encryptSecret(plaintext: string, passphrase: string): string {\n if (!passphrase) {\n throw new Error('Passphrase must not be empty.');\n }\n\n const salt = randomBytes(SALT_LENGTH);\n const key = scryptSync(passphrase, salt, KEY_LENGTH, {\n N: SCRYPT_N,\n r: SCRYPT_R,\n p: SCRYPT_P,\n maxmem: SCRYPT_MAXMEM,\n });\n const iv = randomBytes(IV_LENGTH);\n\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);\n const tag = cipher.getAuthTag();\n\n const payload = Buffer.concat([salt, iv, encrypted, tag]);\n return PREFIX + payload.toString('base64');\n}\n\n/** Decrypt an encrypted secret with a passphrase. Throws on wrong passphrase or corrupted data. Node.js/Bun only. */\nexport function decryptSecret(encrypted: string, passphrase: string): string {\n if (!isEncrypted(encrypted)) {\n throw new Error('Value is not encrypted (missing encrypted:v1: prefix).');\n }\n if (!passphrase) {\n throw new Error('Passphrase must not be empty.');\n }\n\n const payload = Buffer.from(encrypted.slice(PREFIX.length), 'base64');\n if (payload.length < SALT_LENGTH + IV_LENGTH + TAG_LENGTH) {\n throw new Error('Encrypted payload is too short.');\n }\n\n const salt = payload.subarray(0, SALT_LENGTH);\n const iv = payload.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);\n const tag = payload.subarray(payload.length - TAG_LENGTH);\n const ciphertext = payload.subarray(SALT_LENGTH + IV_LENGTH, payload.length - TAG_LENGTH);\n\n const key = scryptSync(passphrase, salt, KEY_LENGTH, {\n N: SCRYPT_N,\n r: SCRYPT_R,\n p: SCRYPT_P,\n maxmem: SCRYPT_MAXMEM,\n });\n\n const decipher = createDecipheriv('aes-256-gcm', key, iv);\n decipher.setAuthTag(tag);\n\n try {\n const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);\n return decrypted.toString('utf8');\n } catch {\n throw new Error('Decryption failed. Wrong passphrase or corrupted data.');\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/primitives/encryption.ts","../src/agent-store/writer.ts","../src/config/global-schema.ts","../src/config/global.ts"],"names":["randomBytes","scryptSync","createCipheriv","Buffer","createDecipheriv","path","mkdir","dirname","writeFile","rename","z","readFile","YAML"],"mappings":"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","file":"node.cjs","sourcesContent":["/**\n * Secret encryption/decryption for agent config files.\n * Uses scrypt (KDF) + AES-256-GCM (cipher).\n * Format: \"encrypted:v1:\" + base64(salt[16] + iv[12] + ciphertext + tag[16])\n *\n * scrypt params: N=2^17, r=8, p=1 (~128 MB RAM per derivation).\n *\n * Node.js/Bun only - not available in browsers. Reachable only via the\n * '@elisym/sdk/node' subpath, which browser bundlers will not resolve.\n */\n\nimport { Buffer } from 'node:buffer';\nimport { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';\n\nconst PREFIX = 'encrypted:v1:';\nconst SALT_LENGTH = 16;\nconst IV_LENGTH = 12;\nconst TAG_LENGTH = 16;\nconst KEY_LENGTH = 32; // AES-256\n// v1: N=2^17 (OWASP minimum). v2 will use N=2^20 with format migration.\nconst SCRYPT_N = 2 ** 17;\nconst SCRYPT_R = 8;\nconst SCRYPT_P = 1;\nconst SCRYPT_MAXMEM = 128 * SCRYPT_N * SCRYPT_R * 2; // 2x the minimum required memory\n\n/** Check if a value is encrypted (has the encrypted:v1: prefix). */\nexport function isEncrypted(value: string): boolean {\n return value.startsWith(PREFIX);\n}\n\n/** Encrypt a plaintext secret with a passphrase. Returns \"encrypted:v1:base64...\". Node.js/Bun only. */\nexport function encryptSecret(plaintext: string, passphrase: string): string {\n if (!passphrase) {\n throw new Error('Passphrase must not be empty.');\n }\n\n const salt = randomBytes(SALT_LENGTH);\n const key = scryptSync(passphrase, salt, KEY_LENGTH, {\n N: SCRYPT_N,\n r: SCRYPT_R,\n p: SCRYPT_P,\n maxmem: SCRYPT_MAXMEM,\n });\n const iv = randomBytes(IV_LENGTH);\n\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);\n const tag = cipher.getAuthTag();\n\n const payload = Buffer.concat([salt, iv, encrypted, tag]);\n return PREFIX + payload.toString('base64');\n}\n\n/** Decrypt an encrypted secret with a passphrase. Throws on wrong passphrase or corrupted data. Node.js/Bun only. */\nexport function decryptSecret(encrypted: string, passphrase: string): string {\n if (!isEncrypted(encrypted)) {\n throw new Error('Value is not encrypted (missing encrypted:v1: prefix).');\n }\n if (!passphrase) {\n throw new Error('Passphrase must not be empty.');\n }\n\n const payload = Buffer.from(encrypted.slice(PREFIX.length), 'base64');\n if (payload.length < SALT_LENGTH + IV_LENGTH + TAG_LENGTH) {\n throw new Error('Encrypted payload is too short.');\n }\n\n const salt = payload.subarray(0, SALT_LENGTH);\n const iv = payload.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);\n const tag = payload.subarray(payload.length - TAG_LENGTH);\n const ciphertext = payload.subarray(SALT_LENGTH + IV_LENGTH, payload.length - TAG_LENGTH);\n\n const key = scryptSync(passphrase, salt, KEY_LENGTH, {\n N: SCRYPT_N,\n r: SCRYPT_R,\n p: SCRYPT_P,\n maxmem: SCRYPT_MAXMEM,\n });\n\n const decipher = createDecipheriv('aes-256-gcm', key, iv);\n decipher.setAuthTag(tag);\n\n try {\n const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);\n return decrypted.toString('utf8');\n } catch {\n throw new Error('Decryption failed. Wrong passphrase or corrupted data.');\n }\n}\n","/**\n * Write agent files: elisym.yaml, .secrets.json, .gitignore, and create agent dirs.\n */\n\nimport { randomBytes } from 'node:crypto';\nimport { mkdir, rename, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport YAML from 'yaml';\nimport { encryptSecret, isEncrypted } from '../primitives/encryption';\nimport { agentPaths, type AgentPaths } from './paths';\nimport { elisymRootFor, type AgentSource } from './resolver';\nimport { ElisymYamlSchema, SecretsSchema, type ElisymYaml, type Secrets } from './schema';\n\nconst GITIGNORE_CONTENT = [\n '# elisym private state - do not commit.',\n '.secrets.json',\n '.media-cache.json',\n '.jobs.json',\n '.jobs.json.corrupt.*',\n '',\n].join('\\n');\n\nexport interface CreateAgentDirOptions {\n target: AgentSource;\n name: string;\n cwd: string;\n /**\n * For `target: 'project'`: if no .elisym/ dir exists above cwd,\n * where should we create one? Defaults to cwd.\n */\n projectRoot?: string;\n}\n\nexport interface CreatedAgentDir {\n dir: string;\n paths: AgentPaths;\n source: AgentSource;\n createdNewElisymRoot: boolean;\n}\n\n/**\n * Create (or reuse) the directory layout for a new agent. Idempotent: if the\n * agent directory already exists, returns its paths without overwriting.\n * Writes `.gitignore` in project-local .elisym/ on first creation.\n */\nexport async function createAgentDir(options: CreateAgentDirOptions): Promise<CreatedAgentDir> {\n const { target, name, cwd, projectRoot } = options;\n\n const existingRoot = elisymRootFor(target, cwd);\n let elisymRoot: string;\n let createdNewElisymRoot = false;\n\n if (existingRoot) {\n elisymRoot = existingRoot;\n } else if (target === 'project') {\n elisymRoot = join(projectRoot ?? cwd, '.elisym');\n createdNewElisymRoot = true;\n } else {\n throw new Error('homeElisymDir should always exist conceptually - this is unreachable');\n }\n\n const agentDir = join(elisymRoot, name);\n const mode = target === 'home' ? 0o700 : 0o755;\n await mkdir(agentDir, { recursive: true, mode });\n await mkdir(join(agentDir, 'skills'), { recursive: true, mode });\n\n if (target === 'project') {\n const gitignorePath = join(elisymRoot, '.gitignore');\n await writeFileIfMissing(gitignorePath, GITIGNORE_CONTENT, 0o644);\n }\n\n return {\n dir: agentDir,\n paths: agentPaths(agentDir),\n source: target,\n createdNewElisymRoot,\n };\n}\n\n/** Write elisym.yaml atomically. Validates via Zod before writing. */\nexport async function writeYaml(agentDir: string, yaml: ElisymYaml): Promise<void> {\n const validated = ElisymYamlSchema.parse(yaml);\n const body = YAML.stringify(validated);\n const target = agentPaths(agentDir).yaml;\n await writeFileAtomic(target, body, 0o644);\n}\n\n/**\n * Write .secrets.json atomically. If `passphrase` is given, encrypts all\n * plaintext secret fields (already-encrypted values are left as-is).\n */\nexport async function writeSecrets(\n agentDir: string,\n secrets: Secrets,\n passphrase?: string,\n): Promise<void> {\n const validated = SecretsSchema.parse(secrets);\n const finalSecrets: Secrets = {\n ...validated,\n nostr_secret_key: maybeEncrypt(validated.nostr_secret_key, passphrase),\n solana_secret_key: validated.solana_secret_key\n ? maybeEncrypt(validated.solana_secret_key, passphrase)\n : undefined,\n llm_api_key: validated.llm_api_key\n ? maybeEncrypt(validated.llm_api_key, passphrase)\n : undefined,\n };\n const body = JSON.stringify(finalSecrets, null, 2) + '\\n';\n const target = agentPaths(agentDir).secrets;\n await writeFileAtomic(target, body, 0o600);\n}\n\nfunction maybeEncrypt(value: string, passphrase: string | undefined): string {\n if (!passphrase) {\n return value;\n }\n if (isEncrypted(value)) {\n return value;\n }\n return encryptSecret(value, passphrase);\n}\n\n/** Atomic write: temp file + rename. Preserves mode. */\nexport async function writeFileAtomic(\n path: string,\n data: string | Buffer,\n mode: number,\n): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n const tmpPath = `${path}.tmp.${randomBytes(6).toString('hex')}`;\n await writeFile(tmpPath, data, { mode });\n try {\n await rename(tmpPath, path);\n } catch (e) {\n // Best-effort cleanup of temp file on rename failure.\n try {\n const { unlink } = await import('node:fs/promises');\n await unlink(tmpPath);\n } catch {\n /* ignore */\n }\n throw e;\n }\n}\n\nasync function writeFileIfMissing(path: string, data: string, mode: number): Promise<void> {\n try {\n await writeFile(path, data, { mode, flag: 'wx' });\n } catch (e: unknown) {\n // wx fails with EEXIST if file exists - that's fine.\n if (!isEexist(e)) {\n throw e;\n }\n }\n}\n\nfunction isEexist(e: unknown): boolean {\n return (\n typeof e === 'object' && e !== null && 'code' in e && (e as { code: string }).code === 'EEXIST'\n );\n}\n","/**\n * Zod schemas and types for `~/.elisym/config.yaml`.\n *\n * Split from `./global` so the schemas can be re-exported from the\n * browser-safe `@elisym/sdk` entry point without dragging in `node:fs/promises`\n * (which the loader/writer in `./global` needs).\n */\n\nimport { z } from 'zod';\n\nexport const SessionSpendLimitEntrySchema = z\n .object({\n chain: z.enum(['solana']),\n token: z\n .string()\n .min(1)\n .max(16)\n .regex(/^[a-z0-9]+$/, 'token must be lowercase alphanumeric'),\n mint: z.string().min(1).max(64).optional(),\n amount: z.number().positive().finite(),\n })\n .strict();\n\nexport const GlobalConfigSchema = z\n .object({\n session_spend_limits: z.array(SessionSpendLimitEntrySchema).max(16).optional(),\n })\n .strict();\n\nexport type SessionSpendLimitEntry = z.infer<typeof SessionSpendLimitEntrySchema>;\nexport type GlobalConfig = z.infer<typeof GlobalConfigSchema>;\n","/**\n * Global (not per-agent) config stored at `~/.elisym/config.yaml`.\n *\n * Node.js/Bun only - reads and writes the filesystem. Browser code must not\n * import this module; import the schemas from `./global-schema` instead, or\n * the loader/writer from `@elisym/sdk/node`.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport YAML from 'yaml';\nimport { writeFileAtomic } from '../agent-store/writer';\nimport { GlobalConfigSchema, type GlobalConfig } from './global-schema';\n\nexport {\n GlobalConfigSchema,\n SessionSpendLimitEntrySchema,\n type GlobalConfig,\n type SessionSpendLimitEntry,\n} from './global-schema';\n\nfunction isEnoent(e: unknown): boolean {\n return (\n typeof e === 'object' && e !== null && 'code' in e && (e as { code: string }).code === 'ENOENT'\n );\n}\n\n/**\n * Read and validate `~/.elisym/config.yaml`. Returns `{}` if missing. Throws\n * on malformed YAML or schema violations — the MCP server treats these as fatal\n * at startup rather than silently ignoring bad overrides.\n */\nexport async function loadGlobalConfig(path: string): Promise<GlobalConfig> {\n let raw: string;\n try {\n raw = await readFile(path, 'utf-8');\n } catch (e) {\n if (isEnoent(e)) {\n return {};\n }\n throw e;\n }\n if (raw.trim() === '') {\n return {};\n }\n const parsed: unknown = YAML.parse(raw);\n return GlobalConfigSchema.parse(parsed ?? {});\n}\n\n/** Write the config YAML atomically. Validates via Zod before writing. */\nexport async function writeGlobalConfig(path: string, config: GlobalConfig): Promise<void> {\n const validated = GlobalConfigSchema.parse(config);\n const body = YAML.stringify(validated);\n await writeFileAtomic(path, body, 0o644);\n}\n"]}
|
package/dist/node.d.cts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { G as GlobalConfig } from './global-schema-CddHP2nk.cjs';
|
|
2
|
+
import 'zod';
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Secret encryption/decryption for agent config files.
|
|
3
6
|
* Uses scrypt (KDF) + AES-256-GCM (cipher).
|
|
@@ -15,4 +18,21 @@ declare function encryptSecret(plaintext: string, passphrase: string): string;
|
|
|
15
18
|
/** Decrypt an encrypted secret with a passphrase. Throws on wrong passphrase or corrupted data. Node.js/Bun only. */
|
|
16
19
|
declare function decryptSecret(encrypted: string, passphrase: string): string;
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Global (not per-agent) config stored at `~/.elisym/config.yaml`.
|
|
23
|
+
*
|
|
24
|
+
* Node.js/Bun only - reads and writes the filesystem. Browser code must not
|
|
25
|
+
* import this module; import the schemas from `./global-schema` instead, or
|
|
26
|
+
* the loader/writer from `@elisym/sdk/node`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read and validate `~/.elisym/config.yaml`. Returns `{}` if missing. Throws
|
|
31
|
+
* on malformed YAML or schema violations — the MCP server treats these as fatal
|
|
32
|
+
* at startup rather than silently ignoring bad overrides.
|
|
33
|
+
*/
|
|
34
|
+
declare function loadGlobalConfig(path: string): Promise<GlobalConfig>;
|
|
35
|
+
/** Write the config YAML atomically. Validates via Zod before writing. */
|
|
36
|
+
declare function writeGlobalConfig(path: string, config: GlobalConfig): Promise<void>;
|
|
37
|
+
|
|
38
|
+
export { decryptSecret, encryptSecret, isEncrypted, loadGlobalConfig, writeGlobalConfig };
|
package/dist/node.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { G as GlobalConfig } from './global-schema-CddHP2nk.js';
|
|
2
|
+
import 'zod';
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Secret encryption/decryption for agent config files.
|
|
3
6
|
* Uses scrypt (KDF) + AES-256-GCM (cipher).
|
|
@@ -15,4 +18,21 @@ declare function encryptSecret(plaintext: string, passphrase: string): string;
|
|
|
15
18
|
/** Decrypt an encrypted secret with a passphrase. Throws on wrong passphrase or corrupted data. Node.js/Bun only. */
|
|
16
19
|
declare function decryptSecret(encrypted: string, passphrase: string): string;
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Global (not per-agent) config stored at `~/.elisym/config.yaml`.
|
|
23
|
+
*
|
|
24
|
+
* Node.js/Bun only - reads and writes the filesystem. Browser code must not
|
|
25
|
+
* import this module; import the schemas from `./global-schema` instead, or
|
|
26
|
+
* the loader/writer from `@elisym/sdk/node`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read and validate `~/.elisym/config.yaml`. Returns `{}` if missing. Throws
|
|
31
|
+
* on malformed YAML or schema violations — the MCP server treats these as fatal
|
|
32
|
+
* at startup rather than silently ignoring bad overrides.
|
|
33
|
+
*/
|
|
34
|
+
declare function loadGlobalConfig(path: string): Promise<GlobalConfig>;
|
|
35
|
+
/** Write the config YAML atomically. Validates via Zod before writing. */
|
|
36
|
+
declare function writeGlobalConfig(path: string, config: GlobalConfig): Promise<void>;
|
|
37
|
+
|
|
38
|
+
export { decryptSecret, encryptSecret, isEncrypted, loadGlobalConfig, writeGlobalConfig };
|
package/dist/node.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { randomBytes, scryptSync, createCipheriv, createDecipheriv } from 'node:crypto';
|
|
3
|
+
import { readFile, mkdir, writeFile, rename } from 'node:fs/promises';
|
|
4
|
+
import YAML2 from 'yaml';
|
|
5
|
+
import { dirname } from 'node:path';
|
|
6
|
+
import { z } from 'zod';
|
|
3
7
|
|
|
4
8
|
// src/primitives/encryption.ts
|
|
5
9
|
var PREFIX = "encrypted:v1:";
|
|
@@ -62,7 +66,57 @@ function decryptSecret(encrypted, passphrase) {
|
|
|
62
66
|
throw new Error("Decryption failed. Wrong passphrase or corrupted data.");
|
|
63
67
|
}
|
|
64
68
|
}
|
|
69
|
+
async function writeFileAtomic(path, data, mode) {
|
|
70
|
+
await mkdir(dirname(path), { recursive: true });
|
|
71
|
+
const tmpPath = `${path}.tmp.${randomBytes(6).toString("hex")}`;
|
|
72
|
+
await writeFile(tmpPath, data, { mode });
|
|
73
|
+
try {
|
|
74
|
+
await rename(tmpPath, path);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
try {
|
|
77
|
+
const { unlink } = await import('node:fs/promises');
|
|
78
|
+
await unlink(tmpPath);
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
throw e;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
var SessionSpendLimitEntrySchema = z.object({
|
|
85
|
+
chain: z.enum(["solana"]),
|
|
86
|
+
token: z.string().min(1).max(16).regex(/^[a-z0-9]+$/, "token must be lowercase alphanumeric"),
|
|
87
|
+
mint: z.string().min(1).max(64).optional(),
|
|
88
|
+
amount: z.number().positive().finite()
|
|
89
|
+
}).strict();
|
|
90
|
+
var GlobalConfigSchema = z.object({
|
|
91
|
+
session_spend_limits: z.array(SessionSpendLimitEntrySchema).max(16).optional()
|
|
92
|
+
}).strict();
|
|
93
|
+
|
|
94
|
+
// src/config/global.ts
|
|
95
|
+
function isEnoent(e) {
|
|
96
|
+
return typeof e === "object" && e !== null && "code" in e && e.code === "ENOENT";
|
|
97
|
+
}
|
|
98
|
+
async function loadGlobalConfig(path) {
|
|
99
|
+
let raw;
|
|
100
|
+
try {
|
|
101
|
+
raw = await readFile(path, "utf-8");
|
|
102
|
+
} catch (e) {
|
|
103
|
+
if (isEnoent(e)) {
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
throw e;
|
|
107
|
+
}
|
|
108
|
+
if (raw.trim() === "") {
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
const parsed = YAML2.parse(raw);
|
|
112
|
+
return GlobalConfigSchema.parse(parsed ?? {});
|
|
113
|
+
}
|
|
114
|
+
async function writeGlobalConfig(path, config) {
|
|
115
|
+
const validated = GlobalConfigSchema.parse(config);
|
|
116
|
+
const body = YAML2.stringify(validated);
|
|
117
|
+
await writeFileAtomic(path, body, 420);
|
|
118
|
+
}
|
|
65
119
|
|
|
66
|
-
export { decryptSecret, encryptSecret, isEncrypted };
|
|
120
|
+
export { decryptSecret, encryptSecret, isEncrypted, loadGlobalConfig, writeGlobalConfig };
|
|
67
121
|
//# sourceMappingURL=node.js.map
|
|
68
122
|
//# sourceMappingURL=node.js.map
|
package/dist/node.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/primitives/encryption.ts"],"names":[],"mappings":"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","file":"node.js","sourcesContent":["/**\n * Secret encryption/decryption for agent config files.\n * Uses scrypt (KDF) + AES-256-GCM (cipher).\n * Format: \"encrypted:v1:\" + base64(salt[16] + iv[12] + ciphertext + tag[16])\n *\n * scrypt params: N=2^17, r=8, p=1 (~128 MB RAM per derivation).\n *\n * Node.js/Bun only - not available in browsers. Reachable only via the\n * '@elisym/sdk/node' subpath, which browser bundlers will not resolve.\n */\n\nimport { Buffer } from 'node:buffer';\nimport { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';\n\nconst PREFIX = 'encrypted:v1:';\nconst SALT_LENGTH = 16;\nconst IV_LENGTH = 12;\nconst TAG_LENGTH = 16;\nconst KEY_LENGTH = 32; // AES-256\n// v1: N=2^17 (OWASP minimum). v2 will use N=2^20 with format migration.\nconst SCRYPT_N = 2 ** 17;\nconst SCRYPT_R = 8;\nconst SCRYPT_P = 1;\nconst SCRYPT_MAXMEM = 128 * SCRYPT_N * SCRYPT_R * 2; // 2x the minimum required memory\n\n/** Check if a value is encrypted (has the encrypted:v1: prefix). */\nexport function isEncrypted(value: string): boolean {\n return value.startsWith(PREFIX);\n}\n\n/** Encrypt a plaintext secret with a passphrase. Returns \"encrypted:v1:base64...\". Node.js/Bun only. */\nexport function encryptSecret(plaintext: string, passphrase: string): string {\n if (!passphrase) {\n throw new Error('Passphrase must not be empty.');\n }\n\n const salt = randomBytes(SALT_LENGTH);\n const key = scryptSync(passphrase, salt, KEY_LENGTH, {\n N: SCRYPT_N,\n r: SCRYPT_R,\n p: SCRYPT_P,\n maxmem: SCRYPT_MAXMEM,\n });\n const iv = randomBytes(IV_LENGTH);\n\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);\n const tag = cipher.getAuthTag();\n\n const payload = Buffer.concat([salt, iv, encrypted, tag]);\n return PREFIX + payload.toString('base64');\n}\n\n/** Decrypt an encrypted secret with a passphrase. Throws on wrong passphrase or corrupted data. Node.js/Bun only. */\nexport function decryptSecret(encrypted: string, passphrase: string): string {\n if (!isEncrypted(encrypted)) {\n throw new Error('Value is not encrypted (missing encrypted:v1: prefix).');\n }\n if (!passphrase) {\n throw new Error('Passphrase must not be empty.');\n }\n\n const payload = Buffer.from(encrypted.slice(PREFIX.length), 'base64');\n if (payload.length < SALT_LENGTH + IV_LENGTH + TAG_LENGTH) {\n throw new Error('Encrypted payload is too short.');\n }\n\n const salt = payload.subarray(0, SALT_LENGTH);\n const iv = payload.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);\n const tag = payload.subarray(payload.length - TAG_LENGTH);\n const ciphertext = payload.subarray(SALT_LENGTH + IV_LENGTH, payload.length - TAG_LENGTH);\n\n const key = scryptSync(passphrase, salt, KEY_LENGTH, {\n N: SCRYPT_N,\n r: SCRYPT_R,\n p: SCRYPT_P,\n maxmem: SCRYPT_MAXMEM,\n });\n\n const decipher = createDecipheriv('aes-256-gcm', key, iv);\n decipher.setAuthTag(tag);\n\n try {\n const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);\n return decrypted.toString('utf8');\n } catch {\n throw new Error('Decryption failed. Wrong passphrase or corrupted data.');\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/primitives/encryption.ts","../src/agent-store/writer.ts","../src/config/global-schema.ts","../src/config/global.ts"],"names":["randomBytes","YAML"],"mappings":"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","file":"node.js","sourcesContent":["/**\n * Secret encryption/decryption for agent config files.\n * Uses scrypt (KDF) + AES-256-GCM (cipher).\n * Format: \"encrypted:v1:\" + base64(salt[16] + iv[12] + ciphertext + tag[16])\n *\n * scrypt params: N=2^17, r=8, p=1 (~128 MB RAM per derivation).\n *\n * Node.js/Bun only - not available in browsers. Reachable only via the\n * '@elisym/sdk/node' subpath, which browser bundlers will not resolve.\n */\n\nimport { Buffer } from 'node:buffer';\nimport { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';\n\nconst PREFIX = 'encrypted:v1:';\nconst SALT_LENGTH = 16;\nconst IV_LENGTH = 12;\nconst TAG_LENGTH = 16;\nconst KEY_LENGTH = 32; // AES-256\n// v1: N=2^17 (OWASP minimum). v2 will use N=2^20 with format migration.\nconst SCRYPT_N = 2 ** 17;\nconst SCRYPT_R = 8;\nconst SCRYPT_P = 1;\nconst SCRYPT_MAXMEM = 128 * SCRYPT_N * SCRYPT_R * 2; // 2x the minimum required memory\n\n/** Check if a value is encrypted (has the encrypted:v1: prefix). */\nexport function isEncrypted(value: string): boolean {\n return value.startsWith(PREFIX);\n}\n\n/** Encrypt a plaintext secret with a passphrase. Returns \"encrypted:v1:base64...\". Node.js/Bun only. */\nexport function encryptSecret(plaintext: string, passphrase: string): string {\n if (!passphrase) {\n throw new Error('Passphrase must not be empty.');\n }\n\n const salt = randomBytes(SALT_LENGTH);\n const key = scryptSync(passphrase, salt, KEY_LENGTH, {\n N: SCRYPT_N,\n r: SCRYPT_R,\n p: SCRYPT_P,\n maxmem: SCRYPT_MAXMEM,\n });\n const iv = randomBytes(IV_LENGTH);\n\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);\n const tag = cipher.getAuthTag();\n\n const payload = Buffer.concat([salt, iv, encrypted, tag]);\n return PREFIX + payload.toString('base64');\n}\n\n/** Decrypt an encrypted secret with a passphrase. Throws on wrong passphrase or corrupted data. Node.js/Bun only. */\nexport function decryptSecret(encrypted: string, passphrase: string): string {\n if (!isEncrypted(encrypted)) {\n throw new Error('Value is not encrypted (missing encrypted:v1: prefix).');\n }\n if (!passphrase) {\n throw new Error('Passphrase must not be empty.');\n }\n\n const payload = Buffer.from(encrypted.slice(PREFIX.length), 'base64');\n if (payload.length < SALT_LENGTH + IV_LENGTH + TAG_LENGTH) {\n throw new Error('Encrypted payload is too short.');\n }\n\n const salt = payload.subarray(0, SALT_LENGTH);\n const iv = payload.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);\n const tag = payload.subarray(payload.length - TAG_LENGTH);\n const ciphertext = payload.subarray(SALT_LENGTH + IV_LENGTH, payload.length - TAG_LENGTH);\n\n const key = scryptSync(passphrase, salt, KEY_LENGTH, {\n N: SCRYPT_N,\n r: SCRYPT_R,\n p: SCRYPT_P,\n maxmem: SCRYPT_MAXMEM,\n });\n\n const decipher = createDecipheriv('aes-256-gcm', key, iv);\n decipher.setAuthTag(tag);\n\n try {\n const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);\n return decrypted.toString('utf8');\n } catch {\n throw new Error('Decryption failed. Wrong passphrase or corrupted data.');\n }\n}\n","/**\n * Write agent files: elisym.yaml, .secrets.json, .gitignore, and create agent dirs.\n */\n\nimport { randomBytes } from 'node:crypto';\nimport { mkdir, rename, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport YAML from 'yaml';\nimport { encryptSecret, isEncrypted } from '../primitives/encryption';\nimport { agentPaths, type AgentPaths } from './paths';\nimport { elisymRootFor, type AgentSource } from './resolver';\nimport { ElisymYamlSchema, SecretsSchema, type ElisymYaml, type Secrets } from './schema';\n\nconst GITIGNORE_CONTENT = [\n '# elisym private state - do not commit.',\n '.secrets.json',\n '.media-cache.json',\n '.jobs.json',\n '.jobs.json.corrupt.*',\n '',\n].join('\\n');\n\nexport interface CreateAgentDirOptions {\n target: AgentSource;\n name: string;\n cwd: string;\n /**\n * For `target: 'project'`: if no .elisym/ dir exists above cwd,\n * where should we create one? Defaults to cwd.\n */\n projectRoot?: string;\n}\n\nexport interface CreatedAgentDir {\n dir: string;\n paths: AgentPaths;\n source: AgentSource;\n createdNewElisymRoot: boolean;\n}\n\n/**\n * Create (or reuse) the directory layout for a new agent. Idempotent: if the\n * agent directory already exists, returns its paths without overwriting.\n * Writes `.gitignore` in project-local .elisym/ on first creation.\n */\nexport async function createAgentDir(options: CreateAgentDirOptions): Promise<CreatedAgentDir> {\n const { target, name, cwd, projectRoot } = options;\n\n const existingRoot = elisymRootFor(target, cwd);\n let elisymRoot: string;\n let createdNewElisymRoot = false;\n\n if (existingRoot) {\n elisymRoot = existingRoot;\n } else if (target === 'project') {\n elisymRoot = join(projectRoot ?? cwd, '.elisym');\n createdNewElisymRoot = true;\n } else {\n throw new Error('homeElisymDir should always exist conceptually - this is unreachable');\n }\n\n const agentDir = join(elisymRoot, name);\n const mode = target === 'home' ? 0o700 : 0o755;\n await mkdir(agentDir, { recursive: true, mode });\n await mkdir(join(agentDir, 'skills'), { recursive: true, mode });\n\n if (target === 'project') {\n const gitignorePath = join(elisymRoot, '.gitignore');\n await writeFileIfMissing(gitignorePath, GITIGNORE_CONTENT, 0o644);\n }\n\n return {\n dir: agentDir,\n paths: agentPaths(agentDir),\n source: target,\n createdNewElisymRoot,\n };\n}\n\n/** Write elisym.yaml atomically. Validates via Zod before writing. */\nexport async function writeYaml(agentDir: string, yaml: ElisymYaml): Promise<void> {\n const validated = ElisymYamlSchema.parse(yaml);\n const body = YAML.stringify(validated);\n const target = agentPaths(agentDir).yaml;\n await writeFileAtomic(target, body, 0o644);\n}\n\n/**\n * Write .secrets.json atomically. If `passphrase` is given, encrypts all\n * plaintext secret fields (already-encrypted values are left as-is).\n */\nexport async function writeSecrets(\n agentDir: string,\n secrets: Secrets,\n passphrase?: string,\n): Promise<void> {\n const validated = SecretsSchema.parse(secrets);\n const finalSecrets: Secrets = {\n ...validated,\n nostr_secret_key: maybeEncrypt(validated.nostr_secret_key, passphrase),\n solana_secret_key: validated.solana_secret_key\n ? maybeEncrypt(validated.solana_secret_key, passphrase)\n : undefined,\n llm_api_key: validated.llm_api_key\n ? maybeEncrypt(validated.llm_api_key, passphrase)\n : undefined,\n };\n const body = JSON.stringify(finalSecrets, null, 2) + '\\n';\n const target = agentPaths(agentDir).secrets;\n await writeFileAtomic(target, body, 0o600);\n}\n\nfunction maybeEncrypt(value: string, passphrase: string | undefined): string {\n if (!passphrase) {\n return value;\n }\n if (isEncrypted(value)) {\n return value;\n }\n return encryptSecret(value, passphrase);\n}\n\n/** Atomic write: temp file + rename. Preserves mode. */\nexport async function writeFileAtomic(\n path: string,\n data: string | Buffer,\n mode: number,\n): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n const tmpPath = `${path}.tmp.${randomBytes(6).toString('hex')}`;\n await writeFile(tmpPath, data, { mode });\n try {\n await rename(tmpPath, path);\n } catch (e) {\n // Best-effort cleanup of temp file on rename failure.\n try {\n const { unlink } = await import('node:fs/promises');\n await unlink(tmpPath);\n } catch {\n /* ignore */\n }\n throw e;\n }\n}\n\nasync function writeFileIfMissing(path: string, data: string, mode: number): Promise<void> {\n try {\n await writeFile(path, data, { mode, flag: 'wx' });\n } catch (e: unknown) {\n // wx fails with EEXIST if file exists - that's fine.\n if (!isEexist(e)) {\n throw e;\n }\n }\n}\n\nfunction isEexist(e: unknown): boolean {\n return (\n typeof e === 'object' && e !== null && 'code' in e && (e as { code: string }).code === 'EEXIST'\n );\n}\n","/**\n * Zod schemas and types for `~/.elisym/config.yaml`.\n *\n * Split from `./global` so the schemas can be re-exported from the\n * browser-safe `@elisym/sdk` entry point without dragging in `node:fs/promises`\n * (which the loader/writer in `./global` needs).\n */\n\nimport { z } from 'zod';\n\nexport const SessionSpendLimitEntrySchema = z\n .object({\n chain: z.enum(['solana']),\n token: z\n .string()\n .min(1)\n .max(16)\n .regex(/^[a-z0-9]+$/, 'token must be lowercase alphanumeric'),\n mint: z.string().min(1).max(64).optional(),\n amount: z.number().positive().finite(),\n })\n .strict();\n\nexport const GlobalConfigSchema = z\n .object({\n session_spend_limits: z.array(SessionSpendLimitEntrySchema).max(16).optional(),\n })\n .strict();\n\nexport type SessionSpendLimitEntry = z.infer<typeof SessionSpendLimitEntrySchema>;\nexport type GlobalConfig = z.infer<typeof GlobalConfigSchema>;\n","/**\n * Global (not per-agent) config stored at `~/.elisym/config.yaml`.\n *\n * Node.js/Bun only - reads and writes the filesystem. Browser code must not\n * import this module; import the schemas from `./global-schema` instead, or\n * the loader/writer from `@elisym/sdk/node`.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport YAML from 'yaml';\nimport { writeFileAtomic } from '../agent-store/writer';\nimport { GlobalConfigSchema, type GlobalConfig } from './global-schema';\n\nexport {\n GlobalConfigSchema,\n SessionSpendLimitEntrySchema,\n type GlobalConfig,\n type SessionSpendLimitEntry,\n} from './global-schema';\n\nfunction isEnoent(e: unknown): boolean {\n return (\n typeof e === 'object' && e !== null && 'code' in e && (e as { code: string }).code === 'ENOENT'\n );\n}\n\n/**\n * Read and validate `~/.elisym/config.yaml`. Returns `{}` if missing. Throws\n * on malformed YAML or schema violations — the MCP server treats these as fatal\n * at startup rather than silently ignoring bad overrides.\n */\nexport async function loadGlobalConfig(path: string): Promise<GlobalConfig> {\n let raw: string;\n try {\n raw = await readFile(path, 'utf-8');\n } catch (e) {\n if (isEnoent(e)) {\n return {};\n }\n throw e;\n }\n if (raw.trim() === '') {\n return {};\n }\n const parsed: unknown = YAML.parse(raw);\n return GlobalConfigSchema.parse(parsed ?? {});\n}\n\n/** Write the config YAML atomically. Validates via Zod before writing. */\nexport async function writeGlobalConfig(path: string, config: GlobalConfig): Promise<void> {\n const validated = GlobalConfigSchema.parse(config);\n const body = YAML.stringify(validated);\n await writeFileAtomic(path, body, 0o644);\n}\n"]}
|
package/dist/runtime.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/runtime/jobLedger.ts","../src/runtime/memoryAdapter.ts","../src/runtime/recoveryLoop.ts"],"names":[],"mappings":"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","file":"runtime.cjs","sourcesContent":["/**\n * Shared JobLedger types + adapter interface for elisym crash recovery.\n *\n * The ledger is an append-only log of state transitions for each job,\n * keyed by the Nostr job-request event id. Each adapter chooses its own\n * storage backend (Eliza memory, SQLite, tests-only in-memory) and\n * exposes the minimal read/write surface this module defines.\n */\n\nexport type JobSide = 'provider' | 'customer';\n\nexport type ProviderState =\n | 'waiting_payment'\n | 'paid'\n | 'executed'\n | 'delivered'\n | 'failed'\n | 'cancelled';\n\nexport type CustomerState =\n | 'submitted'\n | 'waiting_payment'\n | 'payment_sent'\n | 'result_received'\n | 'failed'\n | 'cancelled';\n\nexport type JobState = ProviderState | CustomerState;\n\nexport const JOB_LEDGER_VERSION = 1;\n\nexport const TERMINAL_STATES: ReadonlySet<JobState> = new Set<JobState>([\n 'delivered',\n 'result_received',\n 'failed',\n 'cancelled',\n]);\n\nexport interface JobLedgerEntry {\n jobEventId: string;\n side: JobSide;\n state: JobState;\n capability: string;\n priceLamports: string;\n rawEventJson?: string;\n customerPubkey?: string;\n providerPubkey?: string;\n input?: string;\n paymentRequestJson?: string;\n txSignature?: string;\n resultContent?: string;\n error?: string;\n retryCount?: number;\n transitionAt: number;\n jobCreatedAt: number;\n version: number;\n}\n\n/**\n * Input to `adapter.write`. The adapter is responsible for stamping\n * `transitionAt` (wall-clock) and `version` so callers can spread an\n * earlier entry safely without carrying those fields forward.\n */\nexport type JobLedgerWriteInput = Omit<JobLedgerEntry, 'transitionAt' | 'version'>;\n\nexport interface JobLedgerAdapter {\n /** Append a new state transition for a job. */\n write(entry: JobLedgerWriteInput): Promise<void>;\n /**\n * Return the latest entry per job id. If `side` is given, restrict to\n * one side; otherwise include both.\n */\n loadLatest(side?: JobSide): Promise<Map<string, JobLedgerEntry>>;\n /**\n * Delete terminal entries whose last transition happened before\n * `now - retentionMs`. Non-terminal entries are retained so recovery\n * can keep retrying. Returns the number of entries dropped.\n */\n pruneOldEntries(retentionMs: number): Promise<number>;\n}\n\n/**\n * Convenience: return non-terminal entries for `side`, oldest-first by\n * `jobCreatedAt`. Derived from `loadLatest` so any adapter satisfies it.\n */\nexport async function pendingJobs(\n adapter: JobLedgerAdapter,\n side: JobSide,\n): Promise<JobLedgerEntry[]> {\n const latest = await adapter.loadLatest(side);\n const pending: JobLedgerEntry[] = [];\n for (const entry of latest.values()) {\n if (!TERMINAL_STATES.has(entry.state)) {\n pending.push(entry);\n }\n }\n pending.sort((left, right) => left.jobCreatedAt - right.jobCreatedAt);\n return pending;\n}\n\nexport async function findByJobId(\n adapter: JobLedgerAdapter,\n jobEventId: string,\n): Promise<JobLedgerEntry | undefined> {\n const latest = await adapter.loadLatest();\n return latest.get(jobEventId);\n}\n","import {\n JOB_LEDGER_VERSION,\n TERMINAL_STATES,\n type JobLedgerAdapter,\n type JobLedgerEntry,\n type JobLedgerWriteInput,\n type JobSide,\n} from './jobLedger';\n\ninterface Row {\n entry: JobLedgerEntry;\n /** Row timestamp, separate from entry.transitionAt so we can resolve ties. */\n rowAt: number;\n}\n\n/**\n * In-memory reference adapter. Useful for tests and ephemeral deployments\n * where no durable backing store is required. Not suitable for real\n * crash-recovery (everything is lost on process exit).\n */\nexport function createMemoryJobLedgerAdapter(): JobLedgerAdapter & {\n /** Test-only: drop all rows. */\n clear(): void;\n /** Test-only: inspect the raw append log. */\n rows(): ReadonlyArray<JobLedgerEntry>;\n} {\n const log: Row[] = [];\n let rowCounter = 0;\n\n function latestByJobId(side?: JobSide): Map<string, JobLedgerEntry> {\n const latest = new Map<string, JobLedgerEntry>();\n const latestRowAt = new Map<string, number>();\n for (const row of log) {\n if (side && row.entry.side !== side) {\n continue;\n }\n const jobId = row.entry.jobEventId;\n const previous = latestRowAt.get(jobId) ?? -Infinity;\n if (row.rowAt >= previous) {\n latest.set(jobId, row.entry);\n latestRowAt.set(jobId, row.rowAt);\n }\n }\n return latest;\n }\n\n return {\n async write(entry: JobLedgerWriteInput): Promise<void> {\n const finalized: JobLedgerEntry = {\n ...entry,\n transitionAt: Date.now(),\n version: JOB_LEDGER_VERSION,\n };\n rowCounter += 1;\n log.push({ entry: finalized, rowAt: rowCounter });\n },\n async loadLatest(side?: JobSide): Promise<Map<string, JobLedgerEntry>> {\n return latestByJobId(side);\n },\n async pruneOldEntries(retentionMs: number): Promise<number> {\n const cutoff = Date.now() - retentionMs;\n const latest = latestByJobId();\n const dropIds = new Set<string>();\n for (const [jobId, entry] of latest) {\n if (TERMINAL_STATES.has(entry.state) && entry.transitionAt < cutoff) {\n dropIds.add(jobId);\n }\n }\n if (dropIds.size === 0) {\n return 0;\n }\n let deleted = 0;\n for (let index = log.length - 1; index >= 0; index--) {\n const row = log[index];\n if (row && dropIds.has(row.entry.jobEventId)) {\n log.splice(index, 1);\n deleted += 1;\n }\n }\n return deleted;\n },\n clear(): void {\n log.length = 0;\n rowCounter = 0;\n },\n rows(): ReadonlyArray<JobLedgerEntry> {\n return log.map((row) => row.entry);\n },\n };\n}\n","import { pendingJobs, type JobLedgerAdapter, type JobLedgerEntry, type JobSide } from './jobLedger';\n\nexport interface RecoveryLoopLogger {\n info?(obj: Record<string, unknown>, msg?: string): void;\n warn?(obj: Record<string, unknown>, msg?: string): void;\n debug?(obj: Record<string, unknown>, msg?: string): void;\n}\n\nexport interface RecoveryLoopOptions {\n adapter: JobLedgerAdapter;\n /** Called for each non-terminal provider-side entry. Should advance state. */\n onProviderPending?: (entry: JobLedgerEntry) => Promise<void>;\n /** Called for each non-terminal customer-side entry. Should advance state. */\n onCustomerPending?: (entry: JobLedgerEntry) => Promise<void>;\n /** Sweep cadence. */\n intervalMs: number;\n /** Retention for terminal entries. Passed to adapter.pruneOldEntries each sweep. */\n retentionMs: number;\n /** Concurrent per-job worker count during a sweep. */\n concurrency: number;\n /** Optional structured logger. Falls back to silence. */\n logger?: RecoveryLoopLogger;\n}\n\nexport interface RecoveryLoop {\n /** Kick off an initial sweep (non-blocking) and start the periodic timer. */\n start(): void;\n /** Stop the periodic timer. Does not cancel an in-flight sweep. */\n stop(): void;\n /** Run a single sweep synchronously. Useful for tests or manual triggers. */\n sweepOnce(): Promise<void>;\n}\n\n/**\n * Generic recovery scaffold: periodic pruning + concurrent replay of\n * non-terminal ledger entries. Per-side handlers own the business\n * semantics (retry budget, re-execute, payment verify, delivery retry).\n *\n * Notes:\n * - Overlap guard: if the previous sweep is still in flight, subsequent\n * tick fires are skipped - no queueing. The next on-schedule fire will\n * pick up where the previous left off (ledger is idempotent).\n * - Handler errors are caught and logged at `warn` so a single bad entry\n * doesn't poison the batch.\n */\nexport function createRecoveryLoop(options: RecoveryLoopOptions): RecoveryLoop {\n const { adapter, onProviderPending, onCustomerPending, intervalMs, retentionMs, concurrency } =\n options;\n const logger = options.logger ?? {};\n if (intervalMs <= 0) {\n throw new RangeError('intervalMs must be > 0');\n }\n if (concurrency <= 0) {\n throw new RangeError('concurrency must be > 0');\n }\n if (retentionMs <= 0) {\n throw new RangeError('retentionMs must be > 0');\n }\n\n let timer: ReturnType<typeof setInterval> | undefined;\n let running = false;\n\n async function runBatch(\n entries: JobLedgerEntry[],\n handler: (entry: JobLedgerEntry) => Promise<void>,\n ): Promise<void> {\n let index = 0;\n const workerCount = Math.min(concurrency, entries.length);\n const workers = Array.from({ length: workerCount }, async () => {\n while (index < entries.length) {\n const currentIndex = index++;\n const entry = entries[currentIndex];\n if (!entry) {\n continue;\n }\n try {\n await handler(entry);\n } catch (error) {\n logger.warn?.(\n {\n err: error instanceof Error ? error.message : String(error),\n jobEventId: entry.jobEventId,\n side: entry.side,\n state: entry.state,\n },\n 'recovery handler threw',\n );\n }\n }\n });\n await Promise.all(workers);\n }\n\n async function sweepSide(\n side: JobSide,\n handler: ((entry: JobLedgerEntry) => Promise<void>) | undefined,\n ): Promise<void> {\n if (!handler) {\n return;\n }\n const pending = await pendingJobs(adapter, side);\n if (pending.length === 0) {\n return;\n }\n logger.info?.({ [side]: pending.length }, 'recovery sweep: resuming pending jobs');\n await runBatch(pending, handler);\n }\n\n async function sweepOnce(): Promise<void> {\n if (running) {\n return;\n }\n running = true;\n try {\n try {\n await adapter.pruneOldEntries(retentionMs);\n } catch (error) {\n logger.warn?.(\n { err: error instanceof Error ? error.message : String(error) },\n 'recovery: pruneOldEntries failed',\n );\n }\n await sweepSide('provider', onProviderPending);\n await sweepSide('customer', onCustomerPending);\n } finally {\n running = false;\n }\n }\n\n function start(): void {\n if (timer) {\n return;\n }\n // Initial sweep kicked off in background so start() returns quickly.\n sweepOnce().catch((error) =>\n logger.warn?.(\n { err: error instanceof Error ? error.message : String(error) },\n 'initial recovery sweep failed',\n ),\n );\n timer = setInterval(() => {\n sweepOnce().catch((error) =>\n logger.warn?.(\n { err: error instanceof Error ? error.message : String(error) },\n 'recovery sweep failed',\n ),\n );\n }, intervalMs);\n }\n\n function stop(): void {\n if (timer) {\n clearInterval(timer);\n timer = undefined;\n }\n }\n\n return { start, stop, sweepOnce };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/runtime/jobLedger.ts","../src/runtime/memoryAdapter.ts","../src/runtime/recoveryLoop.ts"],"names":[],"mappings":"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","file":"runtime.cjs","sourcesContent":["/**\n * Shared JobLedger types + adapter interface for elisym crash recovery.\n *\n * The ledger is an append-only log of state transitions for each job,\n * keyed by the Nostr job-request event id. Each adapter chooses its own\n * storage backend (Eliza memory, SQLite, tests-only in-memory) and\n * exposes the minimal read/write surface this module defines.\n */\n\nexport type JobSide = 'provider' | 'customer';\n\nexport type ProviderState =\n | 'waiting_payment'\n | 'paid'\n | 'executed'\n | 'delivered'\n | 'failed'\n | 'cancelled';\n\nexport type CustomerState =\n | 'submitted'\n | 'waiting_payment'\n | 'payment_sent'\n | 'result_received'\n | 'failed'\n | 'cancelled';\n\nexport type JobState = ProviderState | CustomerState;\n\nexport const JOB_LEDGER_VERSION = 1;\n\nexport const TERMINAL_STATES: ReadonlySet<JobState> = new Set<JobState>([\n 'delivered',\n 'result_received',\n 'failed',\n 'cancelled',\n]);\n\nexport interface JobLedgerEntry {\n jobEventId: string;\n side: JobSide;\n state: JobState;\n capability: string;\n /**\n * Price in subunits of the payment asset, stored as a decimal string\n * (bigint-safe on disk).\n *\n * Kept named `priceLamports` for persistence back-compat: old ledger files\n * use this name and interpret the value as lamports (SOL subunits). When\n * `assetKey` is set (new USDC flows), interpret the same string as subunits\n * of that asset (e.g. 1 USDC = 1_000_000).\n */\n priceLamports: string;\n /**\n * Optional asset key (`solana:sol` / `solana:usdc:<mint>`). Absent =>\n * native SOL (back-compat with pre-USDC ledger entries).\n */\n assetKey?: string;\n rawEventJson?: string;\n customerPubkey?: string;\n providerPubkey?: string;\n input?: string;\n paymentRequestJson?: string;\n txSignature?: string;\n resultContent?: string;\n error?: string;\n retryCount?: number;\n transitionAt: number;\n jobCreatedAt: number;\n version: number;\n}\n\n/**\n * Input to `adapter.write`. The adapter is responsible for stamping\n * `transitionAt` (wall-clock) and `version` so callers can spread an\n * earlier entry safely without carrying those fields forward.\n */\nexport type JobLedgerWriteInput = Omit<JobLedgerEntry, 'transitionAt' | 'version'>;\n\nexport interface JobLedgerAdapter {\n /** Append a new state transition for a job. */\n write(entry: JobLedgerWriteInput): Promise<void>;\n /**\n * Return the latest entry per job id. If `side` is given, restrict to\n * one side; otherwise include both.\n */\n loadLatest(side?: JobSide): Promise<Map<string, JobLedgerEntry>>;\n /**\n * Delete terminal entries whose last transition happened before\n * `now - retentionMs`. Non-terminal entries are retained so recovery\n * can keep retrying. Returns the number of entries dropped.\n */\n pruneOldEntries(retentionMs: number): Promise<number>;\n}\n\n/**\n * Convenience: return non-terminal entries for `side`, oldest-first by\n * `jobCreatedAt`. Derived from `loadLatest` so any adapter satisfies it.\n */\nexport async function pendingJobs(\n adapter: JobLedgerAdapter,\n side: JobSide,\n): Promise<JobLedgerEntry[]> {\n const latest = await adapter.loadLatest(side);\n const pending: JobLedgerEntry[] = [];\n for (const entry of latest.values()) {\n if (!TERMINAL_STATES.has(entry.state)) {\n pending.push(entry);\n }\n }\n pending.sort((left, right) => left.jobCreatedAt - right.jobCreatedAt);\n return pending;\n}\n\nexport async function findByJobId(\n adapter: JobLedgerAdapter,\n jobEventId: string,\n): Promise<JobLedgerEntry | undefined> {\n const latest = await adapter.loadLatest();\n return latest.get(jobEventId);\n}\n","import {\n JOB_LEDGER_VERSION,\n TERMINAL_STATES,\n type JobLedgerAdapter,\n type JobLedgerEntry,\n type JobLedgerWriteInput,\n type JobSide,\n} from './jobLedger';\n\ninterface Row {\n entry: JobLedgerEntry;\n /** Row timestamp, separate from entry.transitionAt so we can resolve ties. */\n rowAt: number;\n}\n\n/**\n * In-memory reference adapter. Useful for tests and ephemeral deployments\n * where no durable backing store is required. Not suitable for real\n * crash-recovery (everything is lost on process exit).\n */\nexport function createMemoryJobLedgerAdapter(): JobLedgerAdapter & {\n /** Test-only: drop all rows. */\n clear(): void;\n /** Test-only: inspect the raw append log. */\n rows(): ReadonlyArray<JobLedgerEntry>;\n} {\n const log: Row[] = [];\n let rowCounter = 0;\n\n function latestByJobId(side?: JobSide): Map<string, JobLedgerEntry> {\n const latest = new Map<string, JobLedgerEntry>();\n const latestRowAt = new Map<string, number>();\n for (const row of log) {\n if (side && row.entry.side !== side) {\n continue;\n }\n const jobId = row.entry.jobEventId;\n const previous = latestRowAt.get(jobId) ?? -Infinity;\n if (row.rowAt >= previous) {\n latest.set(jobId, row.entry);\n latestRowAt.set(jobId, row.rowAt);\n }\n }\n return latest;\n }\n\n return {\n async write(entry: JobLedgerWriteInput): Promise<void> {\n const finalized: JobLedgerEntry = {\n ...entry,\n transitionAt: Date.now(),\n version: JOB_LEDGER_VERSION,\n };\n rowCounter += 1;\n log.push({ entry: finalized, rowAt: rowCounter });\n },\n async loadLatest(side?: JobSide): Promise<Map<string, JobLedgerEntry>> {\n return latestByJobId(side);\n },\n async pruneOldEntries(retentionMs: number): Promise<number> {\n const cutoff = Date.now() - retentionMs;\n const latest = latestByJobId();\n const dropIds = new Set<string>();\n for (const [jobId, entry] of latest) {\n if (TERMINAL_STATES.has(entry.state) && entry.transitionAt < cutoff) {\n dropIds.add(jobId);\n }\n }\n if (dropIds.size === 0) {\n return 0;\n }\n let deleted = 0;\n for (let index = log.length - 1; index >= 0; index--) {\n const row = log[index];\n if (row && dropIds.has(row.entry.jobEventId)) {\n log.splice(index, 1);\n deleted += 1;\n }\n }\n return deleted;\n },\n clear(): void {\n log.length = 0;\n rowCounter = 0;\n },\n rows(): ReadonlyArray<JobLedgerEntry> {\n return log.map((row) => row.entry);\n },\n };\n}\n","import { pendingJobs, type JobLedgerAdapter, type JobLedgerEntry, type JobSide } from './jobLedger';\n\nexport interface RecoveryLoopLogger {\n info?(obj: Record<string, unknown>, msg?: string): void;\n warn?(obj: Record<string, unknown>, msg?: string): void;\n debug?(obj: Record<string, unknown>, msg?: string): void;\n}\n\nexport interface RecoveryLoopOptions {\n adapter: JobLedgerAdapter;\n /** Called for each non-terminal provider-side entry. Should advance state. */\n onProviderPending?: (entry: JobLedgerEntry) => Promise<void>;\n /** Called for each non-terminal customer-side entry. Should advance state. */\n onCustomerPending?: (entry: JobLedgerEntry) => Promise<void>;\n /** Sweep cadence. */\n intervalMs: number;\n /** Retention for terminal entries. Passed to adapter.pruneOldEntries each sweep. */\n retentionMs: number;\n /** Concurrent per-job worker count during a sweep. */\n concurrency: number;\n /** Optional structured logger. Falls back to silence. */\n logger?: RecoveryLoopLogger;\n}\n\nexport interface RecoveryLoop {\n /** Kick off an initial sweep (non-blocking) and start the periodic timer. */\n start(): void;\n /** Stop the periodic timer. Does not cancel an in-flight sweep. */\n stop(): void;\n /** Run a single sweep synchronously. Useful for tests or manual triggers. */\n sweepOnce(): Promise<void>;\n}\n\n/**\n * Generic recovery scaffold: periodic pruning + concurrent replay of\n * non-terminal ledger entries. Per-side handlers own the business\n * semantics (retry budget, re-execute, payment verify, delivery retry).\n *\n * Notes:\n * - Overlap guard: if the previous sweep is still in flight, subsequent\n * tick fires are skipped - no queueing. The next on-schedule fire will\n * pick up where the previous left off (ledger is idempotent).\n * - Handler errors are caught and logged at `warn` so a single bad entry\n * doesn't poison the batch.\n */\nexport function createRecoveryLoop(options: RecoveryLoopOptions): RecoveryLoop {\n const { adapter, onProviderPending, onCustomerPending, intervalMs, retentionMs, concurrency } =\n options;\n const logger = options.logger ?? {};\n if (intervalMs <= 0) {\n throw new RangeError('intervalMs must be > 0');\n }\n if (concurrency <= 0) {\n throw new RangeError('concurrency must be > 0');\n }\n if (retentionMs <= 0) {\n throw new RangeError('retentionMs must be > 0');\n }\n\n let timer: ReturnType<typeof setInterval> | undefined;\n let running = false;\n\n async function runBatch(\n entries: JobLedgerEntry[],\n handler: (entry: JobLedgerEntry) => Promise<void>,\n ): Promise<void> {\n let index = 0;\n const workerCount = Math.min(concurrency, entries.length);\n const workers = Array.from({ length: workerCount }, async () => {\n while (index < entries.length) {\n const currentIndex = index++;\n const entry = entries[currentIndex];\n if (!entry) {\n continue;\n }\n try {\n await handler(entry);\n } catch (error) {\n logger.warn?.(\n {\n err: error instanceof Error ? error.message : String(error),\n jobEventId: entry.jobEventId,\n side: entry.side,\n state: entry.state,\n },\n 'recovery handler threw',\n );\n }\n }\n });\n await Promise.all(workers);\n }\n\n async function sweepSide(\n side: JobSide,\n handler: ((entry: JobLedgerEntry) => Promise<void>) | undefined,\n ): Promise<void> {\n if (!handler) {\n return;\n }\n const pending = await pendingJobs(adapter, side);\n if (pending.length === 0) {\n return;\n }\n logger.info?.({ [side]: pending.length }, 'recovery sweep: resuming pending jobs');\n await runBatch(pending, handler);\n }\n\n async function sweepOnce(): Promise<void> {\n if (running) {\n return;\n }\n running = true;\n try {\n try {\n await adapter.pruneOldEntries(retentionMs);\n } catch (error) {\n logger.warn?.(\n { err: error instanceof Error ? error.message : String(error) },\n 'recovery: pruneOldEntries failed',\n );\n }\n await sweepSide('provider', onProviderPending);\n await sweepSide('customer', onCustomerPending);\n } finally {\n running = false;\n }\n }\n\n function start(): void {\n if (timer) {\n return;\n }\n // Initial sweep kicked off in background so start() returns quickly.\n sweepOnce().catch((error) =>\n logger.warn?.(\n { err: error instanceof Error ? error.message : String(error) },\n 'initial recovery sweep failed',\n ),\n );\n timer = setInterval(() => {\n sweepOnce().catch((error) =>\n logger.warn?.(\n { err: error instanceof Error ? error.message : String(error) },\n 'recovery sweep failed',\n ),\n );\n }, intervalMs);\n }\n\n function stop(): void {\n if (timer) {\n clearInterval(timer);\n timer = undefined;\n }\n }\n\n return { start, stop, sweepOnce };\n}\n"]}
|
package/dist/runtime.d.cts
CHANGED
|
@@ -17,7 +17,21 @@ interface JobLedgerEntry {
|
|
|
17
17
|
side: JobSide;
|
|
18
18
|
state: JobState;
|
|
19
19
|
capability: string;
|
|
20
|
+
/**
|
|
21
|
+
* Price in subunits of the payment asset, stored as a decimal string
|
|
22
|
+
* (bigint-safe on disk).
|
|
23
|
+
*
|
|
24
|
+
* Kept named `priceLamports` for persistence back-compat: old ledger files
|
|
25
|
+
* use this name and interpret the value as lamports (SOL subunits). When
|
|
26
|
+
* `assetKey` is set (new USDC flows), interpret the same string as subunits
|
|
27
|
+
* of that asset (e.g. 1 USDC = 1_000_000).
|
|
28
|
+
*/
|
|
20
29
|
priceLamports: string;
|
|
30
|
+
/**
|
|
31
|
+
* Optional asset key (`solana:sol` / `solana:usdc:<mint>`). Absent =>
|
|
32
|
+
* native SOL (back-compat with pre-USDC ledger entries).
|
|
33
|
+
*/
|
|
34
|
+
assetKey?: string;
|
|
21
35
|
rawEventJson?: string;
|
|
22
36
|
customerPubkey?: string;
|
|
23
37
|
providerPubkey?: string;
|
package/dist/runtime.d.ts
CHANGED
|
@@ -17,7 +17,21 @@ interface JobLedgerEntry {
|
|
|
17
17
|
side: JobSide;
|
|
18
18
|
state: JobState;
|
|
19
19
|
capability: string;
|
|
20
|
+
/**
|
|
21
|
+
* Price in subunits of the payment asset, stored as a decimal string
|
|
22
|
+
* (bigint-safe on disk).
|
|
23
|
+
*
|
|
24
|
+
* Kept named `priceLamports` for persistence back-compat: old ledger files
|
|
25
|
+
* use this name and interpret the value as lamports (SOL subunits). When
|
|
26
|
+
* `assetKey` is set (new USDC flows), interpret the same string as subunits
|
|
27
|
+
* of that asset (e.g. 1 USDC = 1_000_000).
|
|
28
|
+
*/
|
|
20
29
|
priceLamports: string;
|
|
30
|
+
/**
|
|
31
|
+
* Optional asset key (`solana:sol` / `solana:usdc:<mint>`). Absent =>
|
|
32
|
+
* native SOL (back-compat with pre-USDC ledger entries).
|
|
33
|
+
*/
|
|
34
|
+
assetKey?: string;
|
|
21
35
|
rawEventJson?: string;
|
|
22
36
|
customerPubkey?: string;
|
|
23
37
|
providerPubkey?: string;
|
package/dist/runtime.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/runtime/jobLedger.ts","../src/runtime/memoryAdapter.ts","../src/runtime/recoveryLoop.ts"],"names":[],"mappings":"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","file":"runtime.js","sourcesContent":["/**\n * Shared JobLedger types + adapter interface for elisym crash recovery.\n *\n * The ledger is an append-only log of state transitions for each job,\n * keyed by the Nostr job-request event id. Each adapter chooses its own\n * storage backend (Eliza memory, SQLite, tests-only in-memory) and\n * exposes the minimal read/write surface this module defines.\n */\n\nexport type JobSide = 'provider' | 'customer';\n\nexport type ProviderState =\n | 'waiting_payment'\n | 'paid'\n | 'executed'\n | 'delivered'\n | 'failed'\n | 'cancelled';\n\nexport type CustomerState =\n | 'submitted'\n | 'waiting_payment'\n | 'payment_sent'\n | 'result_received'\n | 'failed'\n | 'cancelled';\n\nexport type JobState = ProviderState | CustomerState;\n\nexport const JOB_LEDGER_VERSION = 1;\n\nexport const TERMINAL_STATES: ReadonlySet<JobState> = new Set<JobState>([\n 'delivered',\n 'result_received',\n 'failed',\n 'cancelled',\n]);\n\nexport interface JobLedgerEntry {\n jobEventId: string;\n side: JobSide;\n state: JobState;\n capability: string;\n priceLamports: string;\n rawEventJson?: string;\n customerPubkey?: string;\n providerPubkey?: string;\n input?: string;\n paymentRequestJson?: string;\n txSignature?: string;\n resultContent?: string;\n error?: string;\n retryCount?: number;\n transitionAt: number;\n jobCreatedAt: number;\n version: number;\n}\n\n/**\n * Input to `adapter.write`. The adapter is responsible for stamping\n * `transitionAt` (wall-clock) and `version` so callers can spread an\n * earlier entry safely without carrying those fields forward.\n */\nexport type JobLedgerWriteInput = Omit<JobLedgerEntry, 'transitionAt' | 'version'>;\n\nexport interface JobLedgerAdapter {\n /** Append a new state transition for a job. */\n write(entry: JobLedgerWriteInput): Promise<void>;\n /**\n * Return the latest entry per job id. If `side` is given, restrict to\n * one side; otherwise include both.\n */\n loadLatest(side?: JobSide): Promise<Map<string, JobLedgerEntry>>;\n /**\n * Delete terminal entries whose last transition happened before\n * `now - retentionMs`. Non-terminal entries are retained so recovery\n * can keep retrying. Returns the number of entries dropped.\n */\n pruneOldEntries(retentionMs: number): Promise<number>;\n}\n\n/**\n * Convenience: return non-terminal entries for `side`, oldest-first by\n * `jobCreatedAt`. Derived from `loadLatest` so any adapter satisfies it.\n */\nexport async function pendingJobs(\n adapter: JobLedgerAdapter,\n side: JobSide,\n): Promise<JobLedgerEntry[]> {\n const latest = await adapter.loadLatest(side);\n const pending: JobLedgerEntry[] = [];\n for (const entry of latest.values()) {\n if (!TERMINAL_STATES.has(entry.state)) {\n pending.push(entry);\n }\n }\n pending.sort((left, right) => left.jobCreatedAt - right.jobCreatedAt);\n return pending;\n}\n\nexport async function findByJobId(\n adapter: JobLedgerAdapter,\n jobEventId: string,\n): Promise<JobLedgerEntry | undefined> {\n const latest = await adapter.loadLatest();\n return latest.get(jobEventId);\n}\n","import {\n JOB_LEDGER_VERSION,\n TERMINAL_STATES,\n type JobLedgerAdapter,\n type JobLedgerEntry,\n type JobLedgerWriteInput,\n type JobSide,\n} from './jobLedger';\n\ninterface Row {\n entry: JobLedgerEntry;\n /** Row timestamp, separate from entry.transitionAt so we can resolve ties. */\n rowAt: number;\n}\n\n/**\n * In-memory reference adapter. Useful for tests and ephemeral deployments\n * where no durable backing store is required. Not suitable for real\n * crash-recovery (everything is lost on process exit).\n */\nexport function createMemoryJobLedgerAdapter(): JobLedgerAdapter & {\n /** Test-only: drop all rows. */\n clear(): void;\n /** Test-only: inspect the raw append log. */\n rows(): ReadonlyArray<JobLedgerEntry>;\n} {\n const log: Row[] = [];\n let rowCounter = 0;\n\n function latestByJobId(side?: JobSide): Map<string, JobLedgerEntry> {\n const latest = new Map<string, JobLedgerEntry>();\n const latestRowAt = new Map<string, number>();\n for (const row of log) {\n if (side && row.entry.side !== side) {\n continue;\n }\n const jobId = row.entry.jobEventId;\n const previous = latestRowAt.get(jobId) ?? -Infinity;\n if (row.rowAt >= previous) {\n latest.set(jobId, row.entry);\n latestRowAt.set(jobId, row.rowAt);\n }\n }\n return latest;\n }\n\n return {\n async write(entry: JobLedgerWriteInput): Promise<void> {\n const finalized: JobLedgerEntry = {\n ...entry,\n transitionAt: Date.now(),\n version: JOB_LEDGER_VERSION,\n };\n rowCounter += 1;\n log.push({ entry: finalized, rowAt: rowCounter });\n },\n async loadLatest(side?: JobSide): Promise<Map<string, JobLedgerEntry>> {\n return latestByJobId(side);\n },\n async pruneOldEntries(retentionMs: number): Promise<number> {\n const cutoff = Date.now() - retentionMs;\n const latest = latestByJobId();\n const dropIds = new Set<string>();\n for (const [jobId, entry] of latest) {\n if (TERMINAL_STATES.has(entry.state) && entry.transitionAt < cutoff) {\n dropIds.add(jobId);\n }\n }\n if (dropIds.size === 0) {\n return 0;\n }\n let deleted = 0;\n for (let index = log.length - 1; index >= 0; index--) {\n const row = log[index];\n if (row && dropIds.has(row.entry.jobEventId)) {\n log.splice(index, 1);\n deleted += 1;\n }\n }\n return deleted;\n },\n clear(): void {\n log.length = 0;\n rowCounter = 0;\n },\n rows(): ReadonlyArray<JobLedgerEntry> {\n return log.map((row) => row.entry);\n },\n };\n}\n","import { pendingJobs, type JobLedgerAdapter, type JobLedgerEntry, type JobSide } from './jobLedger';\n\nexport interface RecoveryLoopLogger {\n info?(obj: Record<string, unknown>, msg?: string): void;\n warn?(obj: Record<string, unknown>, msg?: string): void;\n debug?(obj: Record<string, unknown>, msg?: string): void;\n}\n\nexport interface RecoveryLoopOptions {\n adapter: JobLedgerAdapter;\n /** Called for each non-terminal provider-side entry. Should advance state. */\n onProviderPending?: (entry: JobLedgerEntry) => Promise<void>;\n /** Called for each non-terminal customer-side entry. Should advance state. */\n onCustomerPending?: (entry: JobLedgerEntry) => Promise<void>;\n /** Sweep cadence. */\n intervalMs: number;\n /** Retention for terminal entries. Passed to adapter.pruneOldEntries each sweep. */\n retentionMs: number;\n /** Concurrent per-job worker count during a sweep. */\n concurrency: number;\n /** Optional structured logger. Falls back to silence. */\n logger?: RecoveryLoopLogger;\n}\n\nexport interface RecoveryLoop {\n /** Kick off an initial sweep (non-blocking) and start the periodic timer. */\n start(): void;\n /** Stop the periodic timer. Does not cancel an in-flight sweep. */\n stop(): void;\n /** Run a single sweep synchronously. Useful for tests or manual triggers. */\n sweepOnce(): Promise<void>;\n}\n\n/**\n * Generic recovery scaffold: periodic pruning + concurrent replay of\n * non-terminal ledger entries. Per-side handlers own the business\n * semantics (retry budget, re-execute, payment verify, delivery retry).\n *\n * Notes:\n * - Overlap guard: if the previous sweep is still in flight, subsequent\n * tick fires are skipped - no queueing. The next on-schedule fire will\n * pick up where the previous left off (ledger is idempotent).\n * - Handler errors are caught and logged at `warn` so a single bad entry\n * doesn't poison the batch.\n */\nexport function createRecoveryLoop(options: RecoveryLoopOptions): RecoveryLoop {\n const { adapter, onProviderPending, onCustomerPending, intervalMs, retentionMs, concurrency } =\n options;\n const logger = options.logger ?? {};\n if (intervalMs <= 0) {\n throw new RangeError('intervalMs must be > 0');\n }\n if (concurrency <= 0) {\n throw new RangeError('concurrency must be > 0');\n }\n if (retentionMs <= 0) {\n throw new RangeError('retentionMs must be > 0');\n }\n\n let timer: ReturnType<typeof setInterval> | undefined;\n let running = false;\n\n async function runBatch(\n entries: JobLedgerEntry[],\n handler: (entry: JobLedgerEntry) => Promise<void>,\n ): Promise<void> {\n let index = 0;\n const workerCount = Math.min(concurrency, entries.length);\n const workers = Array.from({ length: workerCount }, async () => {\n while (index < entries.length) {\n const currentIndex = index++;\n const entry = entries[currentIndex];\n if (!entry) {\n continue;\n }\n try {\n await handler(entry);\n } catch (error) {\n logger.warn?.(\n {\n err: error instanceof Error ? error.message : String(error),\n jobEventId: entry.jobEventId,\n side: entry.side,\n state: entry.state,\n },\n 'recovery handler threw',\n );\n }\n }\n });\n await Promise.all(workers);\n }\n\n async function sweepSide(\n side: JobSide,\n handler: ((entry: JobLedgerEntry) => Promise<void>) | undefined,\n ): Promise<void> {\n if (!handler) {\n return;\n }\n const pending = await pendingJobs(adapter, side);\n if (pending.length === 0) {\n return;\n }\n logger.info?.({ [side]: pending.length }, 'recovery sweep: resuming pending jobs');\n await runBatch(pending, handler);\n }\n\n async function sweepOnce(): Promise<void> {\n if (running) {\n return;\n }\n running = true;\n try {\n try {\n await adapter.pruneOldEntries(retentionMs);\n } catch (error) {\n logger.warn?.(\n { err: error instanceof Error ? error.message : String(error) },\n 'recovery: pruneOldEntries failed',\n );\n }\n await sweepSide('provider', onProviderPending);\n await sweepSide('customer', onCustomerPending);\n } finally {\n running = false;\n }\n }\n\n function start(): void {\n if (timer) {\n return;\n }\n // Initial sweep kicked off in background so start() returns quickly.\n sweepOnce().catch((error) =>\n logger.warn?.(\n { err: error instanceof Error ? error.message : String(error) },\n 'initial recovery sweep failed',\n ),\n );\n timer = setInterval(() => {\n sweepOnce().catch((error) =>\n logger.warn?.(\n { err: error instanceof Error ? error.message : String(error) },\n 'recovery sweep failed',\n ),\n );\n }, intervalMs);\n }\n\n function stop(): void {\n if (timer) {\n clearInterval(timer);\n timer = undefined;\n }\n }\n\n return { start, stop, sweepOnce };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/runtime/jobLedger.ts","../src/runtime/memoryAdapter.ts","../src/runtime/recoveryLoop.ts"],"names":[],"mappings":"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","file":"runtime.js","sourcesContent":["/**\n * Shared JobLedger types + adapter interface for elisym crash recovery.\n *\n * The ledger is an append-only log of state transitions for each job,\n * keyed by the Nostr job-request event id. Each adapter chooses its own\n * storage backend (Eliza memory, SQLite, tests-only in-memory) and\n * exposes the minimal read/write surface this module defines.\n */\n\nexport type JobSide = 'provider' | 'customer';\n\nexport type ProviderState =\n | 'waiting_payment'\n | 'paid'\n | 'executed'\n | 'delivered'\n | 'failed'\n | 'cancelled';\n\nexport type CustomerState =\n | 'submitted'\n | 'waiting_payment'\n | 'payment_sent'\n | 'result_received'\n | 'failed'\n | 'cancelled';\n\nexport type JobState = ProviderState | CustomerState;\n\nexport const JOB_LEDGER_VERSION = 1;\n\nexport const TERMINAL_STATES: ReadonlySet<JobState> = new Set<JobState>([\n 'delivered',\n 'result_received',\n 'failed',\n 'cancelled',\n]);\n\nexport interface JobLedgerEntry {\n jobEventId: string;\n side: JobSide;\n state: JobState;\n capability: string;\n /**\n * Price in subunits of the payment asset, stored as a decimal string\n * (bigint-safe on disk).\n *\n * Kept named `priceLamports` for persistence back-compat: old ledger files\n * use this name and interpret the value as lamports (SOL subunits). When\n * `assetKey` is set (new USDC flows), interpret the same string as subunits\n * of that asset (e.g. 1 USDC = 1_000_000).\n */\n priceLamports: string;\n /**\n * Optional asset key (`solana:sol` / `solana:usdc:<mint>`). Absent =>\n * native SOL (back-compat with pre-USDC ledger entries).\n */\n assetKey?: string;\n rawEventJson?: string;\n customerPubkey?: string;\n providerPubkey?: string;\n input?: string;\n paymentRequestJson?: string;\n txSignature?: string;\n resultContent?: string;\n error?: string;\n retryCount?: number;\n transitionAt: number;\n jobCreatedAt: number;\n version: number;\n}\n\n/**\n * Input to `adapter.write`. The adapter is responsible for stamping\n * `transitionAt` (wall-clock) and `version` so callers can spread an\n * earlier entry safely without carrying those fields forward.\n */\nexport type JobLedgerWriteInput = Omit<JobLedgerEntry, 'transitionAt' | 'version'>;\n\nexport interface JobLedgerAdapter {\n /** Append a new state transition for a job. */\n write(entry: JobLedgerWriteInput): Promise<void>;\n /**\n * Return the latest entry per job id. If `side` is given, restrict to\n * one side; otherwise include both.\n */\n loadLatest(side?: JobSide): Promise<Map<string, JobLedgerEntry>>;\n /**\n * Delete terminal entries whose last transition happened before\n * `now - retentionMs`. Non-terminal entries are retained so recovery\n * can keep retrying. Returns the number of entries dropped.\n */\n pruneOldEntries(retentionMs: number): Promise<number>;\n}\n\n/**\n * Convenience: return non-terminal entries for `side`, oldest-first by\n * `jobCreatedAt`. Derived from `loadLatest` so any adapter satisfies it.\n */\nexport async function pendingJobs(\n adapter: JobLedgerAdapter,\n side: JobSide,\n): Promise<JobLedgerEntry[]> {\n const latest = await adapter.loadLatest(side);\n const pending: JobLedgerEntry[] = [];\n for (const entry of latest.values()) {\n if (!TERMINAL_STATES.has(entry.state)) {\n pending.push(entry);\n }\n }\n pending.sort((left, right) => left.jobCreatedAt - right.jobCreatedAt);\n return pending;\n}\n\nexport async function findByJobId(\n adapter: JobLedgerAdapter,\n jobEventId: string,\n): Promise<JobLedgerEntry | undefined> {\n const latest = await adapter.loadLatest();\n return latest.get(jobEventId);\n}\n","import {\n JOB_LEDGER_VERSION,\n TERMINAL_STATES,\n type JobLedgerAdapter,\n type JobLedgerEntry,\n type JobLedgerWriteInput,\n type JobSide,\n} from './jobLedger';\n\ninterface Row {\n entry: JobLedgerEntry;\n /** Row timestamp, separate from entry.transitionAt so we can resolve ties. */\n rowAt: number;\n}\n\n/**\n * In-memory reference adapter. Useful for tests and ephemeral deployments\n * where no durable backing store is required. Not suitable for real\n * crash-recovery (everything is lost on process exit).\n */\nexport function createMemoryJobLedgerAdapter(): JobLedgerAdapter & {\n /** Test-only: drop all rows. */\n clear(): void;\n /** Test-only: inspect the raw append log. */\n rows(): ReadonlyArray<JobLedgerEntry>;\n} {\n const log: Row[] = [];\n let rowCounter = 0;\n\n function latestByJobId(side?: JobSide): Map<string, JobLedgerEntry> {\n const latest = new Map<string, JobLedgerEntry>();\n const latestRowAt = new Map<string, number>();\n for (const row of log) {\n if (side && row.entry.side !== side) {\n continue;\n }\n const jobId = row.entry.jobEventId;\n const previous = latestRowAt.get(jobId) ?? -Infinity;\n if (row.rowAt >= previous) {\n latest.set(jobId, row.entry);\n latestRowAt.set(jobId, row.rowAt);\n }\n }\n return latest;\n }\n\n return {\n async write(entry: JobLedgerWriteInput): Promise<void> {\n const finalized: JobLedgerEntry = {\n ...entry,\n transitionAt: Date.now(),\n version: JOB_LEDGER_VERSION,\n };\n rowCounter += 1;\n log.push({ entry: finalized, rowAt: rowCounter });\n },\n async loadLatest(side?: JobSide): Promise<Map<string, JobLedgerEntry>> {\n return latestByJobId(side);\n },\n async pruneOldEntries(retentionMs: number): Promise<number> {\n const cutoff = Date.now() - retentionMs;\n const latest = latestByJobId();\n const dropIds = new Set<string>();\n for (const [jobId, entry] of latest) {\n if (TERMINAL_STATES.has(entry.state) && entry.transitionAt < cutoff) {\n dropIds.add(jobId);\n }\n }\n if (dropIds.size === 0) {\n return 0;\n }\n let deleted = 0;\n for (let index = log.length - 1; index >= 0; index--) {\n const row = log[index];\n if (row && dropIds.has(row.entry.jobEventId)) {\n log.splice(index, 1);\n deleted += 1;\n }\n }\n return deleted;\n },\n clear(): void {\n log.length = 0;\n rowCounter = 0;\n },\n rows(): ReadonlyArray<JobLedgerEntry> {\n return log.map((row) => row.entry);\n },\n };\n}\n","import { pendingJobs, type JobLedgerAdapter, type JobLedgerEntry, type JobSide } from './jobLedger';\n\nexport interface RecoveryLoopLogger {\n info?(obj: Record<string, unknown>, msg?: string): void;\n warn?(obj: Record<string, unknown>, msg?: string): void;\n debug?(obj: Record<string, unknown>, msg?: string): void;\n}\n\nexport interface RecoveryLoopOptions {\n adapter: JobLedgerAdapter;\n /** Called for each non-terminal provider-side entry. Should advance state. */\n onProviderPending?: (entry: JobLedgerEntry) => Promise<void>;\n /** Called for each non-terminal customer-side entry. Should advance state. */\n onCustomerPending?: (entry: JobLedgerEntry) => Promise<void>;\n /** Sweep cadence. */\n intervalMs: number;\n /** Retention for terminal entries. Passed to adapter.pruneOldEntries each sweep. */\n retentionMs: number;\n /** Concurrent per-job worker count during a sweep. */\n concurrency: number;\n /** Optional structured logger. Falls back to silence. */\n logger?: RecoveryLoopLogger;\n}\n\nexport interface RecoveryLoop {\n /** Kick off an initial sweep (non-blocking) and start the periodic timer. */\n start(): void;\n /** Stop the periodic timer. Does not cancel an in-flight sweep. */\n stop(): void;\n /** Run a single sweep synchronously. Useful for tests or manual triggers. */\n sweepOnce(): Promise<void>;\n}\n\n/**\n * Generic recovery scaffold: periodic pruning + concurrent replay of\n * non-terminal ledger entries. Per-side handlers own the business\n * semantics (retry budget, re-execute, payment verify, delivery retry).\n *\n * Notes:\n * - Overlap guard: if the previous sweep is still in flight, subsequent\n * tick fires are skipped - no queueing. The next on-schedule fire will\n * pick up where the previous left off (ledger is idempotent).\n * - Handler errors are caught and logged at `warn` so a single bad entry\n * doesn't poison the batch.\n */\nexport function createRecoveryLoop(options: RecoveryLoopOptions): RecoveryLoop {\n const { adapter, onProviderPending, onCustomerPending, intervalMs, retentionMs, concurrency } =\n options;\n const logger = options.logger ?? {};\n if (intervalMs <= 0) {\n throw new RangeError('intervalMs must be > 0');\n }\n if (concurrency <= 0) {\n throw new RangeError('concurrency must be > 0');\n }\n if (retentionMs <= 0) {\n throw new RangeError('retentionMs must be > 0');\n }\n\n let timer: ReturnType<typeof setInterval> | undefined;\n let running = false;\n\n async function runBatch(\n entries: JobLedgerEntry[],\n handler: (entry: JobLedgerEntry) => Promise<void>,\n ): Promise<void> {\n let index = 0;\n const workerCount = Math.min(concurrency, entries.length);\n const workers = Array.from({ length: workerCount }, async () => {\n while (index < entries.length) {\n const currentIndex = index++;\n const entry = entries[currentIndex];\n if (!entry) {\n continue;\n }\n try {\n await handler(entry);\n } catch (error) {\n logger.warn?.(\n {\n err: error instanceof Error ? error.message : String(error),\n jobEventId: entry.jobEventId,\n side: entry.side,\n state: entry.state,\n },\n 'recovery handler threw',\n );\n }\n }\n });\n await Promise.all(workers);\n }\n\n async function sweepSide(\n side: JobSide,\n handler: ((entry: JobLedgerEntry) => Promise<void>) | undefined,\n ): Promise<void> {\n if (!handler) {\n return;\n }\n const pending = await pendingJobs(adapter, side);\n if (pending.length === 0) {\n return;\n }\n logger.info?.({ [side]: pending.length }, 'recovery sweep: resuming pending jobs');\n await runBatch(pending, handler);\n }\n\n async function sweepOnce(): Promise<void> {\n if (running) {\n return;\n }\n running = true;\n try {\n try {\n await adapter.pruneOldEntries(retentionMs);\n } catch (error) {\n logger.warn?.(\n { err: error instanceof Error ? error.message : String(error) },\n 'recovery: pruneOldEntries failed',\n );\n }\n await sweepSide('provider', onProviderPending);\n await sweepSide('customer', onCustomerPending);\n } finally {\n running = false;\n }\n }\n\n function start(): void {\n if (timer) {\n return;\n }\n // Initial sweep kicked off in background so start() returns quickly.\n sweepOnce().catch((error) =>\n logger.warn?.(\n { err: error instanceof Error ? error.message : String(error) },\n 'initial recovery sweep failed',\n ),\n );\n timer = setInterval(() => {\n sweepOnce().catch((error) =>\n logger.warn?.(\n { err: error instanceof Error ? error.message : String(error) },\n 'recovery sweep failed',\n ),\n );\n }, intervalMs);\n }\n\n function stop(): void {\n if (timer) {\n clearInterval(timer);\n timer = undefined;\n }\n }\n\n return { start, stop, sweepOnce };\n}\n"]}
|