@openthink/stamp 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +5499 -3926
- package/dist/index.js.map +1 -1
- package/dist/server/prompts-cache-bootstrap.cjs +276 -0
- package/dist/server/prompts-cache-bootstrap.cjs.map +1 -0
- package/dist/server/stamp-review.cjs +43 -14
- package/dist/server/stamp-review.cjs.map +1 -1
- package/dist/server/start-http-server.cjs +471 -2
- package/dist/server/start-http-server.cjs.map +1 -1
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
// src/server/http-server.ts
|
|
5
|
+
var import_node_crypto4 = require("crypto");
|
|
5
6
|
var import_node_http = require("http");
|
|
6
7
|
|
|
7
8
|
// src/lib/invites.ts
|
|
@@ -184,9 +185,225 @@ function sshFingerprintFromBlob(keyBlob) {
|
|
|
184
185
|
return `SHA256:${b64}`;
|
|
185
186
|
}
|
|
186
187
|
|
|
188
|
+
// src/server/prompts-cache.ts
|
|
189
|
+
var import_node_fs4 = require("fs");
|
|
190
|
+
var import_node_child_process = require("child_process");
|
|
191
|
+
var import_node_url = require("url");
|
|
192
|
+
var import_node_path3 = require("path");
|
|
193
|
+
|
|
194
|
+
// src/server/promptFetch.ts
|
|
195
|
+
var import_node_fs3 = require("fs");
|
|
196
|
+
var import_node_crypto3 = require("crypto");
|
|
197
|
+
var MAX_PROMPT_BYTES = 1024 * 1024;
|
|
198
|
+
|
|
199
|
+
// src/server/prompts-cache.ts
|
|
200
|
+
var import_meta = {};
|
|
201
|
+
var LOCK_STALE_MS = 5 * 60 * 1e3;
|
|
202
|
+
function defaultKnownHostsPath() {
|
|
203
|
+
return (0, import_node_path3.resolve)(
|
|
204
|
+
(0, import_node_path3.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)),
|
|
205
|
+
"..",
|
|
206
|
+
"..",
|
|
207
|
+
"server",
|
|
208
|
+
"github-known-hosts"
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
var inflightRefreshes = /* @__PURE__ */ new Map();
|
|
212
|
+
async function cloneOrFetchPromptsCache(opts) {
|
|
213
|
+
validateOpts(opts);
|
|
214
|
+
const cacheRoot = (0, import_node_path3.resolve)(opts.cacheRoot);
|
|
215
|
+
const existing = inflightRefreshes.get(cacheRoot);
|
|
216
|
+
if (existing) return existing;
|
|
217
|
+
const promise = (async () => {
|
|
218
|
+
const lockPath = `${cacheRoot}.refresh.lock`;
|
|
219
|
+
acquireLock(lockPath);
|
|
220
|
+
try {
|
|
221
|
+
return await refreshInternal({ ...opts, cacheRoot });
|
|
222
|
+
} finally {
|
|
223
|
+
releaseLock(lockPath);
|
|
224
|
+
}
|
|
225
|
+
})();
|
|
226
|
+
inflightRefreshes.set(cacheRoot, promise);
|
|
227
|
+
promise.finally(() => {
|
|
228
|
+
if (inflightRefreshes.get(cacheRoot) === promise) {
|
|
229
|
+
inflightRefreshes.delete(cacheRoot);
|
|
230
|
+
}
|
|
231
|
+
}).catch(() => {
|
|
232
|
+
});
|
|
233
|
+
return promise;
|
|
234
|
+
}
|
|
235
|
+
function validateOpts(opts) {
|
|
236
|
+
if (!opts || typeof opts !== "object") {
|
|
237
|
+
throw new Error("cloneOrFetchPromptsCache: opts must be an object");
|
|
238
|
+
}
|
|
239
|
+
if (!opts.url || typeof opts.url !== "string") {
|
|
240
|
+
throw new Error("cloneOrFetchPromptsCache: url is required");
|
|
241
|
+
}
|
|
242
|
+
if (!opts.ref || typeof opts.ref !== "string") {
|
|
243
|
+
throw new Error("cloneOrFetchPromptsCache: ref is required");
|
|
244
|
+
}
|
|
245
|
+
if (!opts.cacheRoot || typeof opts.cacheRoot !== "string") {
|
|
246
|
+
throw new Error("cloneOrFetchPromptsCache: cacheRoot is required");
|
|
247
|
+
}
|
|
248
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,200}$/.test(opts.ref)) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`cloneOrFetchPromptsCache: ref ${JSON.stringify(opts.ref)} contains characters not allowed in a git refspec`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function acquireLock(lockPath) {
|
|
255
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(lockPath), { recursive: true });
|
|
256
|
+
try {
|
|
257
|
+
const fd = (0, import_node_fs4.openSync)(lockPath, "wx");
|
|
258
|
+
(0, import_node_fs4.closeSync)(fd);
|
|
259
|
+
return;
|
|
260
|
+
} catch (err) {
|
|
261
|
+
const e = err;
|
|
262
|
+
if (e.code !== "EEXIST") {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`prompts-cache: could not create lock file ${lockPath}: ${err.message}`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
let lockStat;
|
|
269
|
+
try {
|
|
270
|
+
lockStat = (0, import_node_fs4.statSync)(lockPath);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
try {
|
|
273
|
+
const fd = (0, import_node_fs4.openSync)(lockPath, "wx");
|
|
274
|
+
(0, import_node_fs4.closeSync)(fd);
|
|
275
|
+
return;
|
|
276
|
+
} catch (err2) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
`prompts-cache: lock-file race on ${lockPath}: ${err2.message}`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const age = Date.now() - lockStat.mtimeMs;
|
|
283
|
+
if (age > LOCK_STALE_MS) {
|
|
284
|
+
(0, import_node_fs4.rmSync)(lockPath, { force: true });
|
|
285
|
+
const fd = (0, import_node_fs4.openSync)(lockPath, "wx");
|
|
286
|
+
(0, import_node_fs4.closeSync)(fd);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
throw new Error(
|
|
290
|
+
`prompts-cache: refresh already in progress (lock ${lockPath} held, ${Math.round(age / 1e3)}s old)`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
function releaseLock(lockPath) {
|
|
294
|
+
try {
|
|
295
|
+
(0, import_node_fs4.rmSync)(lockPath, { force: true });
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function refreshInternal(opts) {
|
|
300
|
+
const { url, ref, cacheRoot, deployKeyPath } = opts;
|
|
301
|
+
const env = buildGitEnv(deployKeyPath);
|
|
302
|
+
const tmpPath = `${cacheRoot}.tmp`;
|
|
303
|
+
if ((0, import_node_fs4.existsSync)(tmpPath)) {
|
|
304
|
+
(0, import_node_fs4.rmSync)(tmpPath, { recursive: true, force: true });
|
|
305
|
+
}
|
|
306
|
+
const cacheIsCheckout = (0, import_node_fs4.existsSync)(cacheRoot) && (0, import_node_fs4.existsSync)(`${cacheRoot}/.git`);
|
|
307
|
+
if (cacheIsCheckout) {
|
|
308
|
+
try {
|
|
309
|
+
runGit(cacheRoot, ["remote", "set-url", "origin", url], env);
|
|
310
|
+
runGit(cacheRoot, ["fetch", "--prune", "origin", ref], env);
|
|
311
|
+
runGit(cacheRoot, ["checkout", ref], env);
|
|
312
|
+
runGit(cacheRoot, ["reset", "--hard", `origin/${ref}`], env);
|
|
313
|
+
const commitSha2 = runGit(cacheRoot, ["rev-parse", "HEAD"], env).trim();
|
|
314
|
+
return { commitSha: commitSha2, refreshedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
315
|
+
} catch (err) {
|
|
316
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
317
|
+
process.stderr.write(
|
|
318
|
+
`prompts-cache: in-place fetch failed (${reason}), falling back to atomic rebuild
|
|
319
|
+
`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(cacheRoot), { recursive: true });
|
|
324
|
+
runGit((0, import_node_path3.dirname)(cacheRoot), ["clone", "--quiet", "--branch", ref, url, tmpPath], env);
|
|
325
|
+
const commitSha = runGit(tmpPath, ["rev-parse", "HEAD"], env).trim();
|
|
326
|
+
if (!/^[0-9a-f]{40}$/.test(commitSha)) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
`prompts-cache: rev-parse HEAD in ${tmpPath} returned non-SHA ${JSON.stringify(commitSha)}`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
if ((0, import_node_fs4.existsSync)(cacheRoot)) {
|
|
332
|
+
const oldPath = `${cacheRoot}.old`;
|
|
333
|
+
if ((0, import_node_fs4.existsSync)(oldPath)) {
|
|
334
|
+
(0, import_node_fs4.rmSync)(oldPath, { recursive: true, force: true });
|
|
335
|
+
}
|
|
336
|
+
(0, import_node_fs4.renameSync)(cacheRoot, oldPath);
|
|
337
|
+
try {
|
|
338
|
+
(0, import_node_fs4.renameSync)(tmpPath, cacheRoot);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
try {
|
|
341
|
+
(0, import_node_fs4.renameSync)(oldPath, cacheRoot);
|
|
342
|
+
} catch {
|
|
343
|
+
}
|
|
344
|
+
throw err;
|
|
345
|
+
}
|
|
346
|
+
(0, import_node_fs4.rmSync)(oldPath, { recursive: true, force: true });
|
|
347
|
+
} else {
|
|
348
|
+
(0, import_node_fs4.renameSync)(tmpPath, cacheRoot);
|
|
349
|
+
}
|
|
350
|
+
return { commitSha, refreshedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
351
|
+
}
|
|
352
|
+
function buildGitEnv(deployKeyPath) {
|
|
353
|
+
const env = { ...process.env };
|
|
354
|
+
if (!deployKeyPath) return env;
|
|
355
|
+
if (!(0, import_node_fs4.existsSync)(deployKeyPath)) {
|
|
356
|
+
throw new Error(
|
|
357
|
+
`prompts-cache: deployKeyPath ${deployKeyPath} does not exist \u2014 operator must provision the private SSH key`
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const knownHostsPath = process.env["GIT_SSH_KNOWN_HOSTS"] || defaultKnownHostsPath();
|
|
361
|
+
if (!(0, import_node_fs4.existsSync)(knownHostsPath)) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`prompts-cache: known-hosts file ${knownHostsPath} does not exist \u2014 image build is missing server/github-known-hosts`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
env["GIT_SSH_COMMAND"] = [
|
|
367
|
+
"ssh",
|
|
368
|
+
"-i",
|
|
369
|
+
quoteForSshCommand(deployKeyPath),
|
|
370
|
+
"-o",
|
|
371
|
+
"StrictHostKeyChecking=yes",
|
|
372
|
+
"-o",
|
|
373
|
+
`UserKnownHostsFile=${quoteForSshCommand(knownHostsPath)}`,
|
|
374
|
+
"-o",
|
|
375
|
+
"IdentitiesOnly=yes"
|
|
376
|
+
].join(" ");
|
|
377
|
+
return env;
|
|
378
|
+
}
|
|
379
|
+
function quoteForSshCommand(s) {
|
|
380
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
381
|
+
}
|
|
382
|
+
function runGit(cwd, args, env) {
|
|
383
|
+
try {
|
|
384
|
+
return (0, import_node_child_process.execFileSync)("git", args, {
|
|
385
|
+
cwd,
|
|
386
|
+
env,
|
|
387
|
+
encoding: "utf8",
|
|
388
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
389
|
+
});
|
|
390
|
+
} catch (err) {
|
|
391
|
+
const e = err;
|
|
392
|
+
const stderr = typeof e.stderr === "string" ? e.stderr : e.stderr?.toString("utf8") ?? "";
|
|
393
|
+
throw new Error(
|
|
394
|
+
`git ${args.join(" ")} (cwd=${cwd}) failed: ${e.message ?? String(err)}${stderr ? `
|
|
395
|
+
stderr: ${stderr.trim()}` : ""}`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
187
400
|
// src/server/http-server.ts
|
|
188
401
|
var DEFAULT_PORT = 8080;
|
|
189
402
|
var MAX_BODY_BYTES = 16 * 1024;
|
|
403
|
+
var WEBHOOK_MAX_BODY_BYTES = 64 * 1024;
|
|
404
|
+
var WEBHOOK_COALESCE_WINDOW_MS = 5e3;
|
|
405
|
+
var DEFAULT_PROMPTS_CACHE_ROOT = "/srv/git/.prompts-cache";
|
|
406
|
+
var DEFAULT_DEPLOY_KEY_PATH = "/srv/git/.ssh-client-keys/prompts_repo_key";
|
|
190
407
|
var SHORT_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$/;
|
|
191
408
|
var STAMP_PUBKEY_PEM_RE = /^\s*-----BEGIN PUBLIC KEY-----[A-Za-z0-9+/=\s]+-----END PUBLIC KEY-----\s*$/;
|
|
192
409
|
function logLine(level, msg) {
|
|
@@ -206,7 +423,7 @@ function sendJson(res, status, body) {
|
|
|
206
423
|
});
|
|
207
424
|
res.end(payload);
|
|
208
425
|
}
|
|
209
|
-
async function readBody(req) {
|
|
426
|
+
async function readBody(req, maxBytes = MAX_BODY_BYTES) {
|
|
210
427
|
return new Promise((resolve2, reject) => {
|
|
211
428
|
const chunks = [];
|
|
212
429
|
let total = 0;
|
|
@@ -214,7 +431,7 @@ async function readBody(req) {
|
|
|
214
431
|
req.on("data", (chunk) => {
|
|
215
432
|
if (tooLarge) return;
|
|
216
433
|
total += chunk.length;
|
|
217
|
-
if (total >
|
|
434
|
+
if (total > maxBytes) {
|
|
218
435
|
tooLarge = true;
|
|
219
436
|
chunks.length = 0;
|
|
220
437
|
return;
|
|
@@ -366,6 +583,250 @@ async function handlePost(req, res) {
|
|
|
366
583
|
sendJson(res, 500, { ok: false, error: "internal_error" });
|
|
367
584
|
}
|
|
368
585
|
}
|
|
586
|
+
var DELIVERY_ID_RE = /^[A-Za-z0-9._:-]{1,200}$/;
|
|
587
|
+
var webhookState = {
|
|
588
|
+
lastKickoffAt: 0,
|
|
589
|
+
pendingTrailingRefresh: false,
|
|
590
|
+
inflight: null
|
|
591
|
+
};
|
|
592
|
+
var refreshFn = cloneOrFetchPromptsCache;
|
|
593
|
+
function buildRefreshOpts() {
|
|
594
|
+
const url = process.env["STAMP_PROMPTS_REPO_URL"];
|
|
595
|
+
if (!url) return null;
|
|
596
|
+
const ref = process.env["STAMP_PROMPTS_REPO_REF"] || "main";
|
|
597
|
+
const cacheRoot = process.env["STAMP_PROMPTS_CACHE_ROOT"] || DEFAULT_PROMPTS_CACHE_ROOT;
|
|
598
|
+
const deployKeyPath = process.env["STAMP_PROMPTS_DEPLOY_KEY_PATH"] || DEFAULT_DEPLOY_KEY_PATH;
|
|
599
|
+
return { url, ref, cacheRoot, deployKeyPath };
|
|
600
|
+
}
|
|
601
|
+
function verifyWebhookSignature(body, signatureHeader, secret) {
|
|
602
|
+
if (!signatureHeader || !signatureHeader.startsWith("sha256=")) {
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
const providedHex = signatureHeader.slice("sha256=".length);
|
|
606
|
+
if (!/^[0-9a-fA-F]{64}$/.test(providedHex)) {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
const provided = Buffer.from(providedHex, "hex");
|
|
610
|
+
const expected = (0, import_node_crypto4.createHmac)("sha256", secret).update(body).digest();
|
|
611
|
+
if (provided.length !== expected.length) {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
return (0, import_node_crypto4.timingSafeEqual)(provided, expected);
|
|
615
|
+
}
|
|
616
|
+
function scheduleWebhookRefresh(opts, deliveryId) {
|
|
617
|
+
const now = Date.now();
|
|
618
|
+
const elapsed = now - webhookState.lastKickoffAt;
|
|
619
|
+
if (elapsed >= WEBHOOK_COALESCE_WINDOW_MS) {
|
|
620
|
+
kickoffRefresh(opts, deliveryId);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (webhookState.inflight) {
|
|
624
|
+
if (!webhookState.pendingTrailingRefresh) {
|
|
625
|
+
logLine(
|
|
626
|
+
"info",
|
|
627
|
+
`webhook/prompts coalesced delivery=${deliveryId} (refresh in flight, trailing scheduled)`
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
webhookState.pendingTrailingRefresh = true;
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
logLine(
|
|
634
|
+
"info",
|
|
635
|
+
`webhook/prompts coalesced delivery=${deliveryId} (refresh ${Math.round(elapsed)}ms ago, within ${WEBHOOK_COALESCE_WINDOW_MS}ms window)`
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
function kickoffRefresh(opts, deliveryId) {
|
|
639
|
+
webhookState.lastKickoffAt = Date.now();
|
|
640
|
+
const promise = refreshFn(opts);
|
|
641
|
+
webhookState.inflight = promise;
|
|
642
|
+
logLine("info", `webhook/prompts refresh start delivery=${deliveryId}`);
|
|
643
|
+
promise.then((result) => {
|
|
644
|
+
logLine(
|
|
645
|
+
"info",
|
|
646
|
+
`webhook/prompts refresh ok delivery=${deliveryId} sha=${result.commitSha} at=${result.refreshedAt}`
|
|
647
|
+
);
|
|
648
|
+
}).catch((err) => {
|
|
649
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
650
|
+
logLine(
|
|
651
|
+
"error",
|
|
652
|
+
`webhook/prompts refresh failed delivery=${deliveryId}: ${msg}`
|
|
653
|
+
);
|
|
654
|
+
}).finally(() => {
|
|
655
|
+
if (webhookState.inflight === promise) {
|
|
656
|
+
webhookState.inflight = null;
|
|
657
|
+
}
|
|
658
|
+
if (webhookState.pendingTrailingRefresh) {
|
|
659
|
+
webhookState.pendingTrailingRefresh = false;
|
|
660
|
+
kickoffRefresh(opts, `${deliveryId}+trailing`);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
async function handleWebhookPrompts(req, res) {
|
|
665
|
+
const rawDelivery = req.headers["x-github-delivery"];
|
|
666
|
+
const deliveryId = typeof rawDelivery === "string" && DELIVERY_ID_RE.test(rawDelivery) ? rawDelivery : rawDelivery ? "malformed" : "none";
|
|
667
|
+
const remoteAddr = req.socket.remoteAddress ?? "unknown";
|
|
668
|
+
const secret = process.env["STAMP_PROMPTS_WEBHOOK_SECRET"];
|
|
669
|
+
if (!secret) {
|
|
670
|
+
logLine(
|
|
671
|
+
"error",
|
|
672
|
+
`webhook/prompts delivery=${deliveryId} rejected: STAMP_PROMPTS_WEBHOOK_SECRET not configured`
|
|
673
|
+
);
|
|
674
|
+
sendJson(res, 503, {
|
|
675
|
+
ok: false,
|
|
676
|
+
error: "webhook_secret_unconfigured",
|
|
677
|
+
detail: "STAMP_PROMPTS_WEBHOOK_SECRET env var must be set on stamp-server to accept prompt-repo webhooks"
|
|
678
|
+
});
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
let read;
|
|
682
|
+
try {
|
|
683
|
+
read = await readBody(req, WEBHOOK_MAX_BODY_BYTES);
|
|
684
|
+
} catch (e) {
|
|
685
|
+
logLine("warn", `webhook/prompts read body failed delivery=${deliveryId}: ${e.message}`);
|
|
686
|
+
sendJson(res, 400, { ok: false, error: "body_read_failed" });
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (read.tooLarge) {
|
|
690
|
+
logLine(
|
|
691
|
+
"warn",
|
|
692
|
+
`webhook/prompts delivery=${deliveryId} from=${remoteAddr} body too large (> ${WEBHOOK_MAX_BODY_BYTES} bytes)`
|
|
693
|
+
);
|
|
694
|
+
sendJson(res, 413, { ok: false, error: "body_too_large" });
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const sigHeader = req.headers["x-hub-signature-256"];
|
|
698
|
+
const sigValue = Array.isArray(sigHeader) ? sigHeader[0] : sigHeader;
|
|
699
|
+
if (!verifyWebhookSignature(read.buf, sigValue, secret)) {
|
|
700
|
+
logLine(
|
|
701
|
+
"warn",
|
|
702
|
+
`webhook/prompts delivery=${deliveryId} from=${remoteAddr} rejected: invalid signature`
|
|
703
|
+
);
|
|
704
|
+
sendJson(res, 401, { ok: false, error: "invalid_signature" });
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const opts = buildRefreshOpts();
|
|
708
|
+
if (!opts) {
|
|
709
|
+
logLine(
|
|
710
|
+
"error",
|
|
711
|
+
`webhook/prompts delivery=${deliveryId} rejected: STAMP_PROMPTS_REPO_URL not configured`
|
|
712
|
+
);
|
|
713
|
+
sendJson(res, 503, {
|
|
714
|
+
ok: false,
|
|
715
|
+
error: "prompts_repo_url_unconfigured",
|
|
716
|
+
detail: "STAMP_PROMPTS_REPO_URL env var must be set on stamp-server to accept prompt-repo webhooks"
|
|
717
|
+
});
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
sendJson(res, 202, { ok: true });
|
|
721
|
+
logLine(
|
|
722
|
+
"info",
|
|
723
|
+
`webhook/prompts delivery=${deliveryId} accepted (signature valid)`
|
|
724
|
+
);
|
|
725
|
+
setImmediate(() => {
|
|
726
|
+
try {
|
|
727
|
+
scheduleWebhookRefresh(opts, deliveryId);
|
|
728
|
+
} catch (e) {
|
|
729
|
+
logLine(
|
|
730
|
+
"error",
|
|
731
|
+
`webhook/prompts schedule error delivery=${deliveryId}: ${e.message}`
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
var DEFAULT_PROMPTS_POLL_INTERVAL_SEC = 3600;
|
|
737
|
+
var MIN_PROMPTS_POLL_INTERVAL_SEC = 5;
|
|
738
|
+
var pollState = {
|
|
739
|
+
handle: null,
|
|
740
|
+
inflight: false,
|
|
741
|
+
tickCount: 0
|
|
742
|
+
};
|
|
743
|
+
function resolvePromptsPollIntervalSec() {
|
|
744
|
+
const raw = process.env["STAMP_PROMPTS_POLL_INTERVAL_SEC"];
|
|
745
|
+
if (raw === void 0 || raw === "") {
|
|
746
|
+
return DEFAULT_PROMPTS_POLL_INTERVAL_SEC;
|
|
747
|
+
}
|
|
748
|
+
if (raw === "0") return 0;
|
|
749
|
+
if (!/^-?\d+$/.test(raw)) {
|
|
750
|
+
logLine(
|
|
751
|
+
"warn",
|
|
752
|
+
`STAMP_PROMPTS_POLL_INTERVAL_SEC=${JSON.stringify(raw)} is not a non-negative integer; falling back to default ${DEFAULT_PROMPTS_POLL_INTERVAL_SEC}s`
|
|
753
|
+
);
|
|
754
|
+
return DEFAULT_PROMPTS_POLL_INTERVAL_SEC;
|
|
755
|
+
}
|
|
756
|
+
const n = Number(raw);
|
|
757
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
758
|
+
logLine(
|
|
759
|
+
"warn",
|
|
760
|
+
`STAMP_PROMPTS_POLL_INTERVAL_SEC=${JSON.stringify(raw)} is not a positive integer (and not the literal '0' opt-out); falling back to default ${DEFAULT_PROMPTS_POLL_INTERVAL_SEC}s`
|
|
761
|
+
);
|
|
762
|
+
return DEFAULT_PROMPTS_POLL_INTERVAL_SEC;
|
|
763
|
+
}
|
|
764
|
+
if (n > 0 && n < MIN_PROMPTS_POLL_INTERVAL_SEC) {
|
|
765
|
+
logLine(
|
|
766
|
+
"warn",
|
|
767
|
+
`STAMP_PROMPTS_POLL_INTERVAL_SEC=${n} is below the floor of ${MIN_PROMPTS_POLL_INTERVAL_SEC}s; clamping to floor`
|
|
768
|
+
);
|
|
769
|
+
return MIN_PROMPTS_POLL_INTERVAL_SEC;
|
|
770
|
+
}
|
|
771
|
+
return n;
|
|
772
|
+
}
|
|
773
|
+
async function runPollTick(opts) {
|
|
774
|
+
if (pollState.inflight) {
|
|
775
|
+
logLine(
|
|
776
|
+
"info",
|
|
777
|
+
"prompts-poll: skipping tick \u2014 previous refresh still in flight"
|
|
778
|
+
);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
pollState.inflight = true;
|
|
782
|
+
pollState.tickCount += 1;
|
|
783
|
+
try {
|
|
784
|
+
const result = await refreshFn(opts);
|
|
785
|
+
logLine(
|
|
786
|
+
"info",
|
|
787
|
+
`prompts-poll: refresh ok sha=${result.commitSha} at=${result.refreshedAt}`
|
|
788
|
+
);
|
|
789
|
+
} catch (err) {
|
|
790
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
791
|
+
logLine("error", `prompts-poll: refresh failed: ${msg}`);
|
|
792
|
+
} finally {
|
|
793
|
+
pollState.inflight = false;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function startPromptsPollWorker() {
|
|
797
|
+
if (pollState.handle !== null) {
|
|
798
|
+
logLine("warn", "prompts-poll: already started \u2014 ignoring duplicate start");
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
const opts = buildRefreshOpts();
|
|
802
|
+
if (!opts) {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const intervalSec = resolvePromptsPollIntervalSec();
|
|
806
|
+
if (intervalSec === 0) {
|
|
807
|
+
logLine(
|
|
808
|
+
"info",
|
|
809
|
+
"prompts-poll: disabled (STAMP_PROMPTS_POLL_INTERVAL_SEC=0)"
|
|
810
|
+
);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const intervalMs = intervalSec * 1e3;
|
|
814
|
+
const handle = setInterval(() => {
|
|
815
|
+
void runPollTick(opts);
|
|
816
|
+
}, intervalMs);
|
|
817
|
+
handle.unref();
|
|
818
|
+
pollState.handle = handle;
|
|
819
|
+
logLine(
|
|
820
|
+
"info",
|
|
821
|
+
`prompts-poll: started (interval=${intervalSec}s, url=${opts.url}, ref=${opts.ref}, cacheRoot=${opts.cacheRoot})`
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
function stopPromptsPollWorker() {
|
|
825
|
+
if (pollState.handle === null) return;
|
|
826
|
+
clearInterval(pollState.handle);
|
|
827
|
+
pollState.handle = null;
|
|
828
|
+
pollState.inflight = false;
|
|
829
|
+
}
|
|
369
830
|
var HTTP_DEFAULT_PORT = DEFAULT_PORT;
|
|
370
831
|
function startServer(port2 = DEFAULT_PORT) {
|
|
371
832
|
const server = (0, import_node_http.createServer)((req, res) => {
|
|
@@ -378,10 +839,18 @@ function startServer(port2 = DEFAULT_PORT) {
|
|
|
378
839
|
void handlePost(req, res);
|
|
379
840
|
return;
|
|
380
841
|
}
|
|
842
|
+
if (req.method === "POST" && url === "/webhook/prompts") {
|
|
843
|
+
void handleWebhookPrompts(req, res);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
381
846
|
sendJson(res, 404, { ok: false, error: "not_found" });
|
|
382
847
|
});
|
|
383
848
|
server.listen(port2, () => {
|
|
384
849
|
logLine("info", `listening on :${port2}`);
|
|
850
|
+
startPromptsPollWorker();
|
|
851
|
+
});
|
|
852
|
+
server.once("close", () => {
|
|
853
|
+
stopPromptsPollWorker();
|
|
385
854
|
});
|
|
386
855
|
return server;
|
|
387
856
|
}
|