thumbgate 1.34.0 โ†’ 1.34.1

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "thumbgate",
3
3
  "description": "One ๐Ÿ‘Ž becomes a hard rule the agent cannot bypass. Captures thumbs-down feedback, distills it into PreToolUse Pre-Action Checks, enforced across every future Claude Code session.",
4
- "version": "1.34.0",
4
+ "version": "1.34.1",
5
5
  "author": {
6
6
  "name": "Igor Ganapolsky",
7
7
  "email": "ig5973700@gmail.com",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thumbgate",
3
- "version": "1.34.0",
3
+ "version": "1.34.1",
4
4
  "description": "ThumbGate โ€” ๐Ÿ‘๐Ÿ‘Ž feedback that teaches your AI agent. Thumbs down a mistake, it never happens again.",
5
5
  "homepage": "https://thumbgate.ai",
6
6
  "transport": "stdio",
@@ -2,13 +2,13 @@
2
2
  "mcpServers": {
3
3
  "thumbgate": {
4
4
  "command": "npx",
5
- "args": ["--yes", "--package", "thumbgate@1.34.0", "thumbgate", "serve"]
5
+ "args": ["--yes", "--package", "thumbgate@1.34.1", "thumbgate", "serve"]
6
6
  }
7
7
  },
8
8
  "hooks": {
9
9
  "preToolUse": {
10
10
  "command": "npx",
11
- "args": ["--yes", "--package", "thumbgate@1.34.0", "thumbgate", "gate-check"]
11
+ "args": ["--yes", "--package", "thumbgate@1.34.1", "thumbgate", "gate-check"]
12
12
  }
13
13
  }
14
14
  }
@@ -9,12 +9,12 @@ version: "1"
9
9
  skills:
10
10
  thumbgate-gate-check:
11
11
  description: "ThumbGate PreToolUse gate โ€” blocks known-bad tool calls"
12
- command: "npx --yes --package thumbgate@1.34.0 thumbgate gate-check"
12
+ command: "npx --yes --package thumbgate@1.34.1 thumbgate gate-check"
13
13
  trigger: pre_tool_use
14
14
 
15
15
  thumbgate-feedback:
16
16
  description: "ThumbGate feedback capture โ€” logs user prompt context"
17
- command: "npx --yes --package thumbgate@1.34.0 thumbgate hook-auto-capture"
17
+ command: "npx --yes --package thumbgate@1.34.1 thumbgate hook-auto-capture"
18
18
  trigger: user_prompt
19
19
 
20
20
  mcp:
@@ -23,6 +23,6 @@ mcp:
23
23
  args:
24
24
  - "--yes"
25
25
  - "--package"
26
- - "thumbgate@1.34.0"
26
+ - "thumbgate@1.34.1"
27
27
  - "thumbgate"
28
28
  - "serve"
@@ -328,7 +328,7 @@ const {
328
328
  finalizeSession: finalizeFeedbackSession,
329
329
  } = require('../../scripts/feedback-session');
330
330
 
331
- const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.34.0' };
331
+ const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.34.1' };
332
332
  const COMMERCE_CATEGORIES = [
333
333
  'product_recommendation',
334
334
  'brand_compliance',
@@ -7,7 +7,7 @@
7
7
  "npx",
8
8
  "--yes",
9
9
  "--package",
10
- "thumbgate@1.34.0",
10
+ "thumbgate@1.34.1",
11
11
  "thumbgate",
12
12
  "serve"
13
13
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thumbgate",
3
- "version": "1.34.0",
3
+ "version": "1.34.1",
4
4
  "description": "ThumbGate Pre-Action Checks self-improve from ranked lessons and repeated failures, hard-block detected secret leaks, and block matches in strict mode.",
5
5
  "homepage": "https://thumbgate.ai",
6
6
  "repository": {
package/public/index.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <meta name="generator" content="ThumbGate">
7
7
  <meta name="author" content="Igor Ganapolsky">
8
- <meta name="thumbgate-version" content="1.34.0">
8
+ <meta name="thumbgate-version" content="1.34.1">
9
9
  __GOOGLE_SITE_VERIFICATION_META__
10
10
  <link rel="icon" type="image/png" href="/thumbgate-icon.png">
11
11
  <link rel="canonical" href="__APP_ORIGIN__/">
@@ -942,7 +942,7 @@ next decision recorded before execution</pre>
942
942
 
943
943
  <footer>
944
944
  <div class="shell footer-inner">
945
- <span>ThumbGate ยท MIT License ยท npm v1.34.0</span>
945
+ <span>ThumbGate ยท MIT License ยท npm v1.34.1</span>
946
946
  <div class="footer-links">
947
947
  <a href="https://github.com/IgorGanapolsky/ThumbGate" target="_blank" rel="noopener">GitHub</a>
948
948
  <a href="/guide">Technical setup</a>
@@ -10,12 +10,12 @@
10
10
  <meta property="og:title" content="ThumbGate โ€” The Numbers">
11
11
  <meta property="og:description" content="Generated first-party operational snapshot: configured gates, recorded interventions, and explicit zero-evidence caveats.">
12
12
  <meta property="og:type" content="website">
13
- <meta property="og:url" content="https://thumbgate.ai/numbers">
13
+ <meta property="og:url" content="https://thumbgate-production.up.railway.app/numbers">
14
14
  <meta name="twitter:card" content="summary_large_image">
15
- <link rel="canonical" href="https://thumbgate.ai/numbers">
15
+ <link rel="canonical" href="https://thumbgate-production.up.railway.app/numbers">
16
16
  <link rel="icon" type="image/png" href="/thumbgate-icon.png">
17
17
  <link rel="apple-touch-icon" href="/apple-touch-icon.png">
18
- <script defer data-domain="thumbgate.ai" src="https://plausible.io/js/script.js"></script>
18
+ <script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
19
19
 
20
20
  <script type="application/ld+json">
21
21
  {
@@ -25,9 +25,9 @@
25
25
  "alternateName": "thumbgate",
26
26
  "applicationCategory": "DeveloperApplication",
27
27
  "operatingSystem": "Cross-platform, Node.js >=18.18.0",
28
- "softwareVersion": "1.34.0",
29
- "url": "https://thumbgate.ai/numbers",
30
- "dateModified": "2026-05-07",
28
+ "softwareVersion": "1.34.1",
29
+ "url": "https://thumbgate-production.up.railway.app/numbers",
30
+ "dateModified": "2026-08-03",
31
31
  "creator": {
32
32
  "@type": "Person",
33
33
  "name": "Igor Ganapolsky",
@@ -46,7 +46,7 @@
46
46
  "@type": "Dataset",
47
47
  "name": "ThumbGate Operational Snapshot",
48
48
  "description": "First-party operational snapshot from the ThumbGate pre-action check runtime: configured checks, recorded block/warn events, estimated token savings from recorded blocks, and Bayes error rate when the sample supports it.",
49
- "url": "https://thumbgate.ai/numbers",
49
+ "url": "https://thumbgate-production.up.railway.app/numbers",
50
50
  "license": "https://opensource.org/licenses/MIT",
51
51
  "creator": {
52
52
  "@type": "Person",
@@ -57,8 +57,8 @@
57
57
  "https://www.linkedin.com/in/igorganapolsky"
58
58
  ]
59
59
  },
60
- "dateModified": "2026-05-07",
61
- "datePublished": "2026-05-07",
60
+ "dateModified": "2026-08-03",
61
+ "datePublished": "2026-08-03",
62
62
  "keywords": [
63
63
  "AI agent gates",
64
64
  "LLM token savings",
@@ -70,7 +70,7 @@
70
70
  {
71
71
  "@type": "PropertyValue",
72
72
  "name": "configured_gates",
73
- "value": 36
73
+ "value": 40
74
74
  },
75
75
  {
76
76
  "@type": "PropertyValue",
@@ -195,7 +195,6 @@
195
195
  <a href="/learn">Learn</a>
196
196
  <a href="/compare">Compare</a>
197
197
  <a href="/numbers">Numbers</a>
198
- <a href="/evaluations">Evaluations</a>
199
198
  <a href="/dashboard">Dashboard</a>
200
199
  <a href="/pro">Pro</a>
201
200
  </nav>
@@ -203,15 +202,15 @@
203
202
  <main class="container">
204
203
  <h1>The Numbers</h1>
205
204
  <p class="subtitle">Generated first-party operational snapshot from the ThumbGate runtime. This is not customer traction, install volume, revenue, or proof that a configured gate has fired.</p>
206
- <div class="freshness">Updated: 2026-05-07 ยท Version 1.34.0</div>
205
+ <div class="freshness">Updated: 2026-08-03 ยท Version 1.34.1</div>
207
206
  <div class="truth-note"><strong>Read this first:</strong> configured checks are inventory. Recorded blocks and warnings are usage evidence. This snapshot currently reports 0 recorded hard-block event(s) and 0 recorded warning event(s).</div>
208
207
 
209
208
  <h2>Gate enforcement</h2>
210
209
  <div class="stats-grid">
211
210
  <div class="stat-card">
212
211
  <div class="stat-label">Configured checks</div>
213
- <div class="stat-value">36</div>
214
- <div class="stat-sub">36 shipped defaults ยท 0 auto-promoted; inventory, not usage</div>
212
+ <div class="stat-value">40</div>
213
+ <div class="stat-sub">40 shipped defaults ยท 0 auto-promoted; inventory, not usage</div>
215
214
  <a class="stat-source" href="https://github.com/IgorGanapolsky/ThumbGate/blob/main/scripts/gate-stats.js">source: gate-stats.js</a>
216
215
  </div>
217
216
  <div class="stat-card">
@@ -278,7 +277,7 @@
278
277
  <div class="cta">
279
278
  <a href="https://www.npmjs.com/package/thumbgate">Install ThumbGate โ€” npx thumbgate init</a>
280
279
  <div class="footer-note">Prefer the raw feed? See <a href="https://github.com/IgorGanapolsky/ThumbGate">GitHub</a> or run <code>npm run gate:stats</code> locally.</div>
281
- <div class="footer-note">Generated at 2026-05-07T14:59:51.816Z UTC.</div>
280
+ <div class="footer-note">Generated at 2026-08-03T05:26:21.201Z UTC.</div>
282
281
  </div>
283
282
  </main>
284
283
  </body>
@@ -48,6 +48,10 @@ const ECONOMIC_ACTION_PATTERNS = [
48
48
  /\badd\s+(?:a\s+)?(?:credit\s+)?card\b/i,
49
49
  /\badd\s+(?:a\s+)?payment\s+method\b/i,
50
50
  /\b(?:buy|purchase|subscribe|top-?up)\b/i,
51
+ /\bcharge\s+(?:me\s+)?(?:\$|usd\s*)\d/i,
52
+ /\bupgrade\b.{0,40}\b(?:pro|premium|business|enterprise)\b/i,
53
+ /\b(?:activate|cancel|change|create|update|upgrade)\s+(?:a\s+)?(?:\w+\s+){0,3}(?:plan|subscription|tier|seat)\b/i,
54
+ /\b(?:activate|cancel|change|create|update|upgrade)\s+(?:a\s+)?(?:\w+\s+){0,3}(?:checkout|payment\s+method)\b/i,
51
55
  /\b(?:confirm|complete|open|start)\s+(?:the\s+)?checkout\b/i,
52
56
  /\bcheckout\s+(?:flow|page|session)\b/i,
53
57
  /\b(?:issue|send)\s+(?:a\s+)?(?:invoice|payout|refund)\b/i,
@@ -58,8 +62,20 @@ const ECONOMIC_ACTION_PATTERNS = [
58
62
  /\brenew\s+(?:a\s+)?subscription\b/i,
59
63
  /\btransfer\s+(?:dollars?|funds|money|usd)\b/i,
60
64
  /\bupgrade\s+(?:a\s+)?plan\b/i,
61
- /\b(?:charges?|checkout[\s_-]*sessions?|invoices?|payment[\s_-]*intents?|payment[\s_-]*methods?|payouts?|refunds?|subscriptions?|top[\s_-]*ups?|transfers?)\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
62
- /\b(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\s+(?:a\s+)?(?:charge|invoice|payment|payment\s+method|payout|refund|subscription|top-?up|transfer)\b/i,
65
+ /\bcharges?\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
66
+ /\bcheckout[\s_-]*sessions?\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
67
+ /\binvoices?\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
68
+ /\bpayment[\s_-]*intents?\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
69
+ /\bpayment[\s_-]*methods?\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
70
+ /\bpayouts?\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
71
+ /\brefunds?\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
72
+ /\bsubscriptions?\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
73
+ /\btop[\s_-]*ups?\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
74
+ /\btransfers?\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
75
+ /\b(?:attach|cancel|capture|confirm|create|detach)\s+(?:a\s+)?(?:charge|invoice|payment|payment\s+method)\b/i,
76
+ /\b(?:finalize|pay|refund|send|update)\s+(?:a\s+)?(?:charge|invoice|payment|payment\s+method)\b/i,
77
+ /\b(?:attach|cancel|capture|confirm|create|detach)\s+(?:a\s+)?(?:payout|refund|subscription|top-?up|transfer)\b/i,
78
+ /\b(?:finalize|pay|refund|send|update)\s+(?:a\s+)?(?:payout|refund|subscription|top-?up|transfer)\b/i,
63
79
  ];
64
80
 
65
81
  const SCREEN_TOOL_PATTERN = /(?:browser|computer|playwright|puppeteer|selenium|click|tap|press)/i;
@@ -156,14 +172,14 @@ function remoteFinancialLedgerAnchorStore(options = {}) {
156
172
  return {
157
173
  read({ ledgerId }) {
158
174
  const response = invoke({ operation: 'read', ledgerId });
159
- if (!response || response.ok !== true || !Object.hasOwn(response, 'anchor')) {
175
+ if (response?.ok !== true || !Object.hasOwn(response, 'anchor')) {
160
176
  throw financialError('remote financial anchor returned an invalid read response');
161
177
  }
162
178
  return response.anchor;
163
179
  },
164
180
  compareAndSet({ ledgerId, expected, next }) {
165
181
  const response = invoke({ operation: 'compareAndSet', ledgerId, expected, next });
166
- if (!response || response.ok !== true || typeof response.applied !== 'boolean') {
182
+ if (response?.ok !== true || typeof response.applied !== 'boolean') {
167
183
  throw financialError('remote financial anchor returned an invalid compare-and-set response');
168
184
  }
169
185
  return response.applied;
@@ -324,17 +340,28 @@ function detectEconomicAction(toolName, toolInput = {}) {
324
340
  // approved; caller-supplied prose must never downgrade a blind click.
325
341
  if (detectOpaqueScreenMutation(normalizedToolName, toolInput)) return true;
326
342
  const command = shellEconomicText(normalizedToolName, toolInput.command || toolInput.cmd);
343
+ const screenMutationProse = isDeclaredScreenMutation(normalizedToolName, toolInput)
344
+ ? [toolInput.goal, toolInput.description, toolInput.prompt, metadata.context]
345
+ : [];
327
346
  const combined = [
328
347
  normalizedToolName.replace(/[_-]+/g, ' '),
329
348
  command,
330
- toolInput.goal,
331
349
  toolInput.action,
332
350
  toolInput.operation,
333
- metadata.context,
351
+ ...screenMutationProse,
334
352
  ].map((value) => String(value || '')).join(' ');
335
353
  return ECONOMIC_ACTION_PATTERNS.some((pattern) => pattern.test(combined));
336
354
  }
337
355
 
356
+ function isDeclaredScreenMutation(toolName, toolInput = {}) {
357
+ if (!SCREEN_TOOL_PATTERN.test(String(toolName || ''))) return false;
358
+ const input = objectValue(toolInput);
359
+ const declaredOperation = [input.action, input.operation, input.type]
360
+ .map((value) => String(value || ''))
361
+ .join(' ');
362
+ return SCREEN_MUTATION_PATTERN.test(toolName) || SCREEN_MUTATION_PATTERN.test(declaredOperation);
363
+ }
364
+
338
365
  function detectOpaqueScreenMutation(toolName, toolInput = {}) {
339
366
  const normalizedToolName = String(toolName || '').trim();
340
367
  const input = objectValue(toolInput);
@@ -372,8 +399,8 @@ function shellEconomicText(toolName, rawCommand) {
372
399
  return command
373
400
  .split(/(?:&&|\|\||[;\n]|(?<!\|)\|(?!\|))/)
374
401
  .map((segment) => {
375
- const clean = segment.trim().replace(/^(?:sudo\s+)?(?:env\s+)?(?:[A-Za-z_][A-Za-z0-9_]*=[^\s]+\s+)*/, '');
376
- const executable = clean.match(/^([A-Za-z0-9_.\/-]+)/)?.[1]?.split('/').at(-1)?.toLowerCase();
402
+ const clean = segment.trim().replace(/^(?:sudo\s+)?(?:env\s+)?(?:[A-Za-z_]\w*=[^\s]+\s+)*/, '');
403
+ const executable = /^([\w./-]+)/.exec(clean)?.[1]?.split('/').at(-1)?.toLowerCase();
377
404
  if (['rg', 'grep', 'git', 'cat', 'head', 'tail', 'less', 'more', 'echo', 'printf'].includes(executable)) return '';
378
405
  return clean;
379
406
  })
@@ -780,40 +807,38 @@ function validateAttachedControl(result) {
780
807
  }
781
808
  }
782
809
 
783
- function validateRequisition(result, options) {
784
- const reconciliation = reconcilePurchaseLedger(options);
785
- if (!reconciliation.ok && (reconciliation.invalidEventHashes.length > 0
810
+ function reconciliationHasIntegrityFailure(reconciliation) {
811
+ return !reconciliation.ok && (
812
+ reconciliation.invalidEventHashes.length > 0
786
813
  || reconciliation.invalidChainLinks.length > 0
787
814
  || reconciliation.ledgerHeadMismatches.length > 0
788
- || reconciliation.malformedRows.length > 0)) {
789
- const anchorUnavailable = reconciliation.ledgerHeadMismatches.some(
790
- (entry) => optionalString(entry.rollbackResistantAnchorError)
815
+ || reconciliation.malformedRows.length > 0
816
+ );
817
+ }
818
+
819
+ function blockLedgerIntegrityFailures(result, reconciliation) {
820
+ const anchorUnavailable = reconciliation.ledgerHeadMismatches.some(
821
+ (entry) => optionalString(entry.rollbackResistantAnchorError)
822
+ );
823
+ const independentTampering = reconciliation.invalidEventHashes.length > 0
824
+ || reconciliation.invalidChainLinks.length > 0
825
+ || reconciliation.malformedRows.length > 0
826
+ || reconciliation.ledgerHeadMismatches.some(
827
+ (entry) => !optionalString(entry.rollbackResistantAnchorError)
791
828
  );
792
- const independentTampering = reconciliation.invalidEventHashes.length > 0
793
- || reconciliation.invalidChainLinks.length > 0
794
- || reconciliation.malformedRows.length > 0
795
- || reconciliation.ledgerHeadMismatches.some(
796
- (entry) => !optionalString(entry.rollbackResistantAnchorError)
797
- );
798
- if (independentTampering) {
799
- addBlock(result, 'financial_ledger_tampered', 'Financial ledger integrity verification failed.');
800
- }
801
- if (anchorUnavailable) {
802
- addBlock(
803
- result,
804
- 'financial_ledger_anchor_unavailable',
805
- 'Rollback-resistant financial ledger anchor is unavailable; financial actions fail closed.'
806
- );
807
- }
808
- return;
829
+ if (independentTampering) {
830
+ addBlock(result, 'financial_ledger_tampered', 'Financial ledger integrity verification failed.');
809
831
  }
810
- if (!result.requisitionId) return;
811
- const requisition = projectRequisition(result.requisitionId, options);
812
- result.requisition = requisition;
813
- if (!requisition) {
814
- addBlock(result, 'unknown_purchase_requisition', `Purchase requisition '${result.requisitionId}' does not exist.`);
815
- return;
832
+ if (anchorUnavailable) {
833
+ addBlock(
834
+ result,
835
+ 'financial_ledger_anchor_unavailable',
836
+ 'Rollback-resistant financial ledger anchor is unavailable; financial actions fail closed.'
837
+ );
816
838
  }
839
+ }
840
+
841
+ function validateRequisitionBindings(result, requisition, options) {
817
842
  try {
818
843
  verifyPurchaseApprovalBinding(requisition, options);
819
844
  } catch (error) {
@@ -824,6 +849,9 @@ function validateRequisition(result, options) {
824
849
  } catch (error) {
825
850
  addBlock(result, 'reservation_not_bound_to_approval', error.message);
826
851
  }
852
+ }
853
+
854
+ function validateRequisitionConstraints(result, requisition) {
827
855
  if (requisition.status !== 'reserved') {
828
856
  addBlock(result, 'requisition_not_reserved', `Purchase requisition '${result.requisitionId}' is ${requisition.status}, not reserved.`);
829
857
  }
@@ -846,6 +874,23 @@ function validateRequisition(result, options) {
846
874
  if (normalizeToolName(result.actionBinding.toolName) !== normalizeToolName(requisition.approvedToolName)) {
847
875
  addBlock(result, 'financial_tool_mismatch', 'Actual financial tool does not match the approved tool.');
848
876
  }
877
+ }
878
+
879
+ function validateRequisition(result, options) {
880
+ const reconciliation = reconcilePurchaseLedger(options);
881
+ if (reconciliationHasIntegrityFailure(reconciliation)) {
882
+ blockLedgerIntegrityFailures(result, reconciliation);
883
+ return;
884
+ }
885
+ if (!result.requisitionId) return;
886
+ const requisition = projectRequisition(result.requisitionId, options);
887
+ result.requisition = requisition;
888
+ if (!requisition) {
889
+ addBlock(result, 'unknown_purchase_requisition', `Purchase requisition '${result.requisitionId}' does not exist.`);
890
+ return;
891
+ }
892
+ validateRequisitionBindings(result, requisition, options);
893
+ validateRequisitionConstraints(result, requisition);
849
894
  validateScope(result, requisition);
850
895
  }
851
896
 
@@ -897,7 +942,7 @@ function consumeReservation(input, options) {
897
942
  const chain = validateLedgerChain(ledger.events, ledger.malformedRows, ledger.head, options);
898
943
  if (!chain.ok) throw financialError('financial ledger integrity check failed before authorization');
899
944
  const current = projectRequisitionFromEvents(ledger.events, input.requisitionId, options);
900
- if (!current || current.status !== 'reserved') {
945
+ if (current?.status !== 'reserved') {
901
946
  throw financialError(`purchase requisition '${input.requisitionId}' is not available for single-use authorization`);
902
947
  }
903
948
  if (current.reservationId !== input.reservationId) {
@@ -1070,25 +1115,74 @@ function withLedgerLock(options, callback) {
1070
1115
  });
1071
1116
  }
1072
1117
 
1073
- function recoverLedgerTransaction(options = {}) {
1074
- const journalPath = getLedgerJournalPath(options);
1075
- let journal;
1118
+ function readLedgerJournal(journalPath) {
1076
1119
  try {
1077
- journal = JSON.parse(fs.readFileSync(journalPath, 'utf8'));
1120
+ return JSON.parse(fs.readFileSync(journalPath, 'utf8'));
1078
1121
  } catch (error) {
1079
- if (error.code === 'ENOENT') return;
1122
+ if (error.code === 'ENOENT') return null;
1080
1123
  throw financialError(`cannot recover financial ledger journal: ${error.message}`);
1081
1124
  }
1082
- const integrityKey = ledgerIntegrityKey(options);
1125
+ }
1126
+
1127
+ function verifiedJournalEvent(journal, integrityKey) {
1128
+ const event = journal?.event;
1083
1129
  if (journal?.schemaVersion !== LEDGER_JOURNAL_SCHEMA
1084
- || !journal.event
1085
- || journal.event.eventHash !== hashEvent(journal.event)
1130
+ || !event
1131
+ || event.eventHash !== hashEvent(event)
1086
1132
  || !verifyIntegrityRecord(journal, integrityKey)) {
1087
1133
  throw financialError('financial ledger journal integrity verification failed');
1088
1134
  }
1135
+ return event;
1136
+ }
1137
+
1138
+ function recoverAppendedLedgerEvent({
1139
+ ledger,
1140
+ event,
1141
+ previousHead,
1142
+ currentAnchor,
1143
+ headAtPrevious,
1144
+ headAtEvent,
1145
+ journalPath,
1146
+ options,
1147
+ }) {
1148
+ const preceding = ledger.events.at(-2) || null;
1149
+ const expectedPrevious = preceding
1150
+ ? { sequence: preceding.sequence, eventHash: preceding.eventHash }
1151
+ : null;
1152
+ const chain = validateLedgerChain(
1153
+ ledger.events,
1154
+ ledger.malformedRows,
1155
+ null,
1156
+ { ...options, skipLedgerHead: true }
1157
+ );
1158
+ const anchorAtPrevious = sameHead(currentAnchor, previousHead);
1159
+ const anchorAtEvent = sameHead(currentAnchor, { sequence: event.sequence, eventHash: event.eventHash });
1160
+ if (!sameHead(previousHead, expectedPrevious)
1161
+ || !chain.ok
1162
+ || (!headAtPrevious && !headAtEvent)
1163
+ || (!anchorAtPrevious && !anchorAtEvent)) {
1164
+ throw financialError('financial ledger journal does not match the recoverable append');
1165
+ }
1166
+ if (!headAtEvent) writeLedgerHeadFile(event, options);
1167
+ if (!anchorAtEvent) advanceFinancialLedgerAnchor(previousHead, event, options);
1168
+ removeDurableFile(journalPath);
1169
+ }
1089
1170
 
1171
+ function canDiscardPreparedLedgerEvent({ ledger, event, previousHead, currentAnchor, headAtPrevious, options }) {
1172
+ const currentChain = validateLedgerChain(ledger.events, ledger.malformedRows, ledger.head, options);
1173
+ return currentChain.ok
1174
+ && headAtPrevious
1175
+ && sameHead(currentAnchor, previousHead)
1176
+ && event.sequence === ledger.events.length + 1;
1177
+ }
1178
+
1179
+ function recoverLedgerTransaction(options = {}) {
1180
+ const journalPath = getLedgerJournalPath(options);
1181
+ const journal = readLedgerJournal(journalPath);
1182
+ if (!journal) return;
1183
+ const integrityKey = ledgerIntegrityKey(options);
1184
+ const event = verifiedJournalEvent(journal, integrityKey);
1090
1185
  const ledger = readLedger(options);
1091
- const event = journal.event;
1092
1186
  const currentLast = ledger.events.at(-1) || null;
1093
1187
  const previousHead = journal.previousHead;
1094
1188
  const currentHead = ledger.head;
@@ -1099,35 +1193,27 @@ function recoverLedgerTransaction(options = {}) {
1099
1193
  const headAtEvent = sameHead(currentHead, { sequence: event.sequence, eventHash: event.eventHash });
1100
1194
 
1101
1195
  if (eventAlreadyAppended) {
1102
- const preceding = ledger.events.at(-2) || null;
1103
- const expectedPrevious = preceding
1104
- ? { sequence: preceding.sequence, eventHash: preceding.eventHash }
1105
- : null;
1106
- const chain = validateLedgerChain(
1107
- ledger.events,
1108
- ledger.malformedRows,
1109
- null,
1110
- { ...options, skipLedgerHead: true }
1111
- );
1112
- const anchorAtPrevious = sameHead(currentAnchor, previousHead);
1113
- const anchorAtEvent = sameHead(currentAnchor, { sequence: event.sequence, eventHash: event.eventHash });
1114
- if (!sameHead(previousHead, expectedPrevious)
1115
- || !chain.ok
1116
- || (!headAtPrevious && !headAtEvent)
1117
- || (!anchorAtPrevious && !anchorAtEvent)) {
1118
- throw financialError('financial ledger journal does not match the recoverable append');
1119
- }
1120
- if (!headAtEvent) writeLedgerHeadFile(event, options);
1121
- if (!anchorAtEvent) advanceFinancialLedgerAnchor(previousHead, event, options);
1122
- removeDurableFile(journalPath);
1196
+ recoverAppendedLedgerEvent({
1197
+ ledger,
1198
+ event,
1199
+ previousHead,
1200
+ currentAnchor,
1201
+ headAtPrevious,
1202
+ headAtEvent,
1203
+ journalPath,
1204
+ options,
1205
+ });
1123
1206
  return;
1124
1207
  }
1125
1208
 
1126
- const currentChain = validateLedgerChain(ledger.events, ledger.malformedRows, ledger.head, options);
1127
- if (currentChain.ok
1128
- && headAtPrevious
1129
- && sameHead(currentAnchor, previousHead)
1130
- && event.sequence === ledger.events.length + 1) {
1209
+ if (canDiscardPreparedLedgerEvent({
1210
+ ledger,
1211
+ event,
1212
+ previousHead,
1213
+ currentAnchor,
1214
+ headAtPrevious,
1215
+ options,
1216
+ })) {
1131
1217
  // The crash happened before the event append. The caller never received a
1132
1218
  // success response, so discard the prepared transaction instead of
1133
1219
  // executing it during recovery.
@@ -1268,7 +1354,7 @@ function signIntegrityRecord(record, key) {
1268
1354
 
1269
1355
  function verifyIntegrityRecord(record, key) {
1270
1356
  const auth = record?.auth;
1271
- if (!record || !auth || auth.algorithm !== 'hmac-sha256') return false;
1357
+ if (!record || auth?.algorithm !== 'hmac-sha256') return false;
1272
1358
  const expectedKeyId = crypto.createHash('sha256').update(key).digest('hex').slice(0, 16);
1273
1359
  if (auth.keyId !== expectedKeyId || !/^[a-f0-9]{64}$/i.test(String(auth.signature || ''))) return false;
1274
1360
  const expected = crypto.createHmac('sha256', key).update(integrityPayload(record)).digest();
@@ -1365,7 +1451,7 @@ function verifyPurchaseApprovalBinding(requisition, options) {
1365
1451
  } catch (error) {
1366
1452
  throw financialError(`requisition '${requisition.requisitionId}' approval verification failed: ${error.message}`);
1367
1453
  }
1368
- if (!escalation || escalation.status !== 'approved') {
1454
+ if (escalation?.status !== 'approved') {
1369
1455
  throw financialError(`requisition '${requisition.requisitionId}' does not have independent human approval`);
1370
1456
  }
1371
1457
  const expectedApprovalContextDigest = requestComparableHash(requisition);
@@ -3387,8 +3387,9 @@ function evaluateUnconditionalHardFloor(input = {}, options = {}) {
3387
3387
 
3388
3388
  function evaluateFinancialHardFloor(input = {}, consumeReservation = false, options = {}) {
3389
3389
  const toolName = input.tool_name || input.toolName || 'unknown';
3390
- const toolInput = input.tool_input && typeof input.tool_input === 'object'
3391
- ? input.tool_input
3390
+ const rawToolInput = input.tool_input ?? input.toolInput;
3391
+ const toolInput = rawToolInput && typeof rawToolInput === 'object'
3392
+ ? rawToolInput
3392
3393
  : {};
3393
3394
  const normalizedAction = normalizeProviderAction({
3394
3395
  toolName,
package/server.json CHANGED
@@ -8,13 +8,13 @@
8
8
  "source": "github",
9
9
  "url": "https://github.com/IgorGanapolsky/ThumbGate"
10
10
  },
11
- "version": "1.34.0",
11
+ "version": "1.34.1",
12
12
  "packages": [
13
13
  {
14
14
  "registryType": "npm",
15
15
  "registryBaseUrl": "https://registry.npmjs.org",
16
16
  "identifier": "thumbgate",
17
- "version": "1.34.0",
17
+ "version": "1.34.1",
18
18
  "runtimeHint": "npx",
19
19
  "runtimeArguments": [
20
20
  {