@delexec/ops 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/README.zh-CN.md +6 -0
- package/node_modules/@delexec/caller-controller/README.md +3 -0
- package/node_modules/@delexec/caller-controller/README.zh-CN.md +6 -0
- package/node_modules/@delexec/caller-controller/package.json +53 -0
- package/node_modules/@delexec/caller-controller/src/server.js +127 -0
- package/node_modules/@delexec/caller-controller-core/README.md +3 -0
- package/node_modules/@delexec/caller-controller-core/README.zh-CN.md +6 -0
- package/node_modules/@delexec/caller-controller-core/package.json +26 -0
- package/node_modules/@delexec/caller-controller-core/src/index.js +1612 -0
- package/node_modules/@delexec/caller-skill-adapter/package.json +12 -0
- package/node_modules/@delexec/caller-skill-adapter/src/server.js +1042 -0
- package/node_modules/@delexec/caller-skill-mcp-adapter/README.md +65 -0
- package/node_modules/@delexec/caller-skill-mcp-adapter/package.json +16 -0
- package/node_modules/@delexec/caller-skill-mcp-adapter/src/server.js +527 -0
- package/node_modules/@delexec/responder-controller/README.md +3 -0
- package/node_modules/@delexec/responder-controller/README.zh-CN.md +6 -0
- package/node_modules/@delexec/responder-controller/package.json +53 -0
- package/node_modules/@delexec/responder-controller/src/server.js +254 -0
- package/node_modules/@delexec/responder-runtime-core/README.md +3 -0
- package/node_modules/@delexec/responder-runtime-core/README.zh-CN.md +6 -0
- package/node_modules/@delexec/responder-runtime-core/package.json +26 -0
- package/node_modules/@delexec/responder-runtime-core/src/executors.js +326 -0
- package/node_modules/@delexec/responder-runtime-core/src/index.js +1202 -0
- package/node_modules/@delexec/runtime-utils/README.md +3 -0
- package/node_modules/@delexec/runtime-utils/README.zh-CN.md +6 -0
- package/node_modules/@delexec/runtime-utils/package.json +23 -0
- package/node_modules/@delexec/runtime-utils/src/index.js +338 -0
- package/node_modules/@delexec/sqlite-store/README.md +3 -0
- package/node_modules/@delexec/sqlite-store/README.zh-CN.md +6 -0
- package/node_modules/@delexec/sqlite-store/package.json +26 -0
- package/node_modules/@delexec/sqlite-store/src/index.js +68 -0
- package/node_modules/@delexec/transport-email/README.md +3 -0
- package/node_modules/@delexec/transport-email/README.zh-CN.md +6 -0
- package/node_modules/@delexec/transport-email/package.json +23 -0
- package/node_modules/@delexec/transport-email/src/index.js +185 -0
- package/node_modules/@delexec/transport-emailengine/README.md +3 -0
- package/node_modules/@delexec/transport-emailengine/README.zh-CN.md +6 -0
- package/node_modules/@delexec/transport-emailengine/package.json +26 -0
- package/node_modules/@delexec/transport-emailengine/src/index.js +210 -0
- package/node_modules/@delexec/transport-gmail/README.md +3 -0
- package/node_modules/@delexec/transport-gmail/README.zh-CN.md +6 -0
- package/node_modules/@delexec/transport-gmail/package.json +26 -0
- package/node_modules/@delexec/transport-gmail/src/index.js +295 -0
- package/node_modules/@delexec/transport-relay-http/README.md +3 -0
- package/node_modules/@delexec/transport-relay-http/README.zh-CN.md +6 -0
- package/node_modules/@delexec/transport-relay-http/package.json +23 -0
- package/node_modules/@delexec/transport-relay-http/src/index.js +124 -0
- package/package.json +64 -0
- package/src/cli.js +1571 -0
- package/src/config.js +1180 -0
- package/src/example-hotline-worker.js +65 -0
- package/src/example-hotline.js +196 -0
- package/src/logging.js +56 -0
- package/src/supervisor.js +3070 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@delexec/runtime-utils",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared runtime utilities for delegated execution services",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./package.json": "./package.json"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"delegated-execution",
|
|
20
|
+
"runtime",
|
|
21
|
+
"utilities"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
|
|
6
|
+
const SECRET_STORE_VERSION = 1;
|
|
7
|
+
const KEY_LENGTH = 32;
|
|
8
|
+
const LEGACY_OPS_HOME_ENV = "CROC_OPS_HOME";
|
|
9
|
+
const OPS_HOME_ENV = "DELEXEC_HOME";
|
|
10
|
+
const LEGACY_OPS_HOME_BASENAME = ".remote-hotline";
|
|
11
|
+
const OPS_HOME_BASENAME = ".delexec";
|
|
12
|
+
const LEGACY_SQLITE_FILENAME = "croc.sqlite";
|
|
13
|
+
const OPS_SQLITE_FILENAME = "delexec.sqlite";
|
|
14
|
+
|
|
15
|
+
function stripWrappingQuotes(value) {
|
|
16
|
+
if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
17
|
+
return value.slice(1, -1);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function toBase64Url(buffer) {
|
|
23
|
+
return Buffer.from(buffer).toString("base64url");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fromBase64Url(value) {
|
|
27
|
+
return Buffer.from(String(value || ""), "base64url");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function requirePassphrase(passphrase) {
|
|
31
|
+
const value = String(passphrase || "");
|
|
32
|
+
if (!value.trim()) {
|
|
33
|
+
throw new Error("secret_store_passphrase_required");
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function deriveKey(passphrase, salt) {
|
|
39
|
+
return crypto.scryptSync(requirePassphrase(passphrase), salt, KEY_LENGTH);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeSecureTextFile(filePath, content) {
|
|
43
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
44
|
+
fs.writeFileSync(filePath, content, { encoding: "utf8", mode: 0o600 });
|
|
45
|
+
fs.chmodSync(filePath, 0o600);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveDefaultOpsHomeDir() {
|
|
49
|
+
return path.join(os.homedir(), OPS_HOME_BASENAME);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveLegacyOpsHomeDir() {
|
|
53
|
+
return path.join(os.homedir(), LEGACY_OPS_HOME_BASENAME);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renameOrCopyDir(sourceDir, targetDir) {
|
|
57
|
+
try {
|
|
58
|
+
fs.renameSync(sourceDir, targetDir);
|
|
59
|
+
return;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (!error || (error.code !== "EXDEV" && error.code !== "EACCES" && error.code !== "EPERM")) {
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
fs.cpSync(sourceDir, targetDir, { recursive: true, force: false });
|
|
66
|
+
fs.rmSync(sourceDir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function migrateLegacySqliteFile(homeDir) {
|
|
70
|
+
const legacyFile = path.join(homeDir, LEGACY_SQLITE_FILENAME);
|
|
71
|
+
const nextFile = path.join(homeDir, OPS_SQLITE_FILENAME);
|
|
72
|
+
if (!fs.existsSync(legacyFile) || fs.existsSync(nextFile)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
fs.renameSync(legacyFile, nextFile);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function migrateLegacyOpsHomeDir({
|
|
79
|
+
explicitHomeDir = process.env[OPS_HOME_ENV] || process.env[LEGACY_OPS_HOME_ENV] || null
|
|
80
|
+
} = {}) {
|
|
81
|
+
if (explicitHomeDir) {
|
|
82
|
+
const resolved = path.resolve(explicitHomeDir);
|
|
83
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
84
|
+
migrateLegacySqliteFile(resolved);
|
|
85
|
+
}
|
|
86
|
+
return resolved;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const nextHomeDir = path.resolve(resolveDefaultOpsHomeDir());
|
|
90
|
+
const legacyHomeDir = path.resolve(resolveLegacyOpsHomeDir());
|
|
91
|
+
if (!fs.existsSync(legacyHomeDir) || fs.existsSync(nextHomeDir)) {
|
|
92
|
+
if (fs.existsSync(nextHomeDir) && fs.statSync(nextHomeDir).isDirectory()) {
|
|
93
|
+
migrateLegacySqliteFile(nextHomeDir);
|
|
94
|
+
}
|
|
95
|
+
return nextHomeDir;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fs.mkdirSync(path.dirname(nextHomeDir), { recursive: true, mode: 0o700 });
|
|
99
|
+
renameOrCopyDir(legacyHomeDir, nextHomeDir);
|
|
100
|
+
migrateLegacySqliteFile(nextHomeDir);
|
|
101
|
+
return nextHomeDir;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildSecretPayload(secrets) {
|
|
105
|
+
return Buffer.from(
|
|
106
|
+
JSON.stringify({
|
|
107
|
+
version: SECRET_STORE_VERSION,
|
|
108
|
+
updated_at: new Date().toISOString(),
|
|
109
|
+
secrets: secrets || {}
|
|
110
|
+
}),
|
|
111
|
+
"utf8"
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function encryptSecrets(passphrase, secrets, salt = crypto.randomBytes(16)) {
|
|
116
|
+
const iv = crypto.randomBytes(12);
|
|
117
|
+
const key = deriveKey(passphrase, salt);
|
|
118
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
119
|
+
const ciphertext = Buffer.concat([cipher.update(buildSecretPayload(secrets)), cipher.final()]);
|
|
120
|
+
const tag = cipher.getAuthTag();
|
|
121
|
+
return {
|
|
122
|
+
version: SECRET_STORE_VERSION,
|
|
123
|
+
kdf: "scrypt",
|
|
124
|
+
cipher: "aes-256-gcm",
|
|
125
|
+
salt: toBase64Url(salt),
|
|
126
|
+
iv: toBase64Url(iv),
|
|
127
|
+
tag: toBase64Url(tag),
|
|
128
|
+
ciphertext: toBase64Url(ciphertext)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function decryptEnvelope(passphrase, envelope) {
|
|
133
|
+
if (!envelope || envelope.version !== SECRET_STORE_VERSION) {
|
|
134
|
+
throw new Error("secret_store_version_unsupported");
|
|
135
|
+
}
|
|
136
|
+
const key = deriveKey(passphrase, fromBase64Url(envelope.salt));
|
|
137
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, fromBase64Url(envelope.iv));
|
|
138
|
+
decipher.setAuthTag(fromBase64Url(envelope.tag));
|
|
139
|
+
const plaintext = Buffer.concat([decipher.update(fromBase64Url(envelope.ciphertext)), decipher.final()]).toString("utf8");
|
|
140
|
+
const parsed = JSON.parse(plaintext);
|
|
141
|
+
if (parsed.version !== SECRET_STORE_VERSION || typeof parsed.secrets !== "object" || parsed.secrets === null) {
|
|
142
|
+
throw new Error("secret_store_payload_invalid");
|
|
143
|
+
}
|
|
144
|
+
return parsed;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function parseEnvText(text) {
|
|
148
|
+
const result = {};
|
|
149
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
150
|
+
const line = rawLine.trim();
|
|
151
|
+
if (!line || line.startsWith("#")) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const separatorIndex = line.indexOf("=");
|
|
155
|
+
if (separatorIndex <= 0) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
159
|
+
const value = stripWrappingQuotes(line.slice(separatorIndex + 1).trim());
|
|
160
|
+
result[key] = value;
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function readEnvFile(filePath) {
|
|
166
|
+
if (!fs.existsSync(filePath)) {
|
|
167
|
+
return {};
|
|
168
|
+
}
|
|
169
|
+
return parseEnvText(fs.readFileSync(filePath, "utf8"));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function loadEnvFiles(filePaths, { override = false } = {}) {
|
|
173
|
+
for (const filePath of filePaths) {
|
|
174
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const entries = readEnvFile(filePath);
|
|
178
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
179
|
+
if (override || process.env[key] === undefined) {
|
|
180
|
+
process.env[key] = value;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function formatEnvFile(entries) {
|
|
187
|
+
return `${Object.entries(entries)
|
|
188
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
189
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
190
|
+
.join("\n")}\n`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function updateEnvFile(filePath, updates, options = {}) {
|
|
194
|
+
const removeNull = options.removeNull === true;
|
|
195
|
+
const current = readEnvFile(filePath);
|
|
196
|
+
const next = { ...current };
|
|
197
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
198
|
+
if (value === undefined) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (value === null) {
|
|
202
|
+
if (removeNull) {
|
|
203
|
+
delete next[key];
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
next[key] = value;
|
|
208
|
+
}
|
|
209
|
+
writeSecureTextFile(filePath, formatEnvFile(next));
|
|
210
|
+
return next;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function getOpsHomeDir() {
|
|
214
|
+
if (process.env[OPS_HOME_ENV]) {
|
|
215
|
+
return path.resolve(process.env[OPS_HOME_ENV]);
|
|
216
|
+
}
|
|
217
|
+
if (process.env[LEGACY_OPS_HOME_ENV]) {
|
|
218
|
+
return path.resolve(process.env[LEGACY_OPS_HOME_ENV]);
|
|
219
|
+
}
|
|
220
|
+
return migrateLegacyOpsHomeDir();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function getOpsEnvFile() {
|
|
224
|
+
return path.join(getOpsHomeDir(), ".env.local");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function getOpsSecretsFile() {
|
|
228
|
+
return path.join(getOpsHomeDir(), "secrets.enc.json");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function getResponderConfigFile() {
|
|
232
|
+
return path.join(getOpsHomeDir(), "responder.config.json");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function getOpsConfigFile() {
|
|
236
|
+
return path.join(getOpsHomeDir(), "ops.config.json");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function ensureOpsDirectories() {
|
|
240
|
+
const homeDir = getOpsHomeDir();
|
|
241
|
+
fs.mkdirSync(homeDir, { recursive: true, mode: 0o700 });
|
|
242
|
+
fs.mkdirSync(path.join(homeDir, "logs"), { recursive: true, mode: 0o700 });
|
|
243
|
+
fs.mkdirSync(path.join(homeDir, "run"), { recursive: true, mode: 0o700 });
|
|
244
|
+
migrateLegacySqliteFile(homeDir);
|
|
245
|
+
return homeDir;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function buildOpsEnvSearchPaths(rootDir, profileName = null) {
|
|
249
|
+
const homeDir = getOpsHomeDir();
|
|
250
|
+
const paths = [path.join(homeDir, ".env"), path.join(homeDir, ".env.local"), path.join(rootDir, ".env"), path.join(rootDir, ".env.local")];
|
|
251
|
+
|
|
252
|
+
if (profileName) {
|
|
253
|
+
paths.push(path.join(rootDir, `deploy/${profileName}/.env`));
|
|
254
|
+
paths.push(path.join(rootDir, `deploy/${profileName}/.env.local`));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
paths.push(path.join(rootDir, "deploy/ops/.env"));
|
|
258
|
+
paths.push(path.join(rootDir, "deploy/ops/.env.local"));
|
|
259
|
+
return paths;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function readJsonFile(filePath, fallback = null) {
|
|
263
|
+
if (!fs.existsSync(filePath)) {
|
|
264
|
+
return fallback;
|
|
265
|
+
}
|
|
266
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function writeJsonFile(filePath, value) {
|
|
270
|
+
writeSecureTextFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function secretStoreExists(filePath) {
|
|
274
|
+
return Boolean(filePath) && fs.existsSync(filePath);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function initializeSecretStore(filePath, passphrase, secrets = {}) {
|
|
278
|
+
if (secretStoreExists(filePath)) {
|
|
279
|
+
throw new Error("secret_store_already_initialized");
|
|
280
|
+
}
|
|
281
|
+
const envelope = encryptSecrets(passphrase, secrets);
|
|
282
|
+
writeSecureTextFile(filePath, `${JSON.stringify(envelope, null, 2)}\n`);
|
|
283
|
+
return {
|
|
284
|
+
initialized: true,
|
|
285
|
+
secret_count: Object.keys(secrets || {}).length
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function unlockSecretStore(filePath, passphrase) {
|
|
290
|
+
if (!secretStoreExists(filePath)) {
|
|
291
|
+
throw new Error("secret_store_not_initialized");
|
|
292
|
+
}
|
|
293
|
+
const envelope = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
294
|
+
const payload = decryptEnvelope(passphrase, envelope);
|
|
295
|
+
return {
|
|
296
|
+
secrets: payload.secrets,
|
|
297
|
+
updated_at: payload.updated_at || null
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function replaceSecretStore(filePath, passphrase, secrets = {}) {
|
|
302
|
+
if (!secretStoreExists(filePath)) {
|
|
303
|
+
throw new Error("secret_store_not_initialized");
|
|
304
|
+
}
|
|
305
|
+
const envelope = encryptSecrets(passphrase, secrets);
|
|
306
|
+
writeSecureTextFile(filePath, `${JSON.stringify(envelope, null, 2)}\n`);
|
|
307
|
+
return {
|
|
308
|
+
updated: true,
|
|
309
|
+
secret_count: Object.keys(secrets || {}).length
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function writeSecretValues(filePath, passphrase, updates = {}) {
|
|
314
|
+
const current = unlockSecretStore(filePath, passphrase).secrets;
|
|
315
|
+
const next = { ...current };
|
|
316
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
317
|
+
if (value === undefined) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (value === null) {
|
|
321
|
+
delete next[key];
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
next[key] = value;
|
|
325
|
+
}
|
|
326
|
+
replaceSecretStore(filePath, passphrase, next);
|
|
327
|
+
return next;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function rotateSecretStorePassphrase(filePath, currentPassphrase, nextPassphrase) {
|
|
331
|
+
const current = unlockSecretStore(filePath, currentPassphrase).secrets;
|
|
332
|
+
const envelope = encryptSecrets(nextPassphrase, current);
|
|
333
|
+
writeSecureTextFile(filePath, `${JSON.stringify(envelope, null, 2)}\n`);
|
|
334
|
+
return {
|
|
335
|
+
rotated: true,
|
|
336
|
+
secret_count: Object.keys(current).length
|
|
337
|
+
};
|
|
338
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@delexec/sqlite-store",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SQLite snapshot store for delegated execution services",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./package.json": "./package.json"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"delegated-execution",
|
|
20
|
+
"sqlite",
|
|
21
|
+
"storage"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"better-sqlite3": "^12.6.2"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import Database from "better-sqlite3";
|
|
5
|
+
|
|
6
|
+
export async function createSqliteSnapshotStore({
|
|
7
|
+
databasePath,
|
|
8
|
+
serviceName,
|
|
9
|
+
tableName = "service_state_snapshots"
|
|
10
|
+
} = {}) {
|
|
11
|
+
if (!databasePath) {
|
|
12
|
+
throw new Error("sqlite_store_database_path_required");
|
|
13
|
+
}
|
|
14
|
+
if (!serviceName) {
|
|
15
|
+
throw new Error("sqlite_store_service_name_required");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const resolvedPath = path.resolve(databasePath);
|
|
19
|
+
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
|
20
|
+
const db = new Database(resolvedPath);
|
|
21
|
+
|
|
22
|
+
function migrate() {
|
|
23
|
+
db.exec(`
|
|
24
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
25
|
+
version TEXT,
|
|
26
|
+
applied_at TEXT
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
30
|
+
service_name TEXT,
|
|
31
|
+
state_json TEXT,
|
|
32
|
+
updated_at TEXT
|
|
33
|
+
);
|
|
34
|
+
`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function loadSnapshot() {
|
|
38
|
+
const row = db
|
|
39
|
+
.prepare(`SELECT state_json FROM ${tableName} WHERE service_name = ? ORDER BY rowid DESC LIMIT 1`)
|
|
40
|
+
.get(serviceName);
|
|
41
|
+
return row?.state_json ? JSON.parse(row.state_json) : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function saveSnapshot(snapshot) {
|
|
45
|
+
const transaction = db.transaction((payload) => {
|
|
46
|
+
db.prepare(`DELETE FROM ${tableName} WHERE service_name = ?`).run(serviceName);
|
|
47
|
+
db.prepare(`INSERT INTO ${tableName} (service_name, state_json, updated_at) VALUES (?, ?, ?)`).run(
|
|
48
|
+
serviceName,
|
|
49
|
+
JSON.stringify(payload),
|
|
50
|
+
new Date().toISOString()
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
transaction(snapshot);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function close() {
|
|
57
|
+
db.close();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
migrate,
|
|
62
|
+
loadSnapshot,
|
|
63
|
+
saveSnapshot,
|
|
64
|
+
close,
|
|
65
|
+
db,
|
|
66
|
+
databasePath: resolvedPath
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@delexec/transport-email",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared email transport primitives for delegated execution",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./package.json": "./package.json"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"delegated-execution",
|
|
20
|
+
"transport",
|
|
21
|
+
"email"
|
|
22
|
+
]
|
|
23
|
+
}
|