thumbgate 1.28.4 → 1.29.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.
Files changed (65) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.well-known/llms.txt +18 -10
  3. package/.well-known/mcp/server-card.json +1 -1
  4. package/README.md +5 -2
  5. package/adapters/claude/.mcp.json +2 -2
  6. package/adapters/forge/forge.yaml +3 -3
  7. package/adapters/mcp/server-stdio.js +1 -1
  8. package/adapters/opencode/opencode.json +1 -1
  9. package/bin/cli.js +8 -8
  10. package/bin/postinstall.js +4 -13
  11. package/config/github-about.json +5 -4
  12. package/config/post-deploy-marketing-pages.json +6 -6
  13. package/docs/integrations/grafana/README.md +109 -0
  14. package/docs/integrations/grafana/thumbgate-revenue-evidence-dashboard.json +1930 -0
  15. package/openapi/openapi.yaml +240 -5
  16. package/package.json +59 -19
  17. package/public/agent-manager.html +10 -11
  18. package/public/agents-cost-savings.html +2 -2
  19. package/public/assets/brand/thumbgate-logo-transparent.svg +6 -11
  20. package/public/assets/brand/thumbgate-mark-inline-v3.svg +11 -10
  21. package/public/assets/brand/thumbgate-mark.svg +10 -11
  22. package/public/blog/inside-your-boundary.html +114 -0
  23. package/public/blog/process-over-outcome-gates.html +119 -0
  24. package/public/blog.html +296 -402
  25. package/public/brand/thumbgate-mark.svg +5 -9
  26. package/public/codex-enterprise.html +2 -2
  27. package/public/compare.html +12 -3
  28. package/public/diagnostic.html +79 -29
  29. package/public/guide.html +4 -4
  30. package/public/index.html +1088 -2098
  31. package/public/install.html +3 -3
  32. package/public/js/buyer-intent.js +33 -18
  33. package/public/numbers.html +2 -2
  34. package/public/pricing.html +268 -408
  35. package/public/pro.html +4 -4
  36. package/scripts/billing.js +456 -126
  37. package/scripts/buyer-paths.js +102 -0
  38. package/scripts/cli-feedback.js +2 -2
  39. package/scripts/commercial-offer.js +18 -10
  40. package/scripts/external-customer-audit.js +881 -0
  41. package/scripts/feedback-loop.js +26 -0
  42. package/scripts/gates-engine.js +123 -1
  43. package/scripts/grafana-revenue-evidence.js +856 -0
  44. package/scripts/jsonl-window.js +89 -0
  45. package/scripts/lesson-embedding-index.js +3 -7
  46. package/scripts/meta-agent-loop.js +20 -2
  47. package/scripts/observability-env.js +139 -0
  48. package/scripts/observability-setup.js +55 -0
  49. package/scripts/plausible-domain-config.js +4 -0
  50. package/scripts/provider-live-evidence.js +1290 -0
  51. package/scripts/provider-payment-reconciler.js +442 -0
  52. package/scripts/provider-revenue-evidence.js +249 -0
  53. package/scripts/rate-limiter.js +1 -5
  54. package/scripts/revenue-action-eligibility.js +414 -0
  55. package/scripts/revenue-evidence-remediation.js +694 -0
  56. package/scripts/revenue-offer-system.js +709 -0
  57. package/scripts/sales-pipeline.js +1117 -0
  58. package/scripts/seo-gsd.js +8 -4
  59. package/scripts/stripe-credentials.js +37 -0
  60. package/scripts/stripe-revenue-catalog-audit.js +363 -0
  61. package/scripts/stripe-revenue-catalog.js +164 -0
  62. package/scripts/telemetry-analytics.js +23 -3
  63. package/scripts/vector-store.js +83 -7
  64. package/scripts/workflow-intake-queue.js +483 -0
  65. package/src/api/server.js +521 -114
@@ -19,6 +19,7 @@ const MARKETING_CLICK_EVENT_TYPES = new Set([
19
19
  'cta_click',
20
20
  'checkout_start',
21
21
  'checkout_bootstrap',
22
+ 'diagnostic_checkout_confirmed',
22
23
  'checkout_interstitial_cta_clicked',
23
24
  'chatgpt_gpt_open',
24
25
  'chatgpt_gpt_click',
@@ -31,6 +32,14 @@ const MARKETING_CLICK_EVENT_TYPES = new Set([
31
32
  'github_repo_click',
32
33
  'community_landing_redirect',
33
34
  ]);
35
+ const CHECKOUT_START_EVENT_TYPES = new Set([
36
+ 'checkout_start',
37
+ 'checkout_bootstrap',
38
+ // A diagnostic buyer has submitted a valid receipt email and the server is
39
+ // issuing the external Payment Link redirect. This is stronger than a CTA
40
+ // click and is the service equivalent of the Pro checkout bootstrap.
41
+ 'diagnostic_checkout_confirmed',
42
+ ]);
34
43
 
35
44
  function shouldIncludeLegacyTelemetry() {
36
45
  if (
@@ -91,6 +100,10 @@ function isMarketingClickEvent(eventType) {
91
100
  return MARKETING_CLICK_EVENT_TYPES.has(String(eventType || '').toLowerCase());
92
101
  }
93
102
 
103
+ function isCheckoutStartEvent(eventType) {
104
+ return CHECKOUT_START_EVENT_TYPES.has(String(eventType || '').toLowerCase());
105
+ }
106
+
94
107
  function getTelemetryPath(feedbackDir) {
95
108
  return path.join(feedbackDir, TELEMETRY_FILE_NAME);
96
109
  }
@@ -666,7 +679,7 @@ function summarizeExternalTrafficQuality(events) {
666
679
  if ((entry.page || entry.landingPath) && !pathRow.pages.includes(entry.page || entry.landingPath)) {
667
680
  pathRow.pages.push(entry.page || entry.landingPath);
668
681
  }
669
- if (eventType === 'checkout_start' || eventType === 'checkout_bootstrap') {
682
+ if (isCheckoutStartEvent(eventType)) {
670
683
  pathRow.checkoutStarts += 1;
671
684
  }
672
685
  visitorPaths.set(visitorKey, pathRow);
@@ -683,7 +696,7 @@ function summarizeExternalTrafficQuality(events) {
683
696
  externalCtaClicks += 1;
684
697
  }
685
698
 
686
- if (eventType === 'checkout_start' || eventType === 'checkout_bootstrap') {
699
+ if (isCheckoutStartEvent(eventType)) {
687
700
  externalCheckoutStarts += 1;
688
701
  incrementCounter(checkoutStartsBySource, entry.source);
689
702
  incrementCounter(checkoutStartsByTrafficChannel, entry.trafficChannel);
@@ -814,6 +827,7 @@ function getTelemetrySummary(feedbackDir, options = {}) {
814
827
  let ctaClicks = 0;
815
828
  let ctaImpressions = 0;
816
829
  let checkoutStarts = 0;
830
+ let diagnosticCheckoutStarts = 0;
817
831
  let checkoutInterstitialViews = 0;
818
832
  let checkoutBotDeflections = 0;
819
833
  let checkoutInterstitialClicks = 0;
@@ -922,8 +936,11 @@ function getTelemetrySummary(feedbackDir, options = {}) {
922
936
  }
923
937
  }
924
938
 
925
- if ((entry.eventType || entry.event) === 'checkout_start' || (entry.eventType || entry.event) === 'checkout_bootstrap') {
939
+ if (isCheckoutStartEvent(entry.eventType || entry.event)) {
926
940
  checkoutStarts += 1;
941
+ if ((entry.eventType || entry.event) === 'diagnostic_checkout_confirmed') {
942
+ diagnosticCheckoutStarts += 1;
943
+ }
927
944
  incrementCounter(checkoutStartsBySource, entry.source);
928
945
  incrementCounter(checkoutStartsByCampaign, entry.utmCampaign);
929
946
  incrementCounter(checkoutStartsByTrafficChannel, entry.trafficChannel);
@@ -1112,6 +1129,7 @@ function getTelemetrySummary(feedbackDir, options = {}) {
1112
1129
  gptOpens,
1113
1130
  gptActionCalls: chatgptActionCalls,
1114
1131
  checkoutStarts,
1132
+ diagnosticCheckoutStarts,
1115
1133
  checkoutInterstitialViews,
1116
1134
  checkoutInterstitialClicks,
1117
1135
  trialEmails,
@@ -1133,6 +1151,7 @@ function getTelemetrySummary(feedbackDir, options = {}) {
1133
1151
  pageViews,
1134
1152
  ctaClicks,
1135
1153
  checkoutStarts,
1154
+ diagnosticCheckoutStarts,
1136
1155
  checkoutInterstitialViews,
1137
1156
  checkoutBotDeflections,
1138
1157
  checkoutInterstitialClicks,
@@ -1317,6 +1336,7 @@ function getTelemetryAnalytics(feedbackDir, options = {}) {
1317
1336
  ctas: {
1318
1337
  totalClicks: summary.web.ctaClicks,
1319
1338
  checkoutStarts: summary.web.checkoutStarts,
1339
+ diagnosticCheckoutStarts: summary.web.diagnosticCheckoutStarts,
1320
1340
  checkoutInterstitialViews: summary.web.checkoutInterstitialViews,
1321
1341
  checkoutBotDeflections: summary.web.checkoutBotDeflections,
1322
1342
  checkoutInterstitialClicks: summary.web.checkoutInterstitialClicks,
@@ -28,6 +28,7 @@ let _lastEmbeddingProfile = null;
28
28
  let _pipelineLoader = null;
29
29
  let _geminiEmbedderForTests = null;
30
30
  const TABLE_NAME = 'thumbgate_memories';
31
+ const FEATURE_HASH_DIMENSIONS = 384;
31
32
 
32
33
  async function getLanceDB() {
33
34
  if (!_lancedb) {
@@ -69,6 +70,58 @@ async function loadPipelineForProfile(profile) {
69
70
  return pipe;
70
71
  }
71
72
 
73
+ function hasLocalTransformerProvider() {
74
+ if (_pipelineLoader) return true;
75
+ try {
76
+ require.resolve('@huggingface/transformers');
77
+ return true;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ function fnv1a32(value) {
84
+ let hash = 0x811c9dc5;
85
+ const bytes = Buffer.from(String(value), 'utf8');
86
+ for (const byte of bytes) {
87
+ hash ^= byte;
88
+ hash = Math.imul(hash, 0x01000193) >>> 0;
89
+ }
90
+ return hash;
91
+ }
92
+
93
+ function addHashedFeature(vector, feature, weight) {
94
+ const hash = fnv1a32(feature);
95
+ const index = hash % vector.length;
96
+ const sign = (hash & 0x80000000) === 0 ? 1 : -1;
97
+ vector[index] += sign * weight;
98
+ }
99
+
100
+ function embedWithFeatureHash(text) {
101
+ const vector = Array(FEATURE_HASH_DIMENSIONS).fill(0);
102
+ const tokens = String(text || '').toLowerCase().match(/[\p{L}\p{N}_-]+/gu) || [];
103
+
104
+ for (let index = 0; index < tokens.length; index += 1) {
105
+ const token = tokens[index];
106
+ addHashedFeature(vector, `token:${token}`, 1);
107
+ if (index > 0) {
108
+ addHashedFeature(vector, `bigram:${tokens[index - 1]}:${token}`, 0.6);
109
+ }
110
+
111
+ const bounded = `^${token.slice(0, 64)}$`;
112
+ for (let offset = 0; offset <= bounded.length - 3; offset += 1) {
113
+ addHashedFeature(vector, `trigram:${bounded.slice(offset, offset + 3)}`, 0.3);
114
+ }
115
+ }
116
+
117
+ const norm = Math.sqrt(vector.reduce((sum, value) => sum + (value * value), 0));
118
+ if (norm === 0) {
119
+ vector[0] = 1;
120
+ return vector;
121
+ }
122
+ return vector.map((value) => value / norm);
123
+ }
124
+
72
125
  async function getEmbeddingPipeline() {
73
126
  const resolved = resolveEmbeddingProfile();
74
127
  const report = writeModelFitReport(getFeedbackDir(), { resolved }).report;
@@ -247,12 +300,33 @@ async function embed(text, options = {}) {
247
300
  console.warn(`Gemini embedding fallback: ${geminiError.message}`);
248
301
  }
249
302
  }
250
- const { pipe, profile } = await getEmbeddingPipeline();
251
- const output = await pipe(truncateForEmbedding(text, profile.activeProfile.maxChars), {
252
- pooling: 'mean',
253
- normalize: true,
254
- });
255
- return Array.from(output.data); // Float32Array -> plain number[] for LanceDB Arrow serialization
303
+ if (hasLocalTransformerProvider()) {
304
+ try {
305
+ const { pipe, profile } = await getEmbeddingPipeline();
306
+ const output = await pipe(truncateForEmbedding(text, profile.activeProfile.maxChars), {
307
+ pooling: 'mean',
308
+ normalize: true,
309
+ });
310
+ return Array.from(output.data); // Float32Array -> plain number[] for LanceDB Arrow serialization
311
+ } catch (transformerError) {
312
+ console.warn(`Transformers.js embedding fallback: ${transformerError.message}`);
313
+ }
314
+ }
315
+
316
+ const vector = embedWithFeatureHash(text);
317
+ _lastEmbeddingProfile = {
318
+ generatedAt: new Date().toISOString(),
319
+ source: 'built-in',
320
+ activeProfile: {
321
+ id: 'feature-hash-v1',
322
+ model: 'ThumbGate feature hashing',
323
+ outputDimensionality: FEATURE_HASH_DIMENSIONS,
324
+ task: options.task || 'code retrieval',
325
+ rationale: 'Deterministic zero-dependency local text embedding.',
326
+ },
327
+ fallbackUsed: false,
328
+ };
329
+ return vector;
256
330
  }
257
331
 
258
332
  async function upsertFeedback(feedbackEvent) {
@@ -269,7 +343,8 @@ async function upsertFeedback(feedbackEvent) {
269
343
  feedbackEvent.whatWorked || '',
270
344
  ].filter(Boolean).join('. ');
271
345
 
272
- // Embed is pure CPU/model work (transformers.js or stub) — deterministic
346
+ // Embed is pure CPU/model work (managed, optional Transformers.js, built-in,
347
+ // or stub) and deterministic for local providers.
273
348
  // for a given input, so no retry is needed here. Retry wraps the table
274
349
  // write below, which is the actual I/O failure surface.
275
350
  const vector = await embed(textForEmbedding, {
@@ -364,4 +439,5 @@ module.exports = {
364
439
  setLanceLoaderForTests,
365
440
  setGeminiEmbedderForTests,
366
441
  truncateForEmbedding,
442
+ embedWithFeatureHash,
367
443
  };
@@ -0,0 +1,483 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const crypto = require('node:crypto');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+
9
+ const { resolveHostedBillingConfig } = require('./hosted-config');
10
+
11
+ const DEFAULT_TIMEOUT_MS = 15000;
12
+ const DEFAULT_LIMIT = 50;
13
+ const MAX_LIMIT = 100;
14
+ const OPERATOR_CONFIG_PATH = path.join(os.homedir(), '.config', 'thumbgate', 'operator.json');
15
+ const ALLOWED_STATUSES = new Set([
16
+ 'all',
17
+ 'new',
18
+ 'qualified',
19
+ 'named_pilot',
20
+ 'proof_backed_run',
21
+ 'paid_team',
22
+ ]);
23
+ const ALLOWED_PRIORITY_BANDS = new Set(['review_now', 'review_next', 'hold_or_nurture']);
24
+ const ALLOWED_FIT_BANDS = new Set(['strong_evidence_for_review', 'partial_evidence', 'incomplete_evidence']);
25
+ const ALLOWED_ROUTES = new Set(['diagnostic', 'close', 'nurture', 'disqualify']);
26
+ const ALLOWED_OPERATOR_STEPS = new Set([
27
+ 'request_action_time_approval',
28
+ 'request_action_time_approval_for_discovery',
29
+ 'review_and_qualify',
30
+ 'prepare_scope_or_hold',
31
+ ]);
32
+ const ALLOWED_CLOSE_STATUSES = new Set(['approval_ready_not_authorized', 'hold_not_approval_ready']);
33
+ const ALLOWED_DISCOVERY_STATUSES = new Set(['approval_ready_not_authorized', 'hold_not_approval_ready']);
34
+ const ALLOWED_UNKNOWNS = new Set([
35
+ 'contactEmail',
36
+ 'workflow',
37
+ 'owner',
38
+ 'repeatedFailure',
39
+ 'runtime',
40
+ 'urgencyAndTrigger',
41
+ 'measurableCurrentImpact',
42
+ 'decisionAuthority',
43
+ 'budgetMechanism',
44
+ 'priceUnderstanding',
45
+ 'proofRequiredToDecide',
46
+ ]);
47
+ const ALLOWED_CLOSE_BLOCKERS = new Set([
48
+ 'qualification_review_unverified',
49
+ 'lifecycle_not_evidence_qualified',
50
+ 'intake_not_current',
51
+ 'contact_path_missing',
52
+ 'contact_path_invalid',
53
+ 'material_unknowns_remaining',
54
+ 'price_understanding_unconfirmed',
55
+ 'offer_unavailable',
56
+ 'route_not_closeable',
57
+ 'checkout_url_unavailable',
58
+ ]);
59
+ const ALLOWED_DISCOVERY_BLOCKERS = new Set([
60
+ 'lifecycle_not_new',
61
+ 'intake_not_current',
62
+ 'contact_path_missing',
63
+ 'contact_path_invalid',
64
+ 'no_material_questions',
65
+ 'disqualified',
66
+ 'zero_spend_unverified',
67
+ ]);
68
+ const ALLOWED_OFFER_IDS = new Set([
69
+ 'workflow_hardening_diagnostic',
70
+ 'workflow_hardening_sprint',
71
+ 'workflow_reliability_operations',
72
+ 'enterprise_governance_pilot',
73
+ 'enterprise_reliability_operations',
74
+ ]);
75
+
76
+ function queueError(code, message, status = null) {
77
+ const error = new Error(message);
78
+ error.code = code;
79
+ if (status !== null) error.status = status;
80
+ return error;
81
+ }
82
+
83
+ function parsePositiveInteger(value, label, maximum = Number.MAX_SAFE_INTEGER) {
84
+ const parsed = Number(value);
85
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > maximum) {
86
+ throw queueError('invalid_argument', `${label} must be an integer from 1 to ${maximum}.`);
87
+ }
88
+ return parsed;
89
+ }
90
+
91
+ function parseArgs(argv = []) {
92
+ const options = {
93
+ json: false,
94
+ statuses: ['new', 'qualified'],
95
+ limit: DEFAULT_LIMIT,
96
+ timeoutMs: DEFAULT_TIMEOUT_MS,
97
+ exportPrivatePath: null,
98
+ };
99
+
100
+ for (const arg of argv) {
101
+ if (arg === '--json') {
102
+ options.json = true;
103
+ continue;
104
+ }
105
+ if (arg.startsWith('--status=')) {
106
+ const statuses = [...new Set(arg.slice('--status='.length)
107
+ .split(',')
108
+ .map((value) => value.trim().toLowerCase())
109
+ .filter(Boolean))];
110
+ if (statuses.length === 0 || statuses.some((status) => !ALLOWED_STATUSES.has(status)) ||
111
+ (statuses.includes('all') && statuses.length !== 1)) {
112
+ throw queueError(
113
+ 'invalid_argument',
114
+ `status must be all or a comma-separated subset of ${[...ALLOWED_STATUSES].filter((value) => value !== 'all').join(', ')}.`
115
+ );
116
+ }
117
+ options.statuses = statuses;
118
+ continue;
119
+ }
120
+ if (arg.startsWith('--limit=')) {
121
+ options.limit = parsePositiveInteger(arg.slice('--limit='.length), 'limit', MAX_LIMIT);
122
+ continue;
123
+ }
124
+ if (arg.startsWith('--timeout-ms=')) {
125
+ options.timeoutMs = parsePositiveInteger(arg.slice('--timeout-ms='.length), 'timeout-ms', 120000);
126
+ continue;
127
+ }
128
+ if (arg.startsWith('--export-private=')) {
129
+ const outputPath = arg.slice('--export-private='.length).trim();
130
+ if (!outputPath || !path.isAbsolute(outputPath)) {
131
+ throw queueError('invalid_argument', 'export-private must be an absolute local file path.');
132
+ }
133
+ options.exportPrivatePath = path.resolve(outputPath);
134
+ continue;
135
+ }
136
+ throw queueError('invalid_argument', `Unknown argument: ${arg}`);
137
+ }
138
+
139
+ return options;
140
+ }
141
+
142
+ function normalizeText(value) {
143
+ const normalized = String(value || '').trim();
144
+ return normalized || null;
145
+ }
146
+
147
+ function loadOperatorConfig(configPath = OPERATOR_CONFIG_PATH) {
148
+ try {
149
+ const stat = fs.lstatSync(configPath);
150
+ if (!stat.isFile() || stat.isSymbolicLink()) return { operatorKey: null, baseUrl: null };
151
+ if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0) {
152
+ return { operatorKey: null, baseUrl: null };
153
+ }
154
+ const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
155
+ return {
156
+ operatorKey: normalizeText(parsed.operatorKey),
157
+ baseUrl: normalizeText(parsed.baseUrl),
158
+ };
159
+ } catch {
160
+ return { operatorKey: null, baseUrl: null };
161
+ }
162
+ }
163
+
164
+ function resolveHostedQueueConfig(env = process.env, operatorConfig = loadOperatorConfig()) {
165
+ const runtime = resolveHostedBillingConfig();
166
+ return {
167
+ apiBaseUrl: normalizeText(env.THUMBGATE_BILLING_API_BASE_URL)
168
+ || normalizeText(operatorConfig.baseUrl)
169
+ || runtime.billingApiBaseUrl,
170
+ apiKey: normalizeText(env.THUMBGATE_OPERATOR_KEY)
171
+ || normalizeText(operatorConfig.operatorKey)
172
+ || normalizeText(env.THUMBGATE_API_KEY),
173
+ };
174
+ }
175
+
176
+ function validateHostedConfig(config = {}) {
177
+ const apiBaseUrl = String(config.apiBaseUrl || '').trim();
178
+ const apiKey = String(config.apiKey || '').trim();
179
+ if (!apiBaseUrl || !apiKey) {
180
+ throw queueError(
181
+ 'operator_config_missing',
182
+ 'Hosted intake queue is not configured. Set THUMBGATE_OPERATOR_KEY or configure the local ThumbGate operator file.'
183
+ );
184
+ }
185
+ let parsed;
186
+ try {
187
+ parsed = new URL(apiBaseUrl);
188
+ } catch {
189
+ throw queueError('operator_config_invalid', 'Hosted intake queue base URL is invalid.');
190
+ }
191
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) {
192
+ throw queueError(
193
+ 'operator_config_invalid',
194
+ 'Hosted intake queue base URL must not contain credentials, query parameters, or fragments.'
195
+ );
196
+ }
197
+ const localHttp = parsed.protocol === 'http:' && ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname);
198
+ if (parsed.protocol !== 'https:' && !localHttp) {
199
+ throw queueError('operator_config_invalid', 'Hosted intake queue requires HTTPS outside localhost.');
200
+ }
201
+ return { apiBaseUrl: parsed.toString(), apiKey };
202
+ }
203
+
204
+ async function fetchWithTimeout(fetchImpl, url, options, timeoutMs) {
205
+ const controller = new AbortController();
206
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
207
+ try {
208
+ return await fetchImpl(url, { ...options, signal: controller.signal });
209
+ } catch (error) {
210
+ if (error && error.name === 'AbortError') {
211
+ throw queueError('intake_queue_timeout', `Hosted intake queue timed out after ${timeoutMs}ms.`);
212
+ }
213
+ throw queueError('intake_queue_unreachable', 'Hosted intake queue could not be reached.');
214
+ } finally {
215
+ clearTimeout(timer);
216
+ }
217
+ }
218
+
219
+ function assertPrivateResponse(response) {
220
+ const cacheControl = String(response.headers?.get?.('cache-control') || '').toLowerCase();
221
+ const vary = String(response.headers?.get?.('vary') || '').toLowerCase();
222
+ if (!cacheControl.includes('private') || !cacheControl.includes('no-store') ||
223
+ !vary.split(',').map((value) => value.trim()).includes('authorization')) {
224
+ throw queueError(
225
+ 'unsafe_cache_policy',
226
+ 'Hosted intake queue response is missing the required private, no-store, Vary: Authorization cache policy.'
227
+ );
228
+ }
229
+ }
230
+
231
+ function validateQueuePayload(payload) {
232
+ const counters = payload && typeof payload === 'object' ? [
233
+ payload.total,
234
+ payload.eligibleTotal,
235
+ payload.returned,
236
+ payload.approvalReadyTotal,
237
+ payload.discoveryReadyTotal,
238
+ ] : [];
239
+ if (!payload || typeof payload !== 'object' || !Array.isArray(payload.leads) ||
240
+ counters.some((value) => !Number.isSafeInteger(value) || value < 0) ||
241
+ payload.eligibleTotal > payload.total || payload.returned !== payload.leads.length ||
242
+ payload.returned > payload.eligibleTotal ||
243
+ payload.approvalReadyTotal + payload.discoveryReadyTotal > payload.eligibleTotal) {
244
+ throw queueError('invalid_queue_payload', 'Hosted intake queue returned an invalid payload.');
245
+ }
246
+ return payload;
247
+ }
248
+
249
+ function leadRef(leadId) {
250
+ return crypto.createHash('sha256').update(String(leadId || '')).digest('hex').slice(0, 12);
251
+ }
252
+
253
+ function allowlistedText(value, allowed) {
254
+ const normalized = String(value || '').trim();
255
+ return allowed.has(normalized) ? normalized : null;
256
+ }
257
+
258
+ function allowlistedList(values, allowed) {
259
+ if (!Array.isArray(values)) return [];
260
+ return [...new Set(values.map((value) => allowlistedText(value, allowed)).filter(Boolean))];
261
+ }
262
+
263
+ function safeTimestamp(value) {
264
+ const parsed = new Date(String(value || ''));
265
+ return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
266
+ }
267
+
268
+ function summarizeLead(lead = {}) {
269
+ const card = lead.qualificationCard || {};
270
+ const discoveryPacket = lead.discoveryPacket || {};
271
+ const closePacket = lead.closePacket || {};
272
+ const closeOfferId = allowlistedText(closePacket.offer?.offerId, ALLOWED_OFFER_IDS);
273
+ const recommendedOfferId = allowlistedText(card.recommendedOffer?.offerId, ALLOWED_OFFER_IDS);
274
+ const closePriceCents = Number.isSafeInteger(closePacket.offer?.priceCents) && closePacket.offer.priceCents >= 0
275
+ ? closePacket.offer.priceCents
276
+ : null;
277
+ const recommendedPriceCents = Number.isSafeInteger(card.recommendedOffer?.priceCents) && card.recommendedOffer.priceCents >= 0
278
+ ? card.recommendedOffer.priceCents
279
+ : null;
280
+ return {
281
+ ref: leadRef(lead.leadId),
282
+ submittedAt: safeTimestamp(lead.submittedAt),
283
+ updatedAt: safeTimestamp(lead.updatedAt),
284
+ lifecycleStatus: allowlistedText(lead.status, ALLOWED_STATUSES),
285
+ priorityRank: Number.isInteger(lead.priorityRank) ? lead.priorityRank : null,
286
+ priorityScore: Number.isFinite(card.priorityScore) ? card.priorityScore : null,
287
+ priorityBand: allowlistedText(card.priorityBand, ALLOWED_PRIORITY_BANDS),
288
+ fitBand: allowlistedText(card.fitBand, ALLOWED_FIT_BANDS),
289
+ route: allowlistedText(card.route, ALLOWED_ROUTES),
290
+ unknowns: allowlistedList(card.unknowns, ALLOWED_UNKNOWNS),
291
+ nextOperatorStep: allowlistedText(lead.nextOperatorStep, ALLOWED_OPERATOR_STEPS),
292
+ discoveryStatus: allowlistedText(discoveryPacket.status, ALLOWED_DISCOVERY_STATUSES),
293
+ discoveryBlockers: allowlistedList(discoveryPacket.blockers, ALLOWED_DISCOVERY_BLOCKERS),
294
+ closeStatus: allowlistedText(closePacket.status, ALLOWED_CLOSE_STATUSES),
295
+ closeBlockers: allowlistedList(closePacket.blockers, ALLOWED_CLOSE_BLOCKERS),
296
+ offerId: closeOfferId || recommendedOfferId,
297
+ priceCents: closePriceCents ?? recommendedPriceCents,
298
+ };
299
+ }
300
+
301
+ function summarizeQueue(payload, apiBaseUrl) {
302
+ const byStatus = {};
303
+ for (const status of ALLOWED_STATUSES) {
304
+ if (status === 'all') continue;
305
+ const count = payload.byStatus?.[status];
306
+ if (Number.isSafeInteger(count) && count >= 0) byStatus[status] = count;
307
+ }
308
+ return {
309
+ generatedAt: safeTimestamp(payload.generatedAt),
310
+ source: 'hosted_operator_intake_queue',
311
+ apiOrigin: new URL(apiBaseUrl).origin,
312
+ queueAvailable: true,
313
+ total: payload.total,
314
+ eligibleTotal: payload.eligibleTotal,
315
+ returned: Number.isInteger(payload.returned) ? payload.returned : payload.leads.length,
316
+ approvalReadyTotal: payload.approvalReadyTotal,
317
+ discoveryReadyTotal: payload.discoveryReadyTotal,
318
+ primaryApprovalActionAvailable: Boolean(payload.primaryApprovalAction),
319
+ primaryDiscoveryActionAvailable: Boolean(payload.primaryDiscoveryAction),
320
+ byStatus,
321
+ filters: {
322
+ statuses: allowlistedList(payload.filters?.statuses, ALLOWED_STATUSES),
323
+ limit: Number.isInteger(payload.filters?.limit) ? payload.filters.limit : null,
324
+ },
325
+ latestSubmittedAt: safeTimestamp(payload.latestSubmittedAt),
326
+ oldestSubmittedAt: safeTimestamp(payload.oldestSubmittedAt),
327
+ leads: payload.leads.map(summarizeLead),
328
+ privacy: {
329
+ contactDetailsIncluded: false,
330
+ draftsIncluded: false,
331
+ approvalTokensIncluded: false,
332
+ rawLeadIdsIncluded: false,
333
+ },
334
+ externalActionAuthorized: false,
335
+ revenueRecognized: false,
336
+ };
337
+ }
338
+
339
+ function exportPrivateQueue(payload, outputPath) {
340
+ const parent = path.dirname(outputPath);
341
+ const realParent = fs.realpathSync(parent);
342
+ const safePath = path.join(realParent, path.basename(outputPath));
343
+ const serialized = `${JSON.stringify({
344
+ exportedAt: new Date().toISOString(),
345
+ sensitivity: 'private_buyer_intake_do_not_commit_or_share',
346
+ externalActionAuthorized: false,
347
+ revenueRecognized: false,
348
+ queue: payload,
349
+ }, null, 2)}\n`;
350
+ fs.writeFileSync(safePath, serialized, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
351
+ fs.chmodSync(safePath, 0o600);
352
+ const stat = fs.statSync(safePath);
353
+ return {
354
+ path: safePath,
355
+ bytes: stat.size,
356
+ mode: (stat.mode & 0o777).toString(8).padStart(3, '0'),
357
+ sha256: crypto.createHash('sha256').update(serialized).digest('hex'),
358
+ };
359
+ }
360
+
361
+ async function fetchWorkflowIntakeQueue(options = {}, config = resolveHostedQueueConfig(), fetchImpl = fetch) {
362
+ const effectiveOptions = {
363
+ statuses: Array.isArray(options.statuses) ? options.statuses : ['new', 'qualified'],
364
+ limit: Number.isInteger(options.limit) ? options.limit : DEFAULT_LIMIT,
365
+ timeoutMs: Number.isInteger(options.timeoutMs) ? options.timeoutMs : DEFAULT_TIMEOUT_MS,
366
+ };
367
+ const hosted = validateHostedConfig(config);
368
+ const url = new URL('/v1/intake/workflow-sprint/queue', hosted.apiBaseUrl);
369
+ url.searchParams.set('status', effectiveOptions.statuses.join(','));
370
+ url.searchParams.set('limit', String(effectiveOptions.limit));
371
+ const response = await fetchWithTimeout(fetchImpl, url, {
372
+ method: 'GET',
373
+ headers: {
374
+ authorization: `Bearer ${hosted.apiKey}`,
375
+ accept: 'application/json',
376
+ },
377
+ }, effectiveOptions.timeoutMs);
378
+
379
+ if (response.status === 404) {
380
+ throw queueError(
381
+ 'release_required',
382
+ 'Production does not expose the authenticated intake queue yet; deploy the reviewed revenue-evidence candidate first.',
383
+ 404
384
+ );
385
+ }
386
+ if (response.status === 401 || response.status === 403) {
387
+ const authProbeUrl = new URL('/v1/billing/summary?window=today', hosted.apiBaseUrl);
388
+ let authProbe = null;
389
+ try {
390
+ authProbe = await fetchWithTimeout(fetchImpl, authProbeUrl, {
391
+ method: 'GET',
392
+ headers: {
393
+ authorization: `Bearer ${hosted.apiKey}`,
394
+ accept: 'application/json',
395
+ },
396
+ }, effectiveOptions.timeoutMs);
397
+ } catch {
398
+ // Keep the original queue authorization result authoritative when the
399
+ // compatibility probe is unavailable.
400
+ }
401
+ if (authProbe?.ok) {
402
+ throw queueError(
403
+ 'release_required',
404
+ 'Production accepts the operator key but does not enable the authenticated intake queue yet; deploy the reviewed revenue-evidence candidate first.',
405
+ response.status
406
+ );
407
+ }
408
+ throw queueError('operator_credentials_rejected', 'Hosted intake queue rejected operator credentials.', response.status);
409
+ }
410
+ if (!response.ok) {
411
+ throw queueError('intake_queue_http_error', `Hosted intake queue returned HTTP ${response.status}.`, response.status);
412
+ }
413
+ assertPrivateResponse(response);
414
+ const payload = validateQueuePayload(await response.json());
415
+ return { payload, summary: summarizeQueue(payload, hosted.apiBaseUrl) };
416
+ }
417
+
418
+ function formatSummary(summary, privateExport = null) {
419
+ const lines = [
420
+ 'ThumbGate hosted intake close queue',
421
+ `Source: ${summary.source}`,
422
+ `Total: ${summary.total} | eligible: ${summary.eligibleTotal} | returned: ${summary.returned}`,
423
+ `Approval-ready: ${summary.approvalReadyTotal} (not authorized to send)`,
424
+ `Discovery-ready: ${summary.discoveryReadyTotal} (not authorized to send)`,
425
+ `Status: ${Object.entries(summary.byStatus).map(([key, value]) => `${key}=${value}`).join(', ') || 'none'}`,
426
+ `Latest: ${summary.latestSubmittedAt || 'none'}`,
427
+ 'Privacy: buyer details, drafts, approval tokens, and raw lead IDs are withheld from terminal output.',
428
+ 'Revenue recognized: no',
429
+ ];
430
+ if (privateExport) {
431
+ lines.push(`Private export: ${privateExport.path}`);
432
+ lines.push(`Private export proof: bytes=${privateExport.bytes} mode=${privateExport.mode} sha256=${privateExport.sha256}`);
433
+ }
434
+ return `${lines.join('\n')}\n`;
435
+ }
436
+
437
+ async function main(argv = process.argv.slice(2)) {
438
+ const options = parseArgs(argv);
439
+ const { payload, summary } = await fetchWorkflowIntakeQueue(options);
440
+ const privateExport = options.exportPrivatePath
441
+ ? exportPrivateQueue(payload, options.exportPrivatePath)
442
+ : null;
443
+ if (options.json) {
444
+ process.stdout.write(`${JSON.stringify({ ...summary, privateExport }, null, 2)}\n`);
445
+ return;
446
+ }
447
+ process.stdout.write(formatSummary(summary, privateExport));
448
+ }
449
+
450
+ module.exports = {
451
+ ALLOWED_STATUSES,
452
+ ALLOWED_CLOSE_BLOCKERS,
453
+ ALLOWED_DISCOVERY_BLOCKERS,
454
+ ALLOWED_OFFER_IDS,
455
+ ALLOWED_UNKNOWNS,
456
+ DEFAULT_LIMIT,
457
+ DEFAULT_TIMEOUT_MS,
458
+ MAX_LIMIT,
459
+ OPERATOR_CONFIG_PATH,
460
+ parseArgs,
461
+ loadOperatorConfig,
462
+ resolveHostedQueueConfig,
463
+ validateHostedConfig,
464
+ assertPrivateResponse,
465
+ validateQueuePayload,
466
+ leadRef,
467
+ allowlistedText,
468
+ allowlistedList,
469
+ safeTimestamp,
470
+ summarizeLead,
471
+ summarizeQueue,
472
+ exportPrivateQueue,
473
+ fetchWorkflowIntakeQueue,
474
+ formatSummary,
475
+ main,
476
+ };
477
+
478
+ if (require.main === module) { // NOSONAR
479
+ main().catch((error) => {
480
+ process.stderr.write(`${error.code || 'intake_queue_error'}: ${error.message}\n`);
481
+ process.exit(1);
482
+ });
483
+ }