instar 1.3.1348 → 1.3.1349
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/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +15 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/core/LifelineIngressSecret.d.ts +83 -0
- package/dist/core/LifelineIngressSecret.d.ts.map +1 -0
- package/dist/core/LifelineIngressSecret.js +203 -0
- package/dist/core/LifelineIngressSecret.js.map +1 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +15 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
- package/dist/core/WriteDomainRegistry.js +17 -0
- package/dist/core/WriteDomainRegistry.js.map +1 -1
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +6 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/core/keyMaterialPaths.d.ts.map +1 -1
- package/dist/core/keyMaterialPaths.js +16 -0
- package/dist/core/keyMaterialPaths.js.map +1 -1
- package/dist/core/types.d.ts +23 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/version-skew.d.ts +4 -2
- package/dist/core/version-skew.d.ts.map +1 -1
- package/dist/core/version-skew.js +3 -1
- package/dist/core/version-skew.js.map +1 -1
- package/dist/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/dist/lifeline/TelegramLifeline.d.ts +49 -0
- package/dist/lifeline/TelegramLifeline.d.ts.map +1 -1
- package/dist/lifeline/TelegramLifeline.js +202 -17
- package/dist/lifeline/TelegramLifeline.js.map +1 -1
- package/dist/lifeline/forwardErrors.d.ts +11 -0
- package/dist/lifeline/forwardErrors.d.ts.map +1 -1
- package/dist/lifeline/forwardErrors.js +15 -1
- package/dist/lifeline/forwardErrors.js.map +1 -1
- package/dist/monitoring/guardManifest.d.ts.map +1 -1
- package/dist/monitoring/guardManifest.js +24 -0
- package/dist/monitoring/guardManifest.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +4 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/LifelineIngressGuard.d.ts +326 -0
- package/dist/server/LifelineIngressGuard.d.ts.map +1 -0
- package/dist/server/LifelineIngressGuard.js +713 -0
- package/dist/server/LifelineIngressGuard.js.map +1 -0
- package/dist/server/TelegramCallbackDedupe.d.ts +58 -0
- package/dist/server/TelegramCallbackDedupe.d.ts.map +1 -0
- package/dist/server/TelegramCallbackDedupe.js +151 -0
- package/dist/server/TelegramCallbackDedupe.js.map +1 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +218 -10
- package/dist/server/routes.js.map +1 -1
- package/dist/testing/selfActionRegistry.d.ts.map +1 -1
- package/dist/testing/selfActionRegistry.js +98 -0
- package/dist/testing/selfActionRegistry.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +64 -64
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/src/data/state-coherence-registry.json +19 -0
- package/src/scaffold/templates.ts +4 -0
- package/upgrades/1.3.1349.md +59 -0
- package/upgrades/side-effects/telegram-internal-route-lifeline-secret.md +187 -0
- package/upgrades/1.3.1348.md +0 -24
package/dist/server/routes.js
CHANGED
|
@@ -143,6 +143,9 @@ import { reviewWithinBudget } from './outboundGateBudget.js';
|
|
|
143
143
|
import { resolveToneRecipientClass } from './toneRecipientClass.js';
|
|
144
144
|
import { RULE_DISPOSITIONS, buildDegradedToneResult, resolveToneGateOperatorConfig, fingerprintAutomatedTemplate } from '../core/MessagingToneGate.js';
|
|
145
145
|
import { writeLifelineRestartSignal } from '../core/version-skew.js';
|
|
146
|
+
import { LifelineIngressGuard } from './LifelineIngressGuard.js';
|
|
147
|
+
import { sharedLifelineIngressSecretHolder } from '../core/LifelineIngressSecret.js';
|
|
148
|
+
import { TelegramCallbackDedupe } from './TelegramCallbackDedupe.js';
|
|
146
149
|
import { readSessionClocks } from '../core/SessionClockReader.js';
|
|
147
150
|
import { P13_PROTOCOL_VERSION } from '../core/CompletionEvaluator.js';
|
|
148
151
|
import { CodexTaskContinuationStore, parseContinuationTasks } from '../core/CodexTaskContinuationStore.js';
|
|
@@ -2413,6 +2416,90 @@ export function createRoutes(ctx) {
|
|
|
2413
2416
|
dryRun: ctx.liveConfig?.get('threadline.localRouteSignature.dryRun', undefined),
|
|
2414
2417
|
}, ctx.config);
|
|
2415
2418
|
const localEnvelopeSigner = createAgentLocalEnvelopeSigner(ctx.config.projectName, ctx.config.stateDir);
|
|
2419
|
+
// Lifeline ingress secret (docs/specs/telegram-internal-route-lifeline-secret.md):
|
|
2420
|
+
// the two internal Telegram routes require proof the caller is the lifeline,
|
|
2421
|
+
// not a session holding the ordinary Bearer token. The mode is read live.
|
|
2422
|
+
const lifelineIngressGuard = new LifelineIngressGuard({
|
|
2423
|
+
stateDir: ctx.config.stateDir,
|
|
2424
|
+
// Live when a live reader exists (so REMOVING the key takes effect too);
|
|
2425
|
+
// the boot snapshot only when there is none.
|
|
2426
|
+
bypass: () => (ctx.liveConfig
|
|
2427
|
+
? ctx.liveConfig.get('telegramIngress.lifelineSecret.bypass', undefined)
|
|
2428
|
+
: ctx.config.telegramIngress?.lifelineSecret?.bypass) === true,
|
|
2429
|
+
explicitEnforce: () => ctx.liveConfig
|
|
2430
|
+
? ctx.liveConfig.get('telegramIngress.lifelineSecret.enforce', undefined)
|
|
2431
|
+
: ctx.config.telegramIngress?.lifelineSecret?.enforce,
|
|
2432
|
+
config: ctx.config,
|
|
2433
|
+
holder: sharedLifelineIngressSecretHolder(ctx.config.stateDir),
|
|
2434
|
+
// The 60-second boot nudge is a real-time timer that writes into the
|
|
2435
|
+
// state dir; under the test runner routes are built thousands of times
|
|
2436
|
+
// against throwaway dirs, so it is exercised directly there instead.
|
|
2437
|
+
bootNudgeDelayMs: process.env.VITEST ? null : undefined,
|
|
2438
|
+
// The lifeline is the Telegram ingress unless this server polls Telegram
|
|
2439
|
+
// itself, or Telegram is not CONFIGURED at all (then no lifeline can
|
|
2440
|
+
// exist). A configured adapter that is merely absent is UNKNOWN, and
|
|
2441
|
+
// unknown waits for proof like any other machine.
|
|
2442
|
+
lifelineIsIngress: () => {
|
|
2443
|
+
const t = ctx.telegram;
|
|
2444
|
+
if (t)
|
|
2445
|
+
return t.isPolling !== true;
|
|
2446
|
+
const configured = (ctx.config.messaging ?? []).some((m) => m.type === 'telegram' && m.enabled !== false);
|
|
2447
|
+
return configured;
|
|
2448
|
+
},
|
|
2449
|
+
// Answers false when nothing reached the operator, so the guard's daily
|
|
2450
|
+
// throttle is released and the next occurrence tries again.
|
|
2451
|
+
raiseAttention: async (item) => {
|
|
2452
|
+
if (!ctx.telegram)
|
|
2453
|
+
return false;
|
|
2454
|
+
try {
|
|
2455
|
+
await ctx.telegram.createAttentionItem({
|
|
2456
|
+
id: item.id,
|
|
2457
|
+
title: item.title,
|
|
2458
|
+
summary: item.summary,
|
|
2459
|
+
category: 'security',
|
|
2460
|
+
priority: 'HIGH',
|
|
2461
|
+
sourceContext: 'telegram-ingress-lifeline-secret',
|
|
2462
|
+
healthKey: item.id,
|
|
2463
|
+
});
|
|
2464
|
+
return true;
|
|
2465
|
+
}
|
|
2466
|
+
catch {
|
|
2467
|
+
// @silent-fallback-ok — the degradation record and /health already carry it.
|
|
2468
|
+
return false;
|
|
2469
|
+
}
|
|
2470
|
+
},
|
|
2471
|
+
writeRestartSignal: ({ previousVersion }) => {
|
|
2472
|
+
// Never write a target the lifeline cannot match: it would restart an
|
|
2473
|
+
// up-to-date lifeline. No known version → decline, budget unspent.
|
|
2474
|
+
const targetVersion = ProcessIntegrity.getInstance()?.getState().runningVersion;
|
|
2475
|
+
if (!targetVersion)
|
|
2476
|
+
return false;
|
|
2477
|
+
writeLifelineRestartSignal({
|
|
2478
|
+
stateDir: ctx.config.stateDir,
|
|
2479
|
+
requestedBy: 'server-lifeline-secret',
|
|
2480
|
+
reason: 'lifeline-secret-required',
|
|
2481
|
+
previousVersion,
|
|
2482
|
+
targetVersion,
|
|
2483
|
+
});
|
|
2484
|
+
return true;
|
|
2485
|
+
},
|
|
2486
|
+
reportDegradation: ({ kind, reason, impact }) => {
|
|
2487
|
+
DegradationReporter.getInstance().report({
|
|
2488
|
+
feature: 'TelegramIngress.lifelineSecret',
|
|
2489
|
+
primary: 'Internal Telegram routes accept only requests that carry the lifeline secret',
|
|
2490
|
+
fallback: 'Request handled per the current mode; see /health telegramIngress.lifelineSecret',
|
|
2491
|
+
reason: `${kind}: ${reason}`,
|
|
2492
|
+
impact,
|
|
2493
|
+
});
|
|
2494
|
+
},
|
|
2495
|
+
});
|
|
2496
|
+
ctx.guardRegistry?.register('telegramIngress.lifelineSecret', () => lifelineIngressGuard.guardStatus());
|
|
2497
|
+
// One inline-button tap has one effect, however many times it is delivered.
|
|
2498
|
+
// Proven taps go to the durable store; a tap accepted without proof (only
|
|
2499
|
+
// possible before the guard arms) is deduped in memory, so an unproven
|
|
2500
|
+
// caller can neither fill nor evict the durable record of real taps.
|
|
2501
|
+
const telegramCallbackDedupe = new TelegramCallbackDedupe(ctx.config.stateDir);
|
|
2502
|
+
const telegramCallbackDedupeUnproven = new TelegramCallbackDedupe(null);
|
|
2416
2503
|
const localRouteTrustCounters = createLocalRouteTrustCounters();
|
|
2417
2504
|
const localRouteTrustMode = () => resolveLocalRouteTrustMode({
|
|
2418
2505
|
enabled: ctx.liveConfig?.get('threadline.localRouteTrust.enabled', undefined),
|
|
@@ -3923,6 +4010,22 @@ export function createRoutes(ctx) {
|
|
|
3923
4010
|
if (ctx.a2aRelayForward) {
|
|
3924
4011
|
base.threadline = { ...(base.threadline ?? {}), relayForward: { ...ctx.a2aRelayForward.counters } };
|
|
3925
4012
|
}
|
|
4013
|
+
// Lifeline ingress secret: mode, arming, the observe exit rule and the
|
|
4014
|
+
// PERSISTED counters (they survive a restart). AUTHED only; no secret.
|
|
4015
|
+
try {
|
|
4016
|
+
base.telegramIngress = {
|
|
4017
|
+
lifelineSecret: lifelineIngressGuard.health(),
|
|
4018
|
+
callbackDedupe: {
|
|
4019
|
+
tracked: telegramCallbackDedupe.size,
|
|
4020
|
+
duplicatesDropped: telegramCallbackDedupe.duplicates + telegramCallbackDedupeUnproven.duplicates,
|
|
4021
|
+
writeFailures: telegramCallbackDedupe.writeFailures,
|
|
4022
|
+
corruptAtLoad: telegramCallbackDedupe.corruptAtLoad,
|
|
4023
|
+
},
|
|
4024
|
+
};
|
|
4025
|
+
}
|
|
4026
|
+
catch {
|
|
4027
|
+
base.telegramIngress = { lifelineSecret: { state: 'unavailable' } };
|
|
4028
|
+
}
|
|
3926
4029
|
// Local-route trust (docs/specs/a2a-local-route-trust.md): the live mode,
|
|
3927
4030
|
// whether a trust manager is wired, and the verdict counters. AUTHED only.
|
|
3928
4031
|
base.threadline = {
|
|
@@ -23243,7 +23346,10 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
23243
23346
|
// handler below). Avoids per-request log spam while preserving the signal.
|
|
23244
23347
|
let _versionMissingLogged = false;
|
|
23245
23348
|
router.post('/internal/telegram-forward', async (req, res) => {
|
|
23246
|
-
const { topicId, text,
|
|
23349
|
+
const { topicId, text, messageId, lifelineVersion } = req.body;
|
|
23350
|
+
// The sender-identity fields are `let`: a request accepted WITHOUT proof of
|
|
23351
|
+
// the lifeline has them removed below, in one place, before any use.
|
|
23352
|
+
let { fromUserId, fromUsername, fromFirstName,
|
|
23247
23353
|
// a2a spoof-defense fields (MENTOR-LIVE-READINESS-SPEC §Recipient side). The
|
|
23248
23354
|
// lifeline forwards these when present in the Telegram update. Older lifelines
|
|
23249
23355
|
// omit them → `senderIsBot` defaults to falsy → any marker-bearing forwarded
|
|
@@ -23251,6 +23357,33 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
23251
23357
|
// defense, matching the spec invariant that a real user typing a marker MUST
|
|
23252
23358
|
// be dropped). Upgraded lifelines populate them and the hook routes normally.
|
|
23253
23359
|
senderIsBot, senderChatId, senderBotId, } = req.body;
|
|
23360
|
+
// Lifeline-secret gate — FIRST, before any side effect, so a refused
|
|
23361
|
+
// request has had no effect at all and its replay applies exactly once.
|
|
23362
|
+
const ingressDecision = lifelineIngressGuard.evaluate(req.headers, 'telegram-forward', { lifelineVersion });
|
|
23363
|
+
if (ingressDecision.action === 'refuse') {
|
|
23364
|
+
res.status(401).json({ ok: false, error: 'lifeline-secret-required', refused: true, reason: ingressDecision.reason });
|
|
23365
|
+
return;
|
|
23366
|
+
}
|
|
23367
|
+
// Accepted with a secret that is not ours (observe): tell the caller, so a
|
|
23368
|
+
// current lifeline re-reads the file instead of staying stale forever.
|
|
23369
|
+
if (ingressDecision.staleSecretHint)
|
|
23370
|
+
res.setHeader('X-Instar-Lifeline-Secret-Stale', '1');
|
|
23371
|
+
// SENDER NOT TRUSTED: the message is still delivered, but every
|
|
23372
|
+
// body-supplied identity claim is dropped HERE, once, so nothing below —
|
|
23373
|
+
// the inbound log (standing-authorization evidence), the ratifiers, the
|
|
23374
|
+
// a2a hook, either operator auto-bind, command and profile authority in
|
|
23375
|
+
// the message pipeline, the session's sender tag — can read it as an
|
|
23376
|
+
// authenticated sender. Downstream already treats "no sender id" as "no
|
|
23377
|
+
// first-party identity".
|
|
23378
|
+
const senderUntrusted = !ingressDecision.senderTrusted;
|
|
23379
|
+
if (senderUntrusted) {
|
|
23380
|
+
fromUserId = undefined;
|
|
23381
|
+
fromUsername = undefined;
|
|
23382
|
+
fromFirstName = 'Unverified sender';
|
|
23383
|
+
senderIsBot = false;
|
|
23384
|
+
senderChatId = undefined;
|
|
23385
|
+
senderBotId = undefined;
|
|
23386
|
+
}
|
|
23254
23387
|
// Server boot window: if version cache wasn't populated, skip handshake
|
|
23255
23388
|
// rather than 426-erroneously. Lifeline retries after retryAfterMs.
|
|
23256
23389
|
if (!_serverVersionParsed) {
|
|
@@ -23279,14 +23412,18 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
23279
23412
|
// that even if AutoUpdater missed the boundary (deferred restart,
|
|
23280
23413
|
// lockfile race, etc.), the lifeline self-corrects on its next
|
|
23281
23414
|
// tick / supervisor poll. Idempotent vs. AutoUpdater's prior write.
|
|
23415
|
+
// An unverified caller can name any lifelineVersion, so its signal
|
|
23416
|
+
// write goes through the guard's persisted budget; a verified lifeline
|
|
23417
|
+
// is direct evidence and writes as before.
|
|
23282
23418
|
try {
|
|
23283
|
-
|
|
23284
|
-
|
|
23285
|
-
|
|
23286
|
-
|
|
23287
|
-
|
|
23288
|
-
|
|
23289
|
-
|
|
23419
|
+
if (ingressDecision.verified || lifelineIngressGuard.spendSignalBudget())
|
|
23420
|
+
writeLifelineRestartSignal({
|
|
23421
|
+
stateDir: ctx.config.stateDir,
|
|
23422
|
+
requestedBy: 'server-426',
|
|
23423
|
+
reason: 'server-426-direct-evidence',
|
|
23424
|
+
previousVersion: lifelineVersion,
|
|
23425
|
+
targetVersion: decision.serverVersionString,
|
|
23426
|
+
});
|
|
23290
23427
|
}
|
|
23291
23428
|
catch (err) {
|
|
23292
23429
|
// Non-fatal — the 426 response itself still drives the lifeline's
|
|
@@ -23509,7 +23646,10 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
23509
23646
|
console.error(`[telegram-forward] ownership read error (fail-open, claiming/routing normally): ${err instanceof Error ? err.message : err}`);
|
|
23510
23647
|
}
|
|
23511
23648
|
}
|
|
23512
|
-
|
|
23649
|
+
// A request accepted WITHOUT proof of the lifeline chose its own
|
|
23650
|
+
// updateId/messageId: it must not claim a key in the exactly-once ledger,
|
|
23651
|
+
// or it could pre-claim the id of a real message yet to arrive.
|
|
23652
|
+
if (ctx.messageLedger && shouldClaimInbound && !senderUntrusted) {
|
|
23513
23653
|
try {
|
|
23514
23654
|
const eventId = req.body.updateId ?? messageId ?? `${topicId}:${req.body.timestamp ?? Date.now()}`;
|
|
23515
23655
|
const dedupeKey = dedupeKeyFor('telegram', topicId, eventId);
|
|
@@ -23593,8 +23733,10 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
23593
23733
|
if (lifelineSignedByAgent) {
|
|
23594
23734
|
console.log(`[telegram-forward] topic ${topicId} message ${messageId ?? '?'} is agent-signed by "${lifelineSignedByAgent}" — operator auto-bind skipped`);
|
|
23595
23735
|
}
|
|
23736
|
+
// Lifeline-secret gate: belt and braces. The identity was already removed
|
|
23737
|
+
// above for an untrusted request; this bind also checks the flag itself.
|
|
23596
23738
|
if (ctx.topicOperatorStore && !lifelineSignedByAgent && fromUserId !== undefined && fromUserId !== null
|
|
23597
|
-
&& typeof topicId === 'number') {
|
|
23739
|
+
&& typeof topicId === 'number' && !senderUntrusted) {
|
|
23598
23740
|
try {
|
|
23599
23741
|
if (ctx.telegram?.isAuthorizedSender?.(fromUserId)) {
|
|
23600
23742
|
ctx.topicOperatorStore.setAuthenticatedOperator(topicId, {
|
|
@@ -23629,6 +23771,9 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
23629
23771
|
firstName: fromFirstName,
|
|
23630
23772
|
messageThreadId: topicId,
|
|
23631
23773
|
viaLifeline: true,
|
|
23774
|
+
// Explicit, so the message pipeline can refuse authority by the
|
|
23775
|
+
// flag as well as by the missing id.
|
|
23776
|
+
...(senderUntrusted ? { senderUnverified: true } : {}),
|
|
23632
23777
|
// Preserve the platform message id separately from Message.id. The
|
|
23633
23778
|
// latter has a Date.now fallback for routing/dedupe compatibility and
|
|
23634
23779
|
// must NEVER be laundered into operator-establishment evidence.
|
|
@@ -24067,6 +24212,23 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
24067
24212
|
}
|
|
24068
24213
|
res.json({ resolved: entry.resolved, key: entry.key });
|
|
24069
24214
|
});
|
|
24215
|
+
// ── Lifeline startup proof ─────────────────────────────────────
|
|
24216
|
+
// The lifeline posts here once at startup (and whenever the server comes
|
|
24217
|
+
// back) with the ingress secret, so the guard is armed within seconds of a
|
|
24218
|
+
// current lifeline starting instead of waiting for the next Telegram
|
|
24219
|
+
// message. Carries no Telegram input and has no effect beyond the guard.
|
|
24220
|
+
router.post('/internal/lifeline-proof', (req, res) => {
|
|
24221
|
+
const decision = lifelineIngressGuard.evaluate(req.headers, 'lifeline-proof', {
|
|
24222
|
+
lifelineVersion: req.body?.lifelineVersion,
|
|
24223
|
+
});
|
|
24224
|
+
if (decision.action === 'refuse') {
|
|
24225
|
+
res.status(401).json({ ok: false, error: 'lifeline-secret-required', refused: true, reason: decision.reason });
|
|
24226
|
+
return;
|
|
24227
|
+
}
|
|
24228
|
+
if (decision.staleSecretHint)
|
|
24229
|
+
res.setHeader('X-Instar-Lifeline-Secret-Stale', '1');
|
|
24230
|
+
res.json({ ok: true, verified: decision.verified, mode: decision.mode });
|
|
24231
|
+
});
|
|
24070
24232
|
// ── Telegram Callback Query Forwarding (from Lifeline) ────────
|
|
24071
24233
|
// Receives inline keyboard callback queries that the Lifeline forwarded.
|
|
24072
24234
|
// Processes them through TelegramAdapter.processCallbackQuery().
|
|
@@ -24076,10 +24238,54 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
24076
24238
|
res.status(400).json({ error: 'callbackQueryId and data required' });
|
|
24077
24239
|
return;
|
|
24078
24240
|
}
|
|
24241
|
+
if (typeof callbackQueryId !== 'string' && typeof callbackQueryId !== 'number') {
|
|
24242
|
+
res.status(400).json({ error: 'callbackQueryId must be a string or a number' });
|
|
24243
|
+
return;
|
|
24244
|
+
}
|
|
24245
|
+
// Lifeline-secret gate — before any effect. A tap is nothing but a
|
|
24246
|
+
// privileged act (it can be an approval), so once the guard is armed an
|
|
24247
|
+
// unproven tap is refused in EITHER mode, and a tap is refused while our
|
|
24248
|
+
// own secret is unreadable (nobody can be verified). The user is told to
|
|
24249
|
+
// try again by the lifeline.
|
|
24250
|
+
const callbackDecision = lifelineIngressGuard.evaluate(req.headers, 'telegram-callback');
|
|
24251
|
+
if (callbackDecision.action === 'refuse') {
|
|
24252
|
+
if (callbackDecision.reason === 'secret-unavailable') {
|
|
24253
|
+
res.status(503).json({ ok: false, error: 'lifeline-secret-unavailable', refused: true });
|
|
24254
|
+
}
|
|
24255
|
+
else {
|
|
24256
|
+
res.status(401).json({ ok: false, error: 'lifeline-secret-required', refused: true, reason: callbackDecision.reason });
|
|
24257
|
+
}
|
|
24258
|
+
return;
|
|
24259
|
+
}
|
|
24260
|
+
if (callbackDecision.staleSecretHint)
|
|
24261
|
+
res.setHeader('X-Instar-Lifeline-Secret-Stale', '1');
|
|
24079
24262
|
if (!ctx.telegram) {
|
|
24080
24263
|
res.status(503).json({ error: 'Telegram not configured' });
|
|
24081
24264
|
return;
|
|
24082
24265
|
}
|
|
24266
|
+
// Idempotency: the same tap delivered twice (a lifeline retry, or a
|
|
24267
|
+
// re-polled update after a lifeline restart) has ONE effect. Claimed
|
|
24268
|
+
// before processing and kept on failure — at-most-once.
|
|
24269
|
+
const claimStore = callbackDecision.verified ? telegramCallbackDedupe : telegramCallbackDedupeUnproven;
|
|
24270
|
+
// A tap accepted unproven before the guard armed may be re-polled and
|
|
24271
|
+
// arrive again, now proven, after the lifeline restarts: one effect still.
|
|
24272
|
+
const seenUnproven = callbackDecision.verified ? telegramCallbackDedupeUnproven.peek(callbackQueryId) : 'none';
|
|
24273
|
+
const claim = seenUnproven !== 'none' ? seenUnproven : claimStore.claim(callbackQueryId);
|
|
24274
|
+
if (claim === 'duplicate') {
|
|
24275
|
+
res.json({ ok: true, deduped: true });
|
|
24276
|
+
return;
|
|
24277
|
+
}
|
|
24278
|
+
if (claim === 'duplicate-of-failed' || claim === 'duplicate-of-unknown') {
|
|
24279
|
+
// A refusal stays a refusal: the first delivery of this tap FAILED, or
|
|
24280
|
+
// never recorded an outcome (the server stopped mid-handling). Its
|
|
24281
|
+
// redelivery must not read as success. It is still not re-applied.
|
|
24282
|
+
res.status(409).json({
|
|
24283
|
+
ok: false,
|
|
24284
|
+
deduped: true,
|
|
24285
|
+
error: claim === 'duplicate-of-failed' ? 'callback-previously-failed' : 'callback-outcome-unknown',
|
|
24286
|
+
});
|
|
24287
|
+
return;
|
|
24288
|
+
}
|
|
24083
24289
|
try {
|
|
24084
24290
|
// Reconstruct a callback query object and process it through TelegramAdapter
|
|
24085
24291
|
const query = {
|
|
@@ -24090,9 +24296,11 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
24090
24296
|
};
|
|
24091
24297
|
// Use the adapter's public method for processing forwarded callbacks
|
|
24092
24298
|
await ctx.telegram.handleForwardedCallback(query);
|
|
24299
|
+
claimStore.markDone(callbackQueryId);
|
|
24093
24300
|
res.json({ ok: true });
|
|
24094
24301
|
}
|
|
24095
24302
|
catch (err) {
|
|
24303
|
+
claimStore.markFailed(callbackQueryId);
|
|
24096
24304
|
console.error(`[telegram-callback] Processing failed:`, err instanceof Error ? err.message : err);
|
|
24097
24305
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
24098
24306
|
}
|