instar 1.3.704 → 1.3.705
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 +216 -17
- package/dist/commands/server.js.map +1 -1
- package/dist/core/DeliverMessageHandler.d.ts +16 -0
- package/dist/core/DeliverMessageHandler.d.ts.map +1 -1
- package/dist/core/DeliverMessageHandler.js +0 -0
- package/dist/core/DeliverMessageHandler.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +15 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +108 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/QueueDrainLoop.d.ts +6 -2
- package/dist/core/QueueDrainLoop.d.ts.map +1 -1
- package/dist/core/QueueDrainLoop.js +0 -0
- package/dist/core/QueueDrainLoop.js.map +1 -1
- package/dist/core/SessionRouter.d.ts +24 -4
- package/dist/core/SessionRouter.d.ts.map +1 -1
- package/dist/core/SessionRouter.js +25 -2
- package/dist/core/SessionRouter.js.map +1 -1
- package/dist/core/meshRejectionLog.d.ts +37 -0
- package/dist/core/meshRejectionLog.d.ts.map +1 -0
- package/dist/core/meshRejectionLog.js +73 -0
- package/dist/core/meshRejectionLog.js.map +1 -0
- package/dist/core/registryHighWater.d.ts +59 -0
- package/dist/core/registryHighWater.d.ts.map +1 -0
- package/dist/core/registryHighWater.js +120 -0
- package/dist/core/registryHighWater.js.map +1 -0
- package/dist/core/senderRejectionNotice.d.ts +89 -0
- package/dist/core/senderRejectionNotice.d.ts.map +1 -0
- package/dist/core/senderRejectionNotice.js +149 -0
- package/dist/core/senderRejectionNotice.js.map +1 -0
- package/dist/core/senderValidationGate.d.ts +80 -0
- package/dist/core/senderValidationGate.d.ts.map +1 -0
- package/dist/core/senderValidationGate.js +133 -0
- package/dist/core/senderValidationGate.js.map +1 -0
- package/dist/core/types.d.ts +12 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/messaging/MessageProcessingLedger.d.ts +33 -5
- package/dist/messaging/MessageProcessingLedger.d.ts.map +1 -1
- package/dist/messaging/MessageProcessingLedger.js +50 -7
- package/dist/messaging/MessageProcessingLedger.js.map +1 -1
- package/dist/messaging/ingressDedup.d.ts.map +1 -1
- package/dist/messaging/ingressDedup.js +9 -0
- package/dist/messaging/ingressDedup.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +2 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +1 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +36 -0
- package/dist/server/routes.js.map +1 -1
- package/dist/users/UserManager.d.ts +22 -1
- package/dist/users/UserManager.d.ts.map +1 -1
- package/dist/users/UserManager.js +87 -2
- package/dist/users/UserManager.js.map +1 -1
- package/dist/users/testIdentityMarkers.d.ts +94 -0
- package/dist/users/testIdentityMarkers.d.ts.map +1 -0
- package/dist/users/testIdentityMarkers.js +170 -0
- package/dist/users/testIdentityMarkers.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +63 -63
- package/src/scaffold/templates.ts +2 -1
- package/upgrades/1.3.705.md +79 -0
- package/upgrades/side-effects/silent-loss-refusal-conservation.md +124 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry high-water marker + degenerate-state classification
|
|
3
|
+
* (silent-loss-refusal-conservation §2.D — "Verify the State, Not Its Symbol" +
|
|
4
|
+
* "Cross-Store Coherence Is an Invariant").
|
|
5
|
+
*
|
|
6
|
+
* The 2026-07-01 silent-loss root cause: a machine's `users.json` was a fixture-
|
|
7
|
+
* clobbered / emptied store, and sender re-validation armed against it and
|
|
8
|
+
* rejected EVERYONE (including the operator). The fix (§2.D) refuses to arm
|
|
9
|
+
* against a degenerate registry — but a FRESH install writes `users.json` as `[]`,
|
|
10
|
+
* byte-identical to an EMPTIED-by-deletion store. A durable "this registry has
|
|
11
|
+
* held a real user before" high-water marker disambiguates them:
|
|
12
|
+
*
|
|
13
|
+
* - never-populated `[]` (no high-water) → DEGENERATE → fail toward DELIVERY.
|
|
14
|
+
* - emptied-by-deletion `[]` (high-water) → POPULATED → keep REJECTING + shout.
|
|
15
|
+
* - parse-failure / corrupt / partial → UNKNOWN_UNSAFE → fail CLOSED (reject).
|
|
16
|
+
*
|
|
17
|
+
* High-water is MONOTONIC (never cleared on user removal) and machine-local — it
|
|
18
|
+
* shares `users.json`'s FS trust boundary (flipping it in the dangerous direction
|
|
19
|
+
* needs the same local FS access as clobbering users.json itself), so it needs no
|
|
20
|
+
* separate integrity envelope (§2.D at-rest honesty).
|
|
21
|
+
*
|
|
22
|
+
* Pure + dependency-free (fs only): the arm decision reads the RAW file bytes so it
|
|
23
|
+
* can see states the in-memory UserManager (post fixture-refusal) has already
|
|
24
|
+
* dropped. Callers stat-gate the read (§2.D per-call arm decision).
|
|
25
|
+
*/
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
/** The durable high-water marker path (machine-local, shares users.json trust boundary). */
|
|
29
|
+
export function registryHighWaterPath(stateDir) {
|
|
30
|
+
return path.join(stateDir, 'state', 'registry-high-water.json');
|
|
31
|
+
}
|
|
32
|
+
/** Has this machine's authoritative local registry EVER held a resolvable real user? */
|
|
33
|
+
export function readRegistryHighWater(stateDir) {
|
|
34
|
+
try {
|
|
35
|
+
const raw = fs.readFileSync(registryHighWaterPath(stateDir), 'utf-8');
|
|
36
|
+
const obj = JSON.parse(raw);
|
|
37
|
+
return obj?.everPopulated === true;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// @silent-fallback-ok: a high-water read fault → treat as never-populated,
|
|
41
|
+
// which classifies an empty registry as degenerate → DELIVER (fail toward
|
|
42
|
+
// delivery), the safe direction for the silent-loss class this fixes.
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Set the high-water marker (monotonic; idempotent). Called on every path that
|
|
48
|
+
* introduces a real (non-fixture) user into the authoritative local `users.json`
|
|
49
|
+
* — an API/CLI register + a non-fixture initialUsers merge + the §4 boot back-fill.
|
|
50
|
+
* NEVER set from WS2.6 replication-in (advisory, does not enter users.json).
|
|
51
|
+
* Returns true iff it wrote (was previously unset).
|
|
52
|
+
*/
|
|
53
|
+
export function setRegistryHighWater(stateDir, reason) {
|
|
54
|
+
if (readRegistryHighWater(stateDir))
|
|
55
|
+
return false;
|
|
56
|
+
const p = registryHighWaterPath(stateDir);
|
|
57
|
+
try {
|
|
58
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
59
|
+
const tmp = `${p}.${process.pid}.tmp`;
|
|
60
|
+
fs.writeFileSync(tmp, JSON.stringify({ everPopulated: true, setAt: new Date().toISOString(), reason }, null, 2));
|
|
61
|
+
fs.renameSync(tmp, p);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// @silent-fallback-ok: best-effort marker write; a write fault just means the
|
|
66
|
+
// marker isn't set yet — it is retried on the next real-user register, and an
|
|
67
|
+
// unset marker fails TOWARD delivery (never a silent reject).
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Classify the raw on-disk registry state for the sender-re-validation arm
|
|
73
|
+
* decision (§2.D taxonomy). Reads the RAW file (never the in-memory manager) so
|
|
74
|
+
* it sees emptied / corrupt states the manager may have masked.
|
|
75
|
+
*
|
|
76
|
+
* Taxonomy (decision 5):
|
|
77
|
+
* - clean ENOENT + NO high-water → degenerate (deliver — a fresh install).
|
|
78
|
+
* - valid `[]` + NO high-water → degenerate (deliver).
|
|
79
|
+
* - valid `[]` + high-water → populated (emptied locally → keep rejecting + HIGH alert).
|
|
80
|
+
* - ENOENT/missing + high-water → unknown-unsafe (had users, file vanished → fail closed).
|
|
81
|
+
* - valid non-empty array → populated (arm normally).
|
|
82
|
+
* - parse-failure / non-array / partial → unknown-unsafe (corruption/tampering → fail closed).
|
|
83
|
+
*/
|
|
84
|
+
export function classifyRegistry(usersFilePath, stateDir) {
|
|
85
|
+
const highWater = readRegistryHighWater(stateDir);
|
|
86
|
+
let raw;
|
|
87
|
+
try {
|
|
88
|
+
raw = fs.readFileSync(usersFilePath, 'utf-8');
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
// ENOENT (never created) → clean degenerate UNLESS high-water evidence exists.
|
|
92
|
+
if (err?.code === 'ENOENT') {
|
|
93
|
+
return highWater
|
|
94
|
+
? { klass: 'unknown-unsafe', detail: 'users.json missing but high-water present (store vanished)', rawUserCount: 0 }
|
|
95
|
+
: { klass: 'degenerate', detail: 'users.json absent (fresh install)', rawUserCount: 0 };
|
|
96
|
+
}
|
|
97
|
+
// A non-ENOENT read error (permissions, transient lock) is NOT "never
|
|
98
|
+
// populated" — never silently open delivery. Fail closed.
|
|
99
|
+
return { klass: 'unknown-unsafe', detail: `users.json unreadable: ${err?.message ?? 'error'}`, rawUserCount: 0 };
|
|
100
|
+
}
|
|
101
|
+
// A raw-non-empty file that will not parse is corruption / a partial write /
|
|
102
|
+
// tampering — NOT "never populated". Fail CLOSED (§2.D UNKNOWN_UNSAFE).
|
|
103
|
+
let parsed;
|
|
104
|
+
try {
|
|
105
|
+
parsed = JSON.parse(raw);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return { klass: 'unknown-unsafe', detail: 'users.json parse-failure (corrupt/partial-write)', rawUserCount: 0 };
|
|
109
|
+
}
|
|
110
|
+
if (!Array.isArray(parsed)) {
|
|
111
|
+
return { klass: 'unknown-unsafe', detail: 'users.json is not a JSON array (schema mismatch)', rawUserCount: 0 };
|
|
112
|
+
}
|
|
113
|
+
if (parsed.length === 0) {
|
|
114
|
+
return highWater
|
|
115
|
+
? { klass: 'populated', detail: 'users.json is [] with high-water (emptied by deletion)', rawUserCount: 0 }
|
|
116
|
+
: { klass: 'degenerate', detail: 'users.json is [] with no high-water (never populated)', rawUserCount: 0 };
|
|
117
|
+
}
|
|
118
|
+
return { klass: 'populated', detail: `users.json has ${parsed.length} user entr${parsed.length === 1 ? 'y' : 'ies'}`, rawUserCount: parsed.length };
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=registryHighWater.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registryHighWater.js","sourceRoot":"","sources":["../../src/core/registryHighWater.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenderRejectionNoticer (silent-loss-refusal-conservation §2.C — the
|
|
3
|
+
* deterministic, UNIFIED loss notice on the refusing path).
|
|
4
|
+
*
|
|
5
|
+
* When the router yields a first-class `rejected` outcome for a USER-originated
|
|
6
|
+
* message (the owner re-validated the sender and refused), the ingress machine
|
|
7
|
+
* must TELL the user — the parent principle "A Refusal Stays a Refusal" demands
|
|
8
|
+
* every rejection be conserved through a notice, never left to a log line the
|
|
9
|
+
* operator might miss. This helper is the single funnel BOTH the live consumer
|
|
10
|
+
* (Telegram / Slack) and the queue-drain loss path route through, keyed on ONE
|
|
11
|
+
* canonical cause (`sender-deauthorized`), so the two paths can never emit two
|
|
12
|
+
* differently-worded notices.
|
|
13
|
+
*
|
|
14
|
+
* Properties (all injected → unit-testable, deterministic on a fake clock):
|
|
15
|
+
* - Neutral FIXED wording, no topology leak, no resend invitation (§2.C).
|
|
16
|
+
* - Durable per-messageId dedupe (the ledger `markRejected` marker seam) so a
|
|
17
|
+
* replay produces ONE notice; a secondary in-memory 30-min (topic,cause) window.
|
|
18
|
+
* - Cross-topic ceiling: >3 distinct topics for one (peer,cause) in a window →
|
|
19
|
+
* suppress per-topic notices, emit ONE aggregated hub alert (deduped).
|
|
20
|
+
* - Flapping-proof decay: per-(peer,cause) re-notice cadence decays 30m→2h→6h on
|
|
21
|
+
* time-since-first-observed, and only resets after a SUSTAINED clear window.
|
|
22
|
+
* - Sender-side divergence signal: local-resolves + remote-rejects → ONE deduped
|
|
23
|
+
* advisory coherence alert (feeds G1; never auto-remediation).
|
|
24
|
+
*
|
|
25
|
+
* Fire-and-forget: the send seams swallow their own errors; a notice failure
|
|
26
|
+
* never throws into the router/drain.
|
|
27
|
+
*/
|
|
28
|
+
/** The canonical cause the live path and the drain path unify on. */
|
|
29
|
+
export declare const SENDER_DEAUTHORIZED_CAUSE: "sender-deauthorized";
|
|
30
|
+
/** The neutral fixed template (§2.C) — no architecture, no "registry out of sync",
|
|
31
|
+
* no resend invitation, no multi-machine topology. */
|
|
32
|
+
export declare const SENDER_DEAUTHORIZED_NOTICE: string;
|
|
33
|
+
export declare const CROSS_TOPIC_CEILING = 3;
|
|
34
|
+
export declare const NOTICE_WINDOW_MS: number;
|
|
35
|
+
/** Flapping-proof re-notice decay steps (time-since-first-observed). */
|
|
36
|
+
export declare const DECAY_STEPS_MS: readonly [number, number, number];
|
|
37
|
+
/** Sustained-clear window before the decay step resets (≥ the longest step). */
|
|
38
|
+
export declare const SUSTAINED_CLEAR_MS: number;
|
|
39
|
+
export interface SenderRejectionEvent {
|
|
40
|
+
/** 'telegram' → topicId is a numeric topic; 'slack' → slackKey routes in-thread. */
|
|
41
|
+
adapter: 'telegram' | 'slack';
|
|
42
|
+
topicId?: number;
|
|
43
|
+
slackKey?: string;
|
|
44
|
+
/** The rejected message's id — the durable dedupe key. */
|
|
45
|
+
messageId: string;
|
|
46
|
+
/** The rejected sender's platform uid (for the divergence probe). */
|
|
47
|
+
senderUid?: number;
|
|
48
|
+
/** Peer that decided the rejection (for the (peer,cause) ceiling/decay). */
|
|
49
|
+
peer?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface SenderRejectionNoticerDeps {
|
|
52
|
+
/** Send the neutral notice to a Telegram topic (fire-and-forget). */
|
|
53
|
+
sendTelegram: (topicId: number, text: string) => void;
|
|
54
|
+
/** Send the neutral notice in a Slack thread (fire-and-forget). Optional. */
|
|
55
|
+
sendSlack?: (slackKey: string, text: string) => void;
|
|
56
|
+
/** Raise ONE aggregated alert to the operator hub (ceiling breach + divergence). */
|
|
57
|
+
alertHub: (title: string, body: string) => void;
|
|
58
|
+
/**
|
|
59
|
+
* Durable per-messageId dedupe marker (the ledger `markRejected` seam). Returns
|
|
60
|
+
* true iff THIS call first-marked the message rejected (so fire the notice);
|
|
61
|
+
* false → already marked (a replay) → skip. When absent, dedupe falls back to
|
|
62
|
+
* the in-memory window only.
|
|
63
|
+
*/
|
|
64
|
+
markRejectedDurable?: (messageId: string) => boolean;
|
|
65
|
+
/** Ingress-side registry probe for the divergence signal: does the rejected uid
|
|
66
|
+
* resolve on OUR (cached) registry? local-resolves + remote-rejects = divergence. */
|
|
67
|
+
resolvesLocally?: (uid: number) => boolean;
|
|
68
|
+
now?: () => number;
|
|
69
|
+
}
|
|
70
|
+
export declare class SenderRejectionNoticer {
|
|
71
|
+
private readonly d;
|
|
72
|
+
private readonly now;
|
|
73
|
+
/** Per-(peer,cause) episode state (machine-local, in-memory; re-arms on lease flip). */
|
|
74
|
+
private readonly causeState;
|
|
75
|
+
/** In-memory secondary dedupe: (topic|cause) → lastNoticeAt (30-min window). */
|
|
76
|
+
private readonly topicWindow;
|
|
77
|
+
/** Divergence-alert dedupe: peer → lastAlertAt. */
|
|
78
|
+
private readonly divergenceAlerted;
|
|
79
|
+
constructor(deps: SenderRejectionNoticerDeps);
|
|
80
|
+
/**
|
|
81
|
+
* Handle a first-class `rejected` outcome. Idempotent per messageId (durable);
|
|
82
|
+
* enforces the cross-topic ceiling + flapping decay; raises the divergence
|
|
83
|
+
* signal. Never throws.
|
|
84
|
+
*/
|
|
85
|
+
onRejected(evt: SenderRejectionEvent): void;
|
|
86
|
+
private onRejectedInner;
|
|
87
|
+
private emit;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=senderRejectionNotice.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"senderRejectionNotice.d.ts","sourceRoot":"","sources":["../../src/core/senderRejectionNotice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,qEAAqE;AACrE,eAAO,MAAM,yBAAyB,EAAG,qBAA8B,CAAC;AAExE;uDACuD;AACvD,eAAO,MAAM,0BAA0B,QAEc,CAAC;AAEtD,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC,eAAO,MAAM,gBAAgB,QAAiB,CAAC;AAC/C,wEAAwE;AACxE,eAAO,MAAM,cAAc,mCAAoE,CAAC;AAChG,gFAAgF;AAChF,eAAO,MAAM,kBAAkB,QAA4C,CAAC;AAE5E,MAAM,WAAW,oBAAoB;IACnC,oFAAoF;IACpF,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,SAAS,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,0BAA0B;IACzC,qEAAqE;IACrE,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,6EAA6E;IAC7E,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrD,oFAAoF;IACpF,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;IACrD;0FACsF;IACtF,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAC3C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAeD,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAA6B;IAC/C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,wFAAwF;IACxF,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAiC;IAC5D,gFAAgF;IAChF,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD,mDAAmD;IACnD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA6B;gBAEnD,IAAI,EAAE,0BAA0B;IAK5C;;;;OAIG;IACH,UAAU,CAAC,GAAG,EAAE,oBAAoB,GAAG,IAAI;IAQ3C,OAAO,CAAC,eAAe;IAqFvB,OAAO,CAAC,IAAI;CAOb"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenderRejectionNoticer (silent-loss-refusal-conservation §2.C — the
|
|
3
|
+
* deterministic, UNIFIED loss notice on the refusing path).
|
|
4
|
+
*
|
|
5
|
+
* When the router yields a first-class `rejected` outcome for a USER-originated
|
|
6
|
+
* message (the owner re-validated the sender and refused), the ingress machine
|
|
7
|
+
* must TELL the user — the parent principle "A Refusal Stays a Refusal" demands
|
|
8
|
+
* every rejection be conserved through a notice, never left to a log line the
|
|
9
|
+
* operator might miss. This helper is the single funnel BOTH the live consumer
|
|
10
|
+
* (Telegram / Slack) and the queue-drain loss path route through, keyed on ONE
|
|
11
|
+
* canonical cause (`sender-deauthorized`), so the two paths can never emit two
|
|
12
|
+
* differently-worded notices.
|
|
13
|
+
*
|
|
14
|
+
* Properties (all injected → unit-testable, deterministic on a fake clock):
|
|
15
|
+
* - Neutral FIXED wording, no topology leak, no resend invitation (§2.C).
|
|
16
|
+
* - Durable per-messageId dedupe (the ledger `markRejected` marker seam) so a
|
|
17
|
+
* replay produces ONE notice; a secondary in-memory 30-min (topic,cause) window.
|
|
18
|
+
* - Cross-topic ceiling: >3 distinct topics for one (peer,cause) in a window →
|
|
19
|
+
* suppress per-topic notices, emit ONE aggregated hub alert (deduped).
|
|
20
|
+
* - Flapping-proof decay: per-(peer,cause) re-notice cadence decays 30m→2h→6h on
|
|
21
|
+
* time-since-first-observed, and only resets after a SUSTAINED clear window.
|
|
22
|
+
* - Sender-side divergence signal: local-resolves + remote-rejects → ONE deduped
|
|
23
|
+
* advisory coherence alert (feeds G1; never auto-remediation).
|
|
24
|
+
*
|
|
25
|
+
* Fire-and-forget: the send seams swallow their own errors; a notice failure
|
|
26
|
+
* never throws into the router/drain.
|
|
27
|
+
*/
|
|
28
|
+
/** The canonical cause the live path and the drain path unify on. */
|
|
29
|
+
export const SENDER_DEAUTHORIZED_CAUSE = 'sender-deauthorized';
|
|
30
|
+
/** The neutral fixed template (§2.C) — no architecture, no "registry out of sync",
|
|
31
|
+
* no resend invitation, no multi-machine topology. */
|
|
32
|
+
export const SENDER_DEAUTHORIZED_NOTICE = "I got your message but couldn't confirm you as an approved sender, so it wasn't delivered. " +
|
|
33
|
+
"I've logged the details so this can be diagnosed.";
|
|
34
|
+
export const CROSS_TOPIC_CEILING = 3;
|
|
35
|
+
export const NOTICE_WINDOW_MS = 30 * 60 * 1000; // 30-min secondary window
|
|
36
|
+
/** Flapping-proof re-notice decay steps (time-since-first-observed). */
|
|
37
|
+
export const DECAY_STEPS_MS = [30 * 60 * 1000, 2 * 60 * 60 * 1000, 6 * 60 * 60 * 1000];
|
|
38
|
+
/** Sustained-clear window before the decay step resets (≥ the longest step). */
|
|
39
|
+
export const SUSTAINED_CLEAR_MS = DECAY_STEPS_MS[DECAY_STEPS_MS.length - 1];
|
|
40
|
+
export class SenderRejectionNoticer {
|
|
41
|
+
d;
|
|
42
|
+
now;
|
|
43
|
+
/** Per-(peer,cause) episode state (machine-local, in-memory; re-arms on lease flip). */
|
|
44
|
+
causeState = new Map();
|
|
45
|
+
/** In-memory secondary dedupe: (topic|cause) → lastNoticeAt (30-min window). */
|
|
46
|
+
topicWindow = new Map();
|
|
47
|
+
/** Divergence-alert dedupe: peer → lastAlertAt. */
|
|
48
|
+
divergenceAlerted = new Map();
|
|
49
|
+
constructor(deps) {
|
|
50
|
+
this.d = deps;
|
|
51
|
+
this.now = deps.now ?? Date.now;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Handle a first-class `rejected` outcome. Idempotent per messageId (durable);
|
|
55
|
+
* enforces the cross-topic ceiling + flapping decay; raises the divergence
|
|
56
|
+
* signal. Never throws.
|
|
57
|
+
*/
|
|
58
|
+
onRejected(evt) {
|
|
59
|
+
try {
|
|
60
|
+
this.onRejectedInner(evt);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
/* fire-and-forget: a notice fault must never propagate into routing */
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
onRejectedInner(evt) {
|
|
67
|
+
const nowMs = this.now();
|
|
68
|
+
const cause = SENDER_DEAUTHORIZED_CAUSE;
|
|
69
|
+
const peer = evt.peer ?? 'unknown';
|
|
70
|
+
const topicKey = evt.adapter === 'telegram' ? (evt.topicId ?? 'tg?') : (evt.slackKey ?? 'slack?');
|
|
71
|
+
// Durable per-messageId dedupe (the ledger marker). A replay of the SAME
|
|
72
|
+
// rejected message must produce ZERO additional notices.
|
|
73
|
+
if (this.d.markRejectedDurable && !this.d.markRejectedDurable(evt.messageId)) {
|
|
74
|
+
return; // already noticed for this message (durable) → nothing more to do.
|
|
75
|
+
}
|
|
76
|
+
// Divergence signal (advisory, feeds G1) — probe our OWN registry.
|
|
77
|
+
if (evt.senderUid != null && this.d.resolvesLocally) {
|
|
78
|
+
try {
|
|
79
|
+
if (this.d.resolvesLocally(evt.senderUid)) {
|
|
80
|
+
const last = this.divergenceAlerted.get(peer) ?? -Infinity;
|
|
81
|
+
if (nowMs - last >= NOTICE_WINDOW_MS) {
|
|
82
|
+
this.divergenceAlerted.set(peer, nowMs);
|
|
83
|
+
this.d.alertHub('Sender-registry divergence', `A sender resolves on THIS machine but was rejected by "${peer}" — the peer's user registry may be degenerate or a deauthorization is still replicating. Advisory only (feeds the coherence audit); no auto-remediation.`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch { /* @silent-fallback-ok: divergence probe is best-effort — an advisory coherence signal only, never gates the notice */ }
|
|
88
|
+
}
|
|
89
|
+
// (peer,cause) episode bookkeeping (cross-topic ceiling + flapping decay).
|
|
90
|
+
const causeKey = `${peer}|${cause}`;
|
|
91
|
+
let st = this.causeState.get(causeKey);
|
|
92
|
+
if (st) {
|
|
93
|
+
// Sustained-clear reset: if the cause has been silent ≥ the longest decay
|
|
94
|
+
// step, this is a fresh episode — re-arm the fast cadence.
|
|
95
|
+
if (nowMs - st.lastObservedAt >= SUSTAINED_CLEAR_MS) {
|
|
96
|
+
st = undefined;
|
|
97
|
+
this.causeState.delete(causeKey);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (!st) {
|
|
101
|
+
st = { firstObservedAt: nowMs, lastNoticeAt: -Infinity, topics: new Map(), ceilingAlerted: false, lastObservedAt: nowMs };
|
|
102
|
+
this.causeState.set(causeKey, st);
|
|
103
|
+
}
|
|
104
|
+
st.lastObservedAt = nowMs;
|
|
105
|
+
// Prune + record the distinct topic within the rolling window.
|
|
106
|
+
for (const [k, seen] of st.topics) {
|
|
107
|
+
if (nowMs - seen > NOTICE_WINDOW_MS)
|
|
108
|
+
st.topics.delete(k);
|
|
109
|
+
}
|
|
110
|
+
st.topics.set(topicKey, nowMs);
|
|
111
|
+
// Cross-topic ceiling: >3 distinct topics for one (peer,cause) → suppress
|
|
112
|
+
// per-topic notices, emit ONE aggregated hub alert.
|
|
113
|
+
if (st.topics.size > CROSS_TOPIC_CEILING) {
|
|
114
|
+
if (!st.ceilingAlerted) {
|
|
115
|
+
st.ceilingAlerted = true;
|
|
116
|
+
this.d.alertHub('Sender rejections across many topics', `"${peer}" is rejecting senders across ${st.topics.size} topics (cause: ${cause}). Per-topic notices are suppressed to avoid a flood; check the peer's user registry health.`);
|
|
117
|
+
}
|
|
118
|
+
return; // ceiling breached — no per-topic notice.
|
|
119
|
+
}
|
|
120
|
+
// Flapping-proof decay cadence (time-since-first-observed). A short recovery
|
|
121
|
+
// does NOT re-arm the fast cadence (only the sustained-clear reset above does).
|
|
122
|
+
const ageMs = nowMs - st.firstObservedAt;
|
|
123
|
+
let step = DECAY_STEPS_MS[0];
|
|
124
|
+
if (ageMs >= DECAY_STEPS_MS[1])
|
|
125
|
+
step = DECAY_STEPS_MS[2];
|
|
126
|
+
else if (ageMs >= DECAY_STEPS_MS[0])
|
|
127
|
+
step = DECAY_STEPS_MS[1];
|
|
128
|
+
// Secondary in-memory (topic,cause) window bound.
|
|
129
|
+
const windowKey = `${topicKey}|${cause}`;
|
|
130
|
+
const lastForTopic = this.topicWindow.get(windowKey) ?? -Infinity;
|
|
131
|
+
// Emit unless within the current decay step for this (peer,cause) AND within
|
|
132
|
+
// the secondary (topic,cause) window.
|
|
133
|
+
if (nowMs - st.lastNoticeAt < step && nowMs - lastForTopic < NOTICE_WINDOW_MS) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
st.lastNoticeAt = nowMs;
|
|
137
|
+
this.topicWindow.set(windowKey, nowMs);
|
|
138
|
+
this.emit(evt);
|
|
139
|
+
}
|
|
140
|
+
emit(evt) {
|
|
141
|
+
if (evt.adapter === 'telegram' && evt.topicId != null) {
|
|
142
|
+
this.d.sendTelegram(evt.topicId, SENDER_DEAUTHORIZED_NOTICE);
|
|
143
|
+
}
|
|
144
|
+
else if (evt.adapter === 'slack' && evt.slackKey && this.d.sendSlack) {
|
|
145
|
+
this.d.sendSlack(evt.slackKey, SENDER_DEAUTHORIZED_NOTICE);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=senderRejectionNotice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"senderRejectionNotice.js","sourceRoot":"","sources":["../../src/core/senderRejectionNotice.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenderValidationGate (silent-loss-refusal-conservation §2.D — the wiring-time
|
|
3
|
+
* registry gate + per-call re-arm).
|
|
4
|
+
*
|
|
5
|
+
* The 2026-07-01 incident: sender re-validation armed UNCONDITIONALLY against a
|
|
6
|
+
* degenerate `users.json` and rejected EVERYONE (including the operator). This
|
|
7
|
+
* gate re-decides arm/disarm PER MESSAGE (a restored registry re-arms with no
|
|
8
|
+
* restart) by probing REAL registry state — never a config flag ("Verify the
|
|
9
|
+
* State, Not Its Symbol"):
|
|
10
|
+
*
|
|
11
|
+
* - degenerate (never-populated / clean-ENOENT / valid-`[]`-no-high-water) →
|
|
12
|
+
* DISARM → fail toward DELIVERY (a fresh install must let the operator's first
|
|
13
|
+
* message through) + a rate-limited loud log + a deduped alert.
|
|
14
|
+
* - unknown-unsafe (parse-failure / corrupt / partial-write / high-water-but-
|
|
15
|
+
* unreadable) → ARM but fail CLOSED: keep rejecting unresolved senders + HIGH
|
|
16
|
+
* alert. The operator still passes via the LOCAL topic-operator binding (KYP).
|
|
17
|
+
* - populated → the PRIMARY arm decision is the OPERATOR-RESOLUTION store-HEALTH
|
|
18
|
+
* probe: if the CURRENT topic's LOCALLY-bound operator uid does NOT resolve in
|
|
19
|
+
* the registry, DISARM + HIGH alert (the incident's exact signature). Else arm
|
|
20
|
+
* and resolve the actual sender.
|
|
21
|
+
*
|
|
22
|
+
* The registry read is stat-gated on `(mtimeMs, size)` so repeated messages under
|
|
23
|
+
* a degenerate broadcast collapse to O(1) while a restored registry re-arms within
|
|
24
|
+
* one write. Pure logic + injected I/O → both sides of every boundary are testable.
|
|
25
|
+
*/
|
|
26
|
+
import { type RegistryClass } from './registryHighWater.js';
|
|
27
|
+
export type SenderVerdict = 'deliver' | 'reject';
|
|
28
|
+
export interface SenderValidationDecision {
|
|
29
|
+
verdict: SenderVerdict;
|
|
30
|
+
/** armed=false means re-validation was skipped (fail toward delivery). */
|
|
31
|
+
armed: boolean;
|
|
32
|
+
klass: RegistryClass;
|
|
33
|
+
reason: string;
|
|
34
|
+
}
|
|
35
|
+
export interface SenderValidationGateDeps {
|
|
36
|
+
usersFilePath: string;
|
|
37
|
+
stateDir: string;
|
|
38
|
+
/** stat (mtimeMs,size) of users.json for the classify cache; null on ENOENT/error. */
|
|
39
|
+
statUsers: () => {
|
|
40
|
+
mtimeMs: number;
|
|
41
|
+
size: number;
|
|
42
|
+
} | null;
|
|
43
|
+
/** Does this uid resolve in THIS machine's authoritative registry? (read-only.) */
|
|
44
|
+
resolveUid: (uid: number) => boolean;
|
|
45
|
+
/** The CURRENT topic's LOCALLY-bound (authenticated) operator uid, or null. Reads
|
|
46
|
+
* the local topic-operator binding ONLY — never a WS2.6 replicated record. */
|
|
47
|
+
operatorUidForTopic: (session: string) => number | null;
|
|
48
|
+
/** Deduped/rate-limited alert sink (once-per-boot + 24h per cause handled here). */
|
|
49
|
+
alert: (level: 'HIGH' | 'INFO', cause: string, message: string) => void;
|
|
50
|
+
/** Loud log sink (the gate applies its own 1/min-per-cause throttle). */
|
|
51
|
+
log: (line: string) => void;
|
|
52
|
+
now?: () => number;
|
|
53
|
+
/** For tests: override the classifier (default reads the raw file + high-water). */
|
|
54
|
+
classify?: () => {
|
|
55
|
+
klass: RegistryClass;
|
|
56
|
+
detail: string;
|
|
57
|
+
rawUserCount: number;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export declare class SenderValidationGate {
|
|
61
|
+
private readonly d;
|
|
62
|
+
private readonly now;
|
|
63
|
+
private cachedStat;
|
|
64
|
+
private cachedClass;
|
|
65
|
+
private readonly logThrottle;
|
|
66
|
+
private readonly alertDedupe;
|
|
67
|
+
constructor(deps: SenderValidationGateDeps);
|
|
68
|
+
private classifyStatGated;
|
|
69
|
+
private throttledLog;
|
|
70
|
+
private dedupedAlert;
|
|
71
|
+
/**
|
|
72
|
+
* Per-call arm + resolve decision for a forwarded sender envelope. `deliver`
|
|
73
|
+
* means the sender re-validation ALLOWS the message (armed+resolved, or
|
|
74
|
+
* disarmed); `reject` means a first-class refusal (→ `sender-rejected` NACK →
|
|
75
|
+
* §2.A rejected outcome → §2.C notice — the refusal is conserved).
|
|
76
|
+
*/
|
|
77
|
+
decide(uid: number, session: string): SenderValidationDecision;
|
|
78
|
+
private safeOperatorUid;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=senderValidationGate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"senderValidationGate.d.ts","sourceRoot":"","sources":["../../src/core/senderValidationGate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAE9E,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEjD,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,aAAa,CAAC;IACvB,0EAA0E;IAC1E,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,aAAa,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,wBAAwB;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,SAAS,EAAE,MAAM;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC1D,mFAAmF;IACnF,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACrC;mFAC+E;IAC/E,mBAAmB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACxD,oFAAoF;IACpF,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACxE,yEAAyE;IACzE,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM;QAAE,KAAK,EAAE,aAAa,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;CACjF;AAKD,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,CAAC,CAA2B;IAC7C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,UAAU,CAAmE;IACrF,OAAO,CAAC,WAAW,CAA+E;IAClG,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;gBAE7C,IAAI,EAAE,wBAAwB;IAK1C,OAAO,CAAC,iBAAiB;IAgBzB,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,YAAY;IASpB;;;;;OAKG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,wBAAwB;IAoD9D,OAAO,CAAC,eAAe;CAWxB"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenderValidationGate (silent-loss-refusal-conservation §2.D — the wiring-time
|
|
3
|
+
* registry gate + per-call re-arm).
|
|
4
|
+
*
|
|
5
|
+
* The 2026-07-01 incident: sender re-validation armed UNCONDITIONALLY against a
|
|
6
|
+
* degenerate `users.json` and rejected EVERYONE (including the operator). This
|
|
7
|
+
* gate re-decides arm/disarm PER MESSAGE (a restored registry re-arms with no
|
|
8
|
+
* restart) by probing REAL registry state — never a config flag ("Verify the
|
|
9
|
+
* State, Not Its Symbol"):
|
|
10
|
+
*
|
|
11
|
+
* - degenerate (never-populated / clean-ENOENT / valid-`[]`-no-high-water) →
|
|
12
|
+
* DISARM → fail toward DELIVERY (a fresh install must let the operator's first
|
|
13
|
+
* message through) + a rate-limited loud log + a deduped alert.
|
|
14
|
+
* - unknown-unsafe (parse-failure / corrupt / partial-write / high-water-but-
|
|
15
|
+
* unreadable) → ARM but fail CLOSED: keep rejecting unresolved senders + HIGH
|
|
16
|
+
* alert. The operator still passes via the LOCAL topic-operator binding (KYP).
|
|
17
|
+
* - populated → the PRIMARY arm decision is the OPERATOR-RESOLUTION store-HEALTH
|
|
18
|
+
* probe: if the CURRENT topic's LOCALLY-bound operator uid does NOT resolve in
|
|
19
|
+
* the registry, DISARM + HIGH alert (the incident's exact signature). Else arm
|
|
20
|
+
* and resolve the actual sender.
|
|
21
|
+
*
|
|
22
|
+
* The registry read is stat-gated on `(mtimeMs, size)` so repeated messages under
|
|
23
|
+
* a degenerate broadcast collapse to O(1) while a restored registry re-arms within
|
|
24
|
+
* one write. Pure logic + injected I/O → both sides of every boundary are testable.
|
|
25
|
+
*/
|
|
26
|
+
import { classifyRegistry } from './registryHighWater.js';
|
|
27
|
+
const DEGENERATE_LOG_THROTTLE_MS = 60 * 1000; // 1/min per cause
|
|
28
|
+
const ALERT_DEDUPE_MS = 24 * 60 * 60 * 1000; // once-per-boot + 24h per cause
|
|
29
|
+
export class SenderValidationGate {
|
|
30
|
+
d;
|
|
31
|
+
now;
|
|
32
|
+
cachedStat = undefined;
|
|
33
|
+
cachedClass = null;
|
|
34
|
+
logThrottle = new Map();
|
|
35
|
+
alertDedupe = new Map();
|
|
36
|
+
constructor(deps) {
|
|
37
|
+
this.d = deps;
|
|
38
|
+
this.now = deps.now ?? Date.now;
|
|
39
|
+
}
|
|
40
|
+
classifyStatGated() {
|
|
41
|
+
const stat = this.d.statUsers();
|
|
42
|
+
// Re-classify only when (mtimeMs,size) changed vs the cached read.
|
|
43
|
+
const unchanged = this.cachedClass !== null &&
|
|
44
|
+
((stat === null && this.cachedStat === null) ||
|
|
45
|
+
(stat !== null && this.cachedStat != null && stat.mtimeMs === this.cachedStat.mtimeMs && stat.size === this.cachedStat.size));
|
|
46
|
+
if (unchanged)
|
|
47
|
+
return this.cachedClass;
|
|
48
|
+
const c = this.d.classify
|
|
49
|
+
? this.d.classify()
|
|
50
|
+
: classifyRegistry(this.d.usersFilePath, this.d.stateDir);
|
|
51
|
+
this.cachedStat = stat;
|
|
52
|
+
this.cachedClass = c;
|
|
53
|
+
return c;
|
|
54
|
+
}
|
|
55
|
+
throttledLog(cause, line) {
|
|
56
|
+
const nowMs = this.now();
|
|
57
|
+
const last = this.logThrottle.get(cause) ?? -Infinity;
|
|
58
|
+
if (nowMs - last >= DEGENERATE_LOG_THROTTLE_MS) {
|
|
59
|
+
this.logThrottle.set(cause, nowMs);
|
|
60
|
+
this.d.log(line);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
dedupedAlert(level, cause, message) {
|
|
64
|
+
const nowMs = this.now();
|
|
65
|
+
const last = this.alertDedupe.get(cause) ?? -Infinity;
|
|
66
|
+
if (nowMs - last >= ALERT_DEDUPE_MS) {
|
|
67
|
+
this.alertDedupe.set(cause, nowMs);
|
|
68
|
+
this.d.alert(level, cause, message);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Per-call arm + resolve decision for a forwarded sender envelope. `deliver`
|
|
73
|
+
* means the sender re-validation ALLOWS the message (armed+resolved, or
|
|
74
|
+
* disarmed); `reject` means a first-class refusal (→ `sender-rejected` NACK →
|
|
75
|
+
* §2.A rejected outcome → §2.C notice — the refusal is conserved).
|
|
76
|
+
*/
|
|
77
|
+
decide(uid, session) {
|
|
78
|
+
// A non-numeric / zero uid cannot be re-validated on the Telegram path — fail
|
|
79
|
+
// toward delivery (Slack sender re-validation is tracked-followup 4).
|
|
80
|
+
if (!Number.isFinite(uid) || uid === 0) {
|
|
81
|
+
return { verdict: 'deliver', armed: false, klass: 'populated', reason: 'no-numeric-uid' };
|
|
82
|
+
}
|
|
83
|
+
let c;
|
|
84
|
+
try {
|
|
85
|
+
c = this.classifyStatGated();
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// A classifier fault is NOT "never populated" — fail CLOSED conservatively,
|
|
89
|
+
// but still honor the local operator binding (KYP).
|
|
90
|
+
c = { klass: 'unknown-unsafe', detail: 'classifier-error', rawUserCount: 0 };
|
|
91
|
+
}
|
|
92
|
+
if (c.klass === 'degenerate') {
|
|
93
|
+
this.throttledLog('registry-degenerate', `[sender-validation] registry DEGENERATE (${c.detail}) — DISARMING sender re-validation (fail toward delivery). ${session}`);
|
|
94
|
+
this.dedupedAlert('INFO', 'registry-degenerate', `Sender re-validation is DISARMED because this machine's user registry is degenerate (${c.detail}). Messages are delivered without sender re-validation until a real user is registered. This is expected on a fresh install; if it is not, the registry may have been emptied — check the registration path and logs/mesh-rejections.jsonl.`);
|
|
95
|
+
return { verdict: 'deliver', armed: false, klass: c.klass, reason: `degenerate:${c.detail}` };
|
|
96
|
+
}
|
|
97
|
+
const opUid = this.safeOperatorUid(session);
|
|
98
|
+
if (c.klass === 'unknown-unsafe') {
|
|
99
|
+
this.dedupedAlert('HIGH', 'registry-unknown-unsafe', `This machine's user registry is UNPARSEABLE/corrupt (${c.detail}). Sender re-validation is failing CLOSED (rejecting unresolved senders) to avoid trusting a tampered store; the locally-bound operator still passes. Repair the registry — do NOT delete it.`);
|
|
100
|
+
// Operator passes via the LOCAL binding even when the registry can't be
|
|
101
|
+
// parsed (KYP) — the incident's operator must never be locked out.
|
|
102
|
+
if (opUid != null && uid === opUid) {
|
|
103
|
+
return { verdict: 'deliver', armed: true, klass: c.klass, reason: 'unknown-unsafe:operator-via-binding' };
|
|
104
|
+
}
|
|
105
|
+
if (this.d.resolveUid(uid)) {
|
|
106
|
+
return { verdict: 'deliver', armed: true, klass: c.klass, reason: 'unknown-unsafe:resolved' };
|
|
107
|
+
}
|
|
108
|
+
return { verdict: 'reject', armed: true, klass: c.klass, reason: 'unknown-unsafe:unresolved' };
|
|
109
|
+
}
|
|
110
|
+
// populated — PRIMARY arm: the operator-resolution store-HEALTH probe.
|
|
111
|
+
if (opUid != null && !this.d.resolveUid(opUid)) {
|
|
112
|
+
this.dedupedAlert('HIGH', 'operator-unresolvable', `The verified operator bound to a topic on this machine no longer resolves in the user registry (uid ${opUid}). Sender re-validation is DISARMED (fail toward reachability) so the operator is never silently locked out — this is the exact 2026-07-01 silent-loss signature. Check the user registration path.`);
|
|
113
|
+
return { verdict: 'deliver', armed: false, klass: c.klass, reason: 'operator-unresolvable-disarm' };
|
|
114
|
+
}
|
|
115
|
+
// Armed + healthy: resolve the actual sender against the real registry.
|
|
116
|
+
return this.d.resolveUid(uid)
|
|
117
|
+
? { verdict: 'deliver', armed: true, klass: c.klass, reason: 'populated:resolved' }
|
|
118
|
+
: { verdict: 'reject', armed: true, klass: c.klass, reason: 'populated:unresolved' };
|
|
119
|
+
}
|
|
120
|
+
safeOperatorUid(session) {
|
|
121
|
+
try {
|
|
122
|
+
const v = this.d.operatorUidForTopic(session);
|
|
123
|
+
return v != null && Number.isFinite(v) && v !== 0 ? v : null;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// @silent-fallback-ok: an operator-resolution fault → null → the gate
|
|
127
|
+
// declines to arm against a populated registry (fails toward delivery +
|
|
128
|
+
// alerts), never a silent reject of the operator.
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=senderValidationGate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"senderValidationGate.js","sourceRoot":"","sources":["../../src/core/senderValidationGate.ts"],"names":[],"mappings":"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"}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -644,6 +644,18 @@ export interface UserProfile {
|
|
|
644
644
|
* absent it is derived from `permissions`. See src/permissions/SlackPrincipalResolver.ts.
|
|
645
645
|
*/
|
|
646
646
|
orgRole?: string;
|
|
647
|
+
/**
|
|
648
|
+
* Signed override allowing a profile that legitimately COLLIDES with a
|
|
649
|
+
* `TEST_IDENTITY_MARKERS` fixture id to persist + load (silent-loss-refusal-
|
|
650
|
+
* conservation §2.D). Minted only on a dashboard-PIN-authed request; the load
|
|
651
|
+
* path VERIFIES the HMAC (no PIN). On the WS2.6 field-clamp denylist — a
|
|
652
|
+
* legitimately-overridden fixture-collision profile does NOT replicate its
|
|
653
|
+
* marker (re-mint on the peer if it ever occurs). NOT a bare boolean.
|
|
654
|
+
*/
|
|
655
|
+
allowTestIdentity?: {
|
|
656
|
+
marker: string;
|
|
657
|
+
sig: string;
|
|
658
|
+
};
|
|
647
659
|
}
|
|
648
660
|
export interface UserChannel {
|
|
649
661
|
/** Channel type (telegram, slack, discord, email, etc.) */
|