rlhf-feedback-loop 0.6.11 → 0.6.13

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 (43) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +116 -74
  3. package/adapters/README.md +3 -3
  4. package/adapters/amp/skills/rlhf-feedback/SKILL.md +2 -0
  5. package/adapters/chatgpt/INSTALL.md +7 -4
  6. package/adapters/chatgpt/openapi.yaml +6 -3
  7. package/adapters/claude/.mcp.json +3 -3
  8. package/adapters/codex/config.toml +3 -3
  9. package/adapters/gemini/function-declarations.json +2 -2
  10. package/adapters/mcp/server-stdio.js +19 -5
  11. package/bin/cli.js +302 -32
  12. package/openapi/openapi.yaml +6 -3
  13. package/package.json +22 -9
  14. package/scripts/a2ui-engine.js +73 -0
  15. package/scripts/adk-consolidator.js +126 -32
  16. package/scripts/billing.js +192 -685
  17. package/scripts/context-engine.js +81 -0
  18. package/scripts/export-kto-pairs.js +310 -0
  19. package/scripts/feedback-ingest-watcher.js +290 -0
  20. package/scripts/feedback-loop.js +154 -9
  21. package/scripts/feedback-quality.js +139 -0
  22. package/scripts/feedback-schema.js +31 -5
  23. package/scripts/feedback-to-memory.js +13 -1
  24. package/scripts/generate-paperbanana-diagrams.sh +1 -1
  25. package/scripts/hook-auto-capture.sh +6 -0
  26. package/scripts/hook-stop-self-score.sh +51 -0
  27. package/scripts/install-mcp.js +168 -0
  28. package/scripts/jsonl-watcher.js +155 -0
  29. package/scripts/local-model-profile.js +207 -0
  30. package/scripts/pr-manager.js +112 -0
  31. package/scripts/prove-adapters.js +137 -15
  32. package/scripts/prove-automation.js +41 -8
  33. package/scripts/prove-lancedb.js +1 -1
  34. package/scripts/prove-local-intelligence.js +244 -0
  35. package/scripts/prove-workflow-contract.js +116 -0
  36. package/scripts/reminder-engine.js +132 -0
  37. package/scripts/risk-scorer.js +458 -0
  38. package/scripts/rlaif-self-audit.js +7 -1
  39. package/scripts/status-dashboard.js +155 -0
  40. package/scripts/test-coverage.js +1 -1
  41. package/scripts/validate-workflow-contract.js +287 -0
  42. package/scripts/vector-store.js +115 -17
  43. package/src/api/server.js +372 -25
@@ -1,16 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * billing.js — Stripe billing integration using raw fetch (no stripe npm package).
4
- *
5
- * Functions:
6
- * createCheckoutSession() — Creates Stripe Checkout session for Cloud Pro
7
- * provisionApiKey(customerId) — Generates unique API key, stores in api-keys.json
8
- * validateApiKey(key) — Checks key exists and is active
9
- * recordUsage(key) — Increments usage counter for the key
10
- * handleWebhook(event) — Processes checkout.session.completed + subscription.deleted
11
- *
12
- * Local mode: When STRIPE_SECRET_KEY is not set, all Stripe calls are no-ops.
13
- * Keys stored in: .claude/memory/feedback/api-keys.json (gitignored)
3
+ * billing.js — Stripe billing integration using official Stripe SDK.
14
4
  */
15
5
 
16
6
  'use strict';
@@ -18,27 +8,40 @@
18
8
  const fs = require('fs');
19
9
  const path = require('path');
20
10
  const crypto = require('crypto');
11
+ const Stripe = require('stripe');
21
12
 
22
13
  // ---------------------------------------------------------------------------
23
14
  // Config
24
15
  // ---------------------------------------------------------------------------
25
16
 
26
- const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY || '';
27
- const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET || '';
28
- const GITHUB_MARKETPLACE_WEBHOOK_SECRET = process.env.GITHUB_MARKETPLACE_WEBHOOK_SECRET || '';
29
- const STRIPE_PRICE_ID = process.env.STRIPE_PRICE_ID || '';
30
-
31
- const API_KEYS_PATH = process.env._TEST_API_KEYS_PATH || path.resolve(
32
- __dirname,
33
- '../.claude/memory/feedback/api-keys.json'
34
- );
17
+ const CONFIG = {
18
+ STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY || '',
19
+ STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET || '',
20
+ GITHUB_MARKETPLACE_WEBHOOK_SECRET: process.env.GITHUB_MARKETPLACE_WEBHOOK_SECRET || '',
21
+ STRIPE_PRICE_ID: process.env.STRIPE_PRICE_ID || 'price_1RNdUBGGBpd520QYG1A9SWF4',
22
+ get API_KEYS_PATH() {
23
+ return process.env._TEST_API_KEYS_PATH || path.resolve(__dirname, '../.claude/memory/feedback/api-keys.json');
24
+ },
25
+ get FUNNEL_LEDGER_PATH() {
26
+ return process.env._TEST_FUNNEL_LEDGER_PATH || process.env.RLHF_FUNNEL_LEDGER_PATH || path.resolve(__dirname, '../.claude/memory/feedback/funnel-events.jsonl');
27
+ },
28
+ get LOCAL_CHECKOUT_SESSIONS_PATH() {
29
+ return process.env._TEST_LOCAL_CHECKOUT_SESSIONS_PATH || path.resolve(__dirname, '../.claude/memory/feedback/local-checkout-sessions.json');
30
+ }
31
+ };
35
32
 
36
- const FUNNEL_LEDGER_PATH = process.env._TEST_FUNNEL_LEDGER_PATH || process.env.RLHF_FUNNEL_LEDGER_PATH || path.resolve(
37
- __dirname,
38
- '../.claude/memory/feedback/funnel-events.jsonl'
39
- );
33
+ let _stripeClient = null;
34
+ function getStripeClient() {
35
+ if (!_stripeClient) {
36
+ if (!CONFIG.STRIPE_SECRET_KEY) {
37
+ throw new Error('STRIPE_SECRET_KEY is missing. Stripe client cannot be initialized.');
38
+ }
39
+ _stripeClient = new Stripe(CONFIG.STRIPE_SECRET_KEY);
40
+ }
41
+ return _stripeClient;
42
+ }
40
43
 
41
- const LOCAL_MODE = !STRIPE_SECRET_KEY;
44
+ const LOCAL_MODE = () => !CONFIG.STRIPE_SECRET_KEY;
42
45
 
43
46
  // ---------------------------------------------------------------------------
44
47
  // Internal helpers
@@ -52,774 +55,278 @@ function ensureParentDir(filePath) {
52
55
  }
53
56
 
54
57
  function sanitizeMetadata(metadata) {
55
- if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
56
- return {};
57
- }
58
-
59
- const normalized = {};
60
- for (const [k, v] of Object.entries(metadata)) {
61
- if (v == null) continue;
62
- normalized[String(k)] = String(v);
63
- }
64
- return normalized;
65
- }
66
-
67
- function readInstallIdFromConfig({ cwd = process.cwd(), configPath } = {}) {
68
- const resolvedPath = configPath || path.resolve(cwd, '.rlhf/config.json');
69
- try {
70
- if (!fs.existsSync(resolvedPath)) {
71
- return null;
72
- }
73
- const parsed = JSON.parse(fs.readFileSync(resolvedPath, 'utf-8'));
74
- if (!parsed || typeof parsed.installId !== 'string') {
75
- return null;
76
- }
77
- const installId = parsed.installId.trim();
78
- return installId || null;
79
- } catch {
80
- return null;
81
- }
58
+ if (!metadata || typeof metadata !== 'object') return {};
59
+ return { ...metadata };
82
60
  }
83
61
 
84
62
  function appendFunnelEvent({ stage, event, installId = null, evidence, metadata = {} } = {}) {
85
- if (!stage || !event) {
86
- return { written: false, reason: 'missing_stage_or_event' };
87
- }
88
-
89
- const payload = {
90
- timestamp: new Date().toISOString(),
91
- stage,
92
- event,
93
- evidence: evidence || event,
94
- installId: installId || null,
95
- metadata: sanitizeMetadata(metadata),
96
- };
97
-
63
+ if (!stage || !event) return { written: false, reason: 'missing_stage_or_event' };
64
+ const payload = { timestamp: new Date().toISOString(), stage, event, evidence: evidence || event, installId: installId || null, metadata: sanitizeMetadata(metadata) };
98
65
  try {
99
- ensureParentDir(FUNNEL_LEDGER_PATH);
100
- fs.appendFileSync(FUNNEL_LEDGER_PATH, `${JSON.stringify(payload)}\n`, 'utf-8');
66
+ const target = CONFIG.FUNNEL_LEDGER_PATH;
67
+ ensureParentDir(target);
68
+ fs.appendFileSync(target, `${JSON.stringify(payload)}\n`, 'utf-8');
101
69
  return { written: true, payload };
102
70
  } catch (err) {
103
- return {
104
- written: false,
105
- reason: 'write_failed',
106
- error: err && err.message ? err.message : 'unknown_error',
107
- };
71
+ return { written: false, reason: 'write_failed', error: err.message };
108
72
  }
109
73
  }
110
74
 
111
75
  function loadFunnelLedger() {
112
76
  try {
113
- if (!fs.existsSync(FUNNEL_LEDGER_PATH)) {
114
- return [];
115
- }
116
-
117
- const lines = fs
118
- .readFileSync(FUNNEL_LEDGER_PATH, 'utf-8')
119
- .split('\n')
120
- .map((line) => line.trim())
121
- .filter(Boolean);
122
-
123
- const events = [];
124
- for (const line of lines) {
125
- try {
126
- const parsed = JSON.parse(line);
127
- if (parsed && typeof parsed === 'object') {
128
- events.push(parsed);
129
- }
130
- } catch {
131
- // Ignore malformed lines to preserve append-only behavior.
132
- }
133
- }
77
+ const target = CONFIG.FUNNEL_LEDGER_PATH;
78
+ if (!fs.existsSync(target)) return [];
79
+ return fs.readFileSync(target, 'utf-8').split('\n').map(l => l.trim()).filter(Boolean).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
80
+ } catch { return []; }
81
+ }
134
82
 
135
- return events;
136
- } catch {
137
- return [];
138
- }
83
+ function loadLocalCheckoutSessions() {
84
+ try {
85
+ const target = CONFIG.LOCAL_CHECKOUT_SESSIONS_PATH;
86
+ if (!fs.existsSync(target)) return { sessions: {} };
87
+ const parsed = JSON.parse(fs.readFileSync(target, 'utf-8'));
88
+ return (parsed && typeof parsed.sessions === 'object') ? parsed : { sessions: {} };
89
+ } catch { return { sessions: {} }; }
139
90
  }
140
91
 
141
- function safeRate(numerator, denominator) {
142
- if (!denominator) {
143
- return 0;
144
- }
145
- return Number((numerator / denominator).toFixed(4));
92
+ function saveLocalCheckoutSessions(store) {
93
+ const target = CONFIG.LOCAL_CHECKOUT_SESSIONS_PATH;
94
+ ensureParentDir(target);
95
+ fs.writeFileSync(target, JSON.stringify(store, null, 2), 'utf-8');
146
96
  }
147
97
 
148
98
  function getFunnelAnalytics() {
149
99
  const events = loadFunnelLedger();
150
- const stageCounts = {
151
- acquisition: 0,
152
- activation: 0,
153
- paid: 0,
154
- };
100
+ const stageCounts = { acquisition: 0, activation: 0, paid: 0 };
155
101
  const eventCounts = {};
156
-
157
102
  for (const entry of events) {
158
- if (entry && typeof entry.stage === 'string') {
159
- if (Object.prototype.hasOwnProperty.call(stageCounts, entry.stage)) {
160
- stageCounts[entry.stage] += 1;
161
- }
162
-
163
- const eventName = typeof entry.event === 'string' ? entry.event : 'unknown';
164
- const key = `${entry.stage}:${eventName}`;
103
+ if (entry && stageCounts.hasOwnProperty(entry.stage)) {
104
+ stageCounts[entry.stage]++;
105
+ const key = `${entry.stage}:${entry.event || 'unknown'}`;
165
106
  eventCounts[key] = (eventCounts[key] || 0) + 1;
166
107
  }
167
108
  }
168
-
169
- return {
170
- totalEvents: events.length,
171
- stageCounts,
172
- eventCounts,
173
- conversionRates: {
174
- acquisitionToActivation: safeRate(stageCounts.activation, stageCounts.acquisition),
175
- activationToPaid: safeRate(stageCounts.paid, stageCounts.activation),
176
- acquisitionToPaid: safeRate(stageCounts.paid, stageCounts.acquisition),
177
- },
178
- };
109
+ const safeRate = (num, den) => den ? Number((num / den).toFixed(4)) : 0;
110
+ return { totalEvents: events.length, stageCounts, eventCounts, conversionRates: { acquisitionToActivation: safeRate(stageCounts.activation, stageCounts.acquisition), activationToPaid: safeRate(stageCounts.paid, stageCounts.activation), acquisitionToPaid: safeRate(stageCounts.paid, stageCounts.acquisition) } };
179
111
  }
180
112
 
181
- // ---------------------------------------------------------------------------
182
- // Key store helpers
183
- // ---------------------------------------------------------------------------
184
-
185
- /**
186
- * Load the API key store from disk.
187
- * Returns { keys: { [key]: { customerId, active, usageCount, createdAt } } }
188
- */
189
113
  function loadKeyStore() {
190
114
  try {
191
- if (!fs.existsSync(API_KEYS_PATH)) {
192
- return { keys: {} };
193
- }
194
- const raw = fs.readFileSync(API_KEYS_PATH, 'utf-8');
195
- const parsed = JSON.parse(raw);
196
- if (!parsed || typeof parsed.keys !== 'object') {
197
- return { keys: {} };
198
- }
199
- return parsed;
200
- } catch {
201
- return { keys: {} };
202
- }
115
+ const target = CONFIG.API_KEYS_PATH;
116
+ if (!fs.existsSync(target)) return { keys: {} };
117
+ const parsed = JSON.parse(fs.readFileSync(target, 'utf-8'));
118
+ return (parsed && typeof parsed.keys === 'object') ? parsed : { keys: {} };
119
+ } catch { return { keys: {} }; }
203
120
  }
204
121
 
205
- /**
206
- * Persist the key store to disk. Creates parent directory if needed.
207
- */
208
122
  function saveKeyStore(store) {
209
- ensureParentDir(API_KEYS_PATH);
210
- fs.writeFileSync(API_KEYS_PATH, JSON.stringify(store, null, 2), 'utf-8');
123
+ const target = CONFIG.API_KEYS_PATH;
124
+ ensureParentDir(target);
125
+ fs.writeFileSync(target, JSON.stringify(store, null, 2), 'utf-8');
211
126
  }
212
127
 
213
128
  // ---------------------------------------------------------------------------
214
- // Stripe REST API helper
129
+ // Core Exports
215
130
  // ---------------------------------------------------------------------------
216
131
 
217
- /**
218
- * Call the Stripe REST API using built-in fetch (Node 18+) or https module.
219
- * Returns parsed JSON response.
220
- * Throws on non-2xx responses with the Stripe error message.
221
- */
222
- async function stripeRequest(method, endpoint, params = {}) {
223
- if (LOCAL_MODE) {
224
- throw new Error('STRIPE_SECRET_KEY not configured — local mode active');
225
- }
132
+ async function createCheckoutSession({ successUrl, cancelUrl, customerEmail, installId, metadata = {} } = {}) {
133
+ const checkoutMetadata = { ...metadata, installId: installId || 'unknown' };
226
134
 
227
- const url = `https://api.stripe.com/v1${endpoint}`;
228
-
229
- // Stripe uses x-www-form-urlencoded for POST/DELETE bodies
230
- const body = method !== 'GET' && Object.keys(params).length > 0
231
- ? new URLSearchParams(flattenParams(params)).toString()
232
- : undefined;
233
-
234
- // For GET requests, add params as query string
235
- const fullUrl = method === 'GET' && Object.keys(params).length > 0
236
- ? `${url}?${new URLSearchParams(flattenParams(params)).toString()}`
237
- : url;
238
-
239
- const headers = {
240
- 'Authorization': `Bearer ${STRIPE_SECRET_KEY}`,
241
- 'Content-Type': 'application/x-www-form-urlencoded',
242
- 'Stripe-Version': '2023-10-16',
243
- };
244
-
245
- // Use fetch if available (Node 18+), otherwise fall back to https module
246
- if (typeof fetch !== 'undefined') {
247
- const response = await fetch(fullUrl, {
248
- method,
249
- headers,
250
- body,
251
- });
252
- const json = await response.json();
253
- if (!response.ok) {
254
- const msg = (json.error && json.error.message) || `Stripe error ${response.status}`;
255
- const err = new Error(msg);
256
- err.stripeError = json.error;
257
- err.statusCode = response.status;
258
- throw err;
259
- }
260
- return json;
135
+ if (LOCAL_MODE()) {
136
+ const localSessionId = `test_session_${crypto.randomBytes(8).toString('hex')}`;
137
+ const store = loadLocalCheckoutSessions();
138
+ store.sessions[localSessionId] = { id: localSessionId, customer: `local_cus_${crypto.randomBytes(4).toString('hex')}`, metadata: checkoutMetadata, payment_status: 'paid', status: 'complete' };
139
+ saveLocalCheckoutSessions(store);
140
+
141
+ appendFunnelEvent({ stage: 'acquisition', event: 'checkout_session_created', installId, evidence: 'local_mode_manual', metadata: checkoutMetadata });
142
+ return { sessionId: localSessionId, url: null, localMode: true, metadata: checkoutMetadata };
261
143
  }
262
144
 
263
- // Node <18 fallback via https module
264
- return new Promise((resolve, reject) => {
265
- const https = require('https');
266
- const urlObj = new URL(fullUrl);
267
- const options = {
268
- hostname: urlObj.hostname,
269
- path: urlObj.pathname + urlObj.search,
270
- method,
271
- headers: { ...headers },
272
- };
273
- if (body) {
274
- options.headers['Content-Length'] = Buffer.byteLength(body);
275
- }
276
- const req = https.request(options, (res) => {
277
- const chunks = [];
278
- res.on('data', (c) => chunks.push(c));
279
- res.on('end', () => {
280
- try {
281
- const json = JSON.parse(Buffer.concat(chunks).toString('utf-8'));
282
- if (res.statusCode >= 400) {
283
- const msg = (json.error && json.error.message) || `Stripe error ${res.statusCode}`;
284
- const err = new Error(msg);
285
- err.stripeError = json.error;
286
- err.statusCode = res.statusCode;
287
- reject(err);
288
- } else {
289
- resolve(json);
290
- }
291
- } catch (e) {
292
- reject(e);
293
- }
294
- });
295
- });
296
- req.on('error', reject);
297
- if (body) req.write(body);
298
- req.end();
145
+ const stripe = getStripeClient();
146
+ const session = await stripe.checkout.sessions.create({
147
+ success_url: successUrl,
148
+ cancel_url: cancelUrl,
149
+ customer_email: customerEmail,
150
+ mode: 'subscription',
151
+ line_items: [{ price: CONFIG.STRIPE_PRICE_ID, quantity: 1 }],
152
+ metadata: checkoutMetadata,
299
153
  });
154
+
155
+ appendFunnelEvent({ stage: 'acquisition', event: 'checkout_session_created', installId, evidence: session.id, metadata: checkoutMetadata });
156
+ return { sessionId: session.id, url: session.url, localMode: false, metadata: checkoutMetadata };
300
157
  }
301
158
 
302
- /**
303
- * Flatten nested objects into Stripe's dot-notation format.
304
- * e.g. { line_items: [{ price: 'p_123', quantity: 1 }] }
305
- * => { 'line_items[0][price]': 'p_123', 'line_items[0][quantity]': '1' }
306
- */
307
- function flattenParams(obj, prefix = '') {
308
- const result = {};
309
- for (const [k, v] of Object.entries(obj)) {
310
- const key = prefix ? `${prefix}[${k}]` : k;
311
- if (Array.isArray(v)) {
312
- v.forEach((item, i) => {
313
- if (item !== null && typeof item === 'object') {
314
- Object.assign(result, flattenParams(item, `${key}[${i}]`));
315
- } else {
316
- result[`${key}[${i}]`] = String(item);
317
- }
318
- });
319
- } else if (v !== null && typeof v === 'object') {
320
- Object.assign(result, flattenParams(v, key));
321
- } else if (v !== undefined && v !== null) {
322
- result[key] = String(v);
323
- }
159
+ async function getCheckoutSessionStatus(sessionId) {
160
+ if (LOCAL_MODE()) {
161
+ const store = loadLocalCheckoutSessions();
162
+ const session = store.sessions[sessionId];
163
+ if (!session) return { found: false };
164
+ const provisioned = provisionApiKey(session.customer, { installId: session.metadata?.installId, source: 'local_checkout_lookup' });
165
+ return { found: true, localMode: true, sessionId, paid: true, paymentStatus: 'paid', status: 'complete', customerId: session.customer, installId: session.metadata?.installId, apiKey: provisioned.key };
324
166
  }
325
- return result;
326
- }
327
167
 
328
- // ---------------------------------------------------------------------------
329
- // Public API
330
- // ---------------------------------------------------------------------------
168
+ try {
169
+ const stripe = getStripeClient();
170
+ const session = await stripe.checkout.sessions.retrieve(sessionId);
171
+ const isPaid = session.payment_status === 'paid' || session.payment_status === 'no_payment_required';
331
172
 
332
- /**
333
- * Create a Stripe Checkout Session for Cloud Pro.
334
- *
335
- * @param {object} opts
336
- * @param {string} opts.successUrl - Redirect URL on payment success
337
- * @param {string} opts.cancelUrl - Redirect URL on cancel
338
- * @param {string} [opts.customerEmail] - Pre-fill customer email
339
- * @param {string} [opts.installId] - Correlation install id
340
- * @param {object} [opts.metadata] - Additional checkout metadata
341
- * @returns {Promise<{sessionId: string, url: string}>} in live mode
342
- * or {sessionId: 'local_<uuid>', url: null} in local mode
343
- */
344
- async function createCheckoutSession({ successUrl, cancelUrl, customerEmail, installId, metadata } = {}) {
345
- const resolvedInstallId = installId || readInstallIdFromConfig() || null;
346
- const checkoutMetadata = sanitizeMetadata({
347
- ...(metadata || {}),
348
- ...(resolvedInstallId ? { installId: resolvedInstallId } : {}),
349
- });
173
+ if (!isPaid) return { found: true, localMode: false, sessionId, paid: false, paymentStatus: session.payment_status, status: session.status };
350
174
 
351
- if (LOCAL_MODE) {
352
- const localSessionId = `local_${crypto.randomUUID()}`;
353
- appendFunnelEvent({
354
- stage: 'acquisition',
355
- event: 'checkout_session_created',
356
- evidence: 'checkout_session_created',
357
- installId: resolvedInstallId,
358
- metadata: {
359
- provider: 'stripe',
360
- mode: 'local',
361
- sessionId: localSessionId,
362
- customerEmail: customerEmail || '',
363
- },
364
- });
175
+ const installId = session.metadata?.installId || null;
176
+ const provisioned = provisionApiKey(session.customer, { installId, source: 'stripe_checkout_session_lookup' });
365
177
 
366
178
  return {
367
- sessionId: localSessionId,
368
- url: null,
369
- localMode: true,
370
- metadata: checkoutMetadata,
179
+ found: true,
180
+ localMode: false,
181
+ sessionId,
182
+ paid: true,
183
+ paymentStatus: session.payment_status,
184
+ customerId: session.customer,
185
+ customerEmail: session.customer_details?.email || '',
186
+ installId,
187
+ apiKey: provisioned.key,
371
188
  };
189
+ } catch {
190
+ return { found: false };
372
191
  }
373
-
374
- if (!STRIPE_PRICE_ID) {
375
- throw new Error('STRIPE_PRICE_ID not configured');
376
- }
377
-
378
- const params = {
379
- mode: 'subscription',
380
- line_items: [
381
- { price: STRIPE_PRICE_ID, quantity: 1 },
382
- ],
383
- success_url: successUrl || 'https://example.com/success?session_id={CHECKOUT_SESSION_ID}',
384
- cancel_url: cancelUrl || 'https://example.com/cancel',
385
- };
386
-
387
- if (customerEmail) {
388
- params.customer_email = customerEmail;
389
- }
390
-
391
- if (Object.keys(checkoutMetadata).length > 0) {
392
- params.metadata = checkoutMetadata;
393
- }
394
-
395
- const session = await stripeRequest('POST', '/checkout/sessions', params);
396
-
397
- appendFunnelEvent({
398
- stage: 'acquisition',
399
- event: 'checkout_session_created',
400
- evidence: 'checkout_session_created',
401
- installId: resolvedInstallId,
402
- metadata: {
403
- provider: 'stripe',
404
- mode: 'live',
405
- sessionId: session.id,
406
- customerEmail: customerEmail || '',
407
- },
408
- });
409
-
410
- return {
411
- sessionId: session.id,
412
- url: session.url,
413
- metadata: checkoutMetadata,
414
- };
415
192
  }
416
193
 
417
- /**
418
- * Provision a unique API key for a customer.
419
- * Stores { customerId, active: true, usageCount: 0, createdAt } in api-keys.json.
420
- *
421
- * @param {string} customerId - Stripe customer ID (e.g. cus_xxx)
422
- * @param {object} [opts]
423
- * @param {string} [opts.installId]
424
- * @param {string} [opts.source]
425
- * @returns {{ key: string, customerId: string, createdAt: string }}
426
- */
427
194
  function provisionApiKey(customerId, opts = {}) {
428
- if (!customerId || typeof customerId !== 'string') {
429
- throw new Error('customerId is required');
430
- }
431
-
432
- const installId = opts.installId || null;
433
- const source = opts.source || 'provision';
434
-
195
+ if (!customerId || typeof customerId !== 'string') throw new Error('customerId is required');
435
196
  const store = loadKeyStore();
436
-
437
- // Check if this customer already has an active key — reuse it
438
- const existing = Object.entries(store.keys).find(
439
- ([, meta]) => meta.customerId === customerId && meta.active
440
- );
197
+ const existing = Object.entries(store.keys).find(([, m]) => m.customerId === customerId && m.active);
441
198
 
442
199
  if (existing) {
443
- if (installId && !existing[1].installId) {
444
- existing[1].installId = installId;
445
- saveKeyStore(store);
446
- }
447
-
448
- return {
449
- key: existing[0],
450
- customerId,
451
- createdAt: existing[1].createdAt,
452
- installId: existing[1].installId || null,
453
- reused: true,
454
- };
200
+ if (opts.installId && !existing[1].installId) { existing[1].installId = opts.installId; saveKeyStore(store); }
201
+ return { key: existing[0], customerId, createdAt: existing[1].createdAt, installId: existing[1].installId || null, reused: true };
455
202
  }
456
203
 
457
- // Generate cryptographically random key: rlhf_<32 hex chars>
458
204
  const key = `rlhf_${crypto.randomBytes(16).toString('hex')}`;
459
205
  const createdAt = new Date().toISOString();
206
+ store.keys[key] = { customerId, active: true, usageCount: 0, createdAt, installId: opts.installId || null, source: opts.source || 'provision' };
207
+ saveKeyStore(store);
208
+ return { key, customerId, createdAt, installId: opts.installId || null };
209
+ }
460
210
 
461
- store.keys[key] = {
462
- customerId,
463
- active: true,
464
- usageCount: 0,
465
- createdAt,
466
- installId,
467
- source,
468
- };
211
+ function rotateApiKey(oldKey) {
212
+ if (!oldKey) return { rotated: false, reason: 'missing_old_key' };
213
+ const store = loadKeyStore();
214
+ const meta = store.keys[oldKey];
215
+ if (!meta || !meta.active) return { rotated: false, reason: 'key_not_active' };
469
216
 
217
+ meta.active = false;
218
+ meta.disabledAt = new Date().toISOString();
219
+ const newKey = `rlhf_${crypto.randomBytes(16).toString('hex')}`;
220
+ store.keys[newKey] = { customerId: meta.customerId, active: true, usageCount: 0, createdAt: new Date().toISOString(), installId: meta.installId, source: 'rotation', replacedKey: oldKey };
470
221
  saveKeyStore(store);
471
-
472
- return { key, customerId, createdAt, installId };
222
+ return { rotated: true, key: newKey, oldKey };
473
223
  }
474
224
 
475
- /**
476
- * Validate an API key.
477
- *
478
- * @param {string} key - API key to validate
479
- * @returns {{ valid: boolean, customerId?: string, usageCount?: number }}
480
- */
481
225
  function validateApiKey(key) {
482
- if (!key || typeof key !== 'string') {
483
- return { valid: false };
484
- }
485
-
226
+ if (!key) return { valid: false };
486
227
  const store = loadKeyStore();
487
228
  const meta = store.keys[key];
488
-
489
- if (!meta) {
490
- return { valid: false };
491
- }
492
-
493
- if (!meta.active) {
494
- return { valid: false, reason: 'key_disabled' };
495
- }
496
-
497
- return {
498
- valid: true,
499
- customerId: meta.customerId,
500
- usageCount: meta.usageCount,
501
- installId: meta.installId || null,
502
- };
229
+ return (meta && meta.active) ? { valid: true, metadata: meta } : { valid: false };
503
230
  }
504
231
 
505
- /**
506
- * Record one usage event for an API key.
507
- * Increments usageCount in the key store.
508
- * Emits an activation event at first usage transition 0 -> 1.
509
- *
510
- * @param {string} key - API key to record usage for
511
- * @returns {{ recorded: boolean, usageCount?: number }}
512
- */
513
232
  function recordUsage(key) {
514
- if (!key || typeof key !== 'string') {
515
- return { recorded: false };
516
- }
517
-
518
233
  const store = loadKeyStore();
519
234
  const meta = store.keys[key];
520
-
521
- if (!meta || !meta.active) {
522
- return { recorded: false };
523
- }
524
-
525
- const previousUsage = Number(meta.usageCount || 0);
526
- meta.usageCount = previousUsage + 1;
527
- saveKeyStore(store);
528
-
529
- if (previousUsage === 0) {
530
- appendFunnelEvent({
531
- stage: 'activation',
532
- event: 'api_key_first_usage',
533
- evidence: 'api_key_first_usage',
534
- installId: meta.installId || null,
535
- metadata: {
536
- customerId: meta.customerId,
537
- usageCount: '1',
538
- },
539
- });
235
+ if (meta && meta.active) {
236
+ const oldVal = meta.usageCount || 0;
237
+ meta.usageCount = oldVal + 1;
238
+ if (oldVal === 0) appendFunnelEvent({ stage: 'activation', event: 'api_key_first_usage', installId: meta.installId, evidence: key, metadata: { customerId: meta.customerId } });
239
+ saveKeyStore(store);
240
+ return { recorded: true, usageCount: meta.usageCount };
540
241
  }
541
-
542
- return { recorded: true, usageCount: meta.usageCount };
242
+ return { recorded: false };
543
243
  }
544
244
 
545
245
  /**
546
- * Disable all API keys for a customer (called on subscription cancellation).
547
- *
548
- * @param {string} customerId - Stripe customer ID
549
- * @returns {{ disabledCount: number }}
246
+ * Report usage to Stripe for metered billing.
550
247
  */
551
- function disableCustomerKeys(customerId) {
552
- if (!customerId || typeof customerId !== 'string') {
553
- return { disabledCount: 0 };
248
+ async function reportUsageToStripe(subscriptionItemId, quantity = 1) {
249
+ if (LOCAL_MODE()) return { reported: false, reason: 'local_mode' };
250
+ try {
251
+ const stripe = getStripeClient();
252
+ const record = await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {
253
+ quantity,
254
+ timestamp: 'now',
255
+ action: 'increment'
256
+ });
257
+ return { reported: true, record };
258
+ } catch (err) {
259
+ return { reported: false, error: err.message };
554
260
  }
261
+ }
555
262
 
263
+ function disableCustomerKeys(customerId) {
556
264
  const store = loadKeyStore();
557
265
  let disabledCount = 0;
558
-
559
- for (const meta of Object.values(store.keys)) {
560
- if (meta.customerId === customerId && meta.active) {
561
- meta.active = false;
562
- disabledCount++;
563
- }
564
- }
565
-
566
- if (disabledCount > 0) {
567
- saveKeyStore(store);
266
+ for (const [key, meta] of Object.entries(store.keys)) {
267
+ if (meta.customerId === customerId && meta.active) { meta.active = false; meta.disabledAt = new Date().toISOString(); disabledCount++; }
568
268
  }
569
-
269
+ if (disabledCount > 0) saveKeyStore(store);
570
270
  return { disabledCount };
571
271
  }
572
272
 
573
- /**
574
- * Handle a Stripe webhook event.
575
- *
576
- * Supported events:
577
- * checkout.session.completed provision API key for the new customer
578
- * customer.subscription.deleted disable all keys for that customer
579
- *
580
- * @param {object} event - Parsed Stripe event object
581
- * @returns {{ handled: boolean, action?: string, result?: object }}
582
- */
583
- function handleWebhook(event) {
584
- if (!event || !event.type) {
585
- return { handled: false, reason: 'missing_event_type' };
273
+ async function handleWebhook(rawBody, signature) {
274
+ if (LOCAL_MODE()) return { handled: false, reason: 'local_mode' };
275
+ let event;
276
+ try {
277
+ const stripe = getStripeClient();
278
+ event = stripe.webhooks.constructEvent(rawBody, signature, CONFIG.STRIPE_WEBHOOK_SECRET);
279
+ } catch (err) {
280
+ return { handled: false, reason: 'invalid_signature', error: err.message };
586
281
  }
587
282
 
588
283
  switch (event.type) {
589
284
  case 'checkout.session.completed': {
590
- const session = event.data && event.data.object;
591
- if (!session) {
592
- return { handled: false, reason: 'missing_session_data' };
593
- }
285
+ const session = event.data.object;
594
286
  const customerId = session.customer;
595
- if (!customerId) {
596
- return { handled: false, reason: 'missing_customer_id' };
597
- }
598
-
599
- const installId = session.metadata && typeof session.metadata.installId === 'string'
600
- ? session.metadata.installId
601
- : null;
602
-
603
- const result = provisionApiKey(customerId, {
604
- installId,
605
- source: 'stripe_checkout_session_completed',
606
- });
607
-
608
- appendFunnelEvent({
609
- stage: 'paid',
610
- event: 'stripe_checkout_session_completed',
611
- evidence: 'stripe_checkout_session_completed',
612
- installId,
613
- metadata: {
614
- provider: 'stripe',
615
- customerId,
616
- checkoutSessionId: session.id || '',
617
- },
618
- });
619
-
620
- return {
621
- handled: true,
622
- action: 'provisioned_api_key',
623
- result,
624
- };
287
+ const installId = session.metadata?.installId;
288
+ const result = provisionApiKey(customerId, { installId, source: 'stripe_webhook_checkout_completed' });
289
+ appendFunnelEvent({ stage: 'paid', event: 'stripe_checkout_completed', installId, evidence: session.id, metadata: { customerId, subscriptionId: session.subscription } });
290
+ return { handled: true, action: 'provisioned_api_key', result };
625
291
  }
626
-
627
292
  case 'customer.subscription.deleted': {
628
- const subscription = event.data && event.data.object;
629
- if (!subscription) {
630
- return { handled: false, reason: 'missing_subscription_data' };
631
- }
632
- const customerId = subscription.customer;
633
- if (!customerId) {
634
- return { handled: false, reason: 'missing_customer_id' };
635
- }
636
- const result = disableCustomerKeys(customerId);
637
- return {
638
- handled: true,
639
- action: 'disabled_customer_keys',
640
- result,
641
- };
293
+ const sub = event.data.object;
294
+ return { handled: true, action: 'disabled_customer_keys', result: disableCustomerKeys(sub.customer) };
642
295
  }
643
-
644
- default:
645
- return { handled: false, reason: `unhandled_event_type:${event.type}` };
296
+ default: return { handled: false, reason: `unhandled_event_type:${event.type}` };
646
297
  }
647
298
  }
648
299
 
649
- /**
650
- * Verify a Stripe webhook signature.
651
- * Returns true if valid, false if STRIPE_WEBHOOK_SECRET is not set (local mode).
652
- *
653
- * @param {string|Buffer} rawBody - Raw request body bytes
654
- * @param {string} signature - Value of stripe-signature header
655
- * @returns {boolean}
656
- */
657
- function verifyWebhookSignature(rawBody, signature) {
658
- if (!STRIPE_WEBHOOK_SECRET) {
659
- // Local mode — skip signature verification
660
- return true;
661
- }
662
-
663
- if (!signature || !rawBody) {
664
- return false;
665
- }
666
-
667
- // Stripe signature format: t=<timestamp>,v1=<hmac>,...
668
- const parts = {};
669
- for (const part of signature.split(',')) {
670
- const [k, v] = part.split('=');
671
- if (k && v) {
672
- parts[k] = v;
673
- }
674
- }
675
-
676
- if (!parts.t || !parts.v1) {
677
- return false;
678
- }
679
-
680
- // Timestamp tolerance: +/- 5 minutes (300 seconds)
681
- const timestamp = parseInt(parts.t, 10);
682
- const now = Math.floor(Date.now() / 1000);
683
- if (isNaN(timestamp) || Math.abs(now - timestamp) > 300) {
684
- return false;
685
- }
686
-
687
- const payload = `${parts.t}.${typeof rawBody === 'string' ? rawBody : rawBody.toString('utf-8')}`;
688
- const expected = crypto
689
- .createHmac('sha256', STRIPE_WEBHOOK_SECRET)
690
- .update(payload, 'utf-8')
691
- .digest('hex');
692
-
693
- try {
694
- return crypto.timingSafeEqual(
695
- Buffer.from(expected, 'hex'),
696
- Buffer.from(parts.v1, 'hex')
697
- );
698
- } catch {
699
- return false;
700
- }
701
- }
702
-
703
- /**
704
- * Verify a GitHub Marketplace webhook signature.
705
- * Returns true if valid, false if GITHUB_MARKETPLACE_WEBHOOK_SECRET is not set (local mode).
706
- *
707
- * @param {string|Buffer} rawBody - Raw request body bytes
708
- * @param {string} signature - Value of x-hub-signature-256 header
709
- * @returns {boolean}
710
- */
711
300
  function verifyGithubWebhookSignature(rawBody, signature) {
712
- if (!GITHUB_MARKETPLACE_WEBHOOK_SECRET) {
713
- // Local mode skip signature verification
714
- return true;
715
- }
716
-
717
- if (!signature || !rawBody) {
718
- return false;
719
- }
720
-
721
- const hmac = crypto.createHmac('sha256', GITHUB_MARKETPLACE_WEBHOOK_SECRET);
722
- const digest = Buffer.from(`sha256=${hmac.update(rawBody).digest('hex')}`, 'utf8');
301
+ if (!CONFIG.GITHUB_MARKETPLACE_WEBHOOK_SECRET) return true;
302
+ if (!signature || !rawBody) return false;
303
+ const expected = crypto.createHmac('sha256', CONFIG.GITHUB_MARKETPLACE_WEBHOOK_SECRET).update(rawBody).digest('hex');
304
+ const digest = Buffer.from(`sha256=${expected}`, 'utf8');
723
305
  const checksum = Buffer.from(signature, 'utf8');
724
-
725
306
  return checksum.length === digest.length && crypto.timingSafeEqual(digest, checksum);
726
307
  }
727
308
 
728
- /**
729
- * Handle a GitHub Marketplace webhook event.
730
- *
731
- * Supported actions:
732
- * purchased — provision API key for the new customer
733
- * changed — plan update (upgrade/downgrade)
734
- * cancelled — disable all keys for that customer
735
- *
736
- * @param {object} event - Parsed GitHub Marketplace event object
737
- * @returns {{ handled: boolean, action?: string, result?: object }}
738
- */
739
309
  function handleGithubWebhook(event) {
740
- if (!event || typeof event !== 'object') {
741
- return { handled: false, reason: 'missing_payload_data' };
742
- }
743
-
744
- const { action, marketplace_purchase: marketplacePurchase } = event;
745
- if (!action || !marketplacePurchase) {
746
- return { handled: false, reason: 'missing_payload_data' };
747
- }
748
-
749
- const account = marketplacePurchase.account;
750
- if (!account || !account.id || !account.type) {
751
- return { handled: false, reason: 'missing_account_id' };
752
- }
753
-
754
- // Map GitHub account to customerId: github_<user|organization>_<id>
755
- const customerId = `github_${String(account.type).toLowerCase()}_${account.id}`;
756
-
310
+ if (!event) return { handled: false, reason: 'missing_payload_data' };
311
+ const { action, marketplace_purchase: mp } = event;
312
+ if (!action || !mp || !mp.account?.id) return { handled: false, reason: 'missing_payload_data' };
313
+ const customerId = `github_${String(mp.account.type).toLowerCase()}_${mp.account.id}`;
757
314
  switch (action) {
758
315
  case 'purchased': {
759
316
  const result = provisionApiKey(customerId, { source: 'github_marketplace_purchased' });
760
- appendFunnelEvent({
761
- stage: 'paid',
762
- event: 'github_marketplace_purchased',
763
- evidence: 'github_marketplace_purchased',
764
- metadata: {
765
- provider: 'github',
766
- customerId,
767
- accountId: String(account.id),
768
- accountType: String(account.type),
769
- },
770
- });
771
- return {
772
- handled: true,
773
- action: 'provisioned_api_key',
774
- result,
775
- };
776
- }
777
-
778
- case 'cancelled': {
779
- const result = disableCustomerKeys(customerId);
780
- return {
781
- handled: true,
782
- action: 'disabled_customer_keys',
783
- result,
784
- };
785
- }
786
-
787
- case 'changed': {
788
- // Keep API access active on plan changes.
789
- const result = provisionApiKey(customerId, { source: 'github_marketplace_changed' });
790
- return {
791
- handled: true,
792
- action: 'plan_changed',
793
- result,
794
- };
317
+ appendFunnelEvent({ stage: 'paid', event: 'github_marketplace_purchased', evidence: 'github_marketplace_purchased', metadata: { provider: 'github', customerId, accountId: String(mp.account.id), accountType: String(mp.account.type) } });
318
+ return { handled: true, action: 'provisioned_api_key', result };
795
319
  }
796
-
797
- default:
798
- return { handled: false, reason: `unhandled_action:${action}` };
320
+ case 'cancelled': return { handled: true, action: 'disabled_customer_keys', result: disableCustomerKeys(customerId) };
321
+ case 'changed': return { handled: true, action: 'plan_changed', result: provisionApiKey(customerId, { source: 'github_marketplace_changed' }) };
322
+ default: return { handled: false, reason: `unhandled_action:${action}` };
799
323
  }
800
324
  }
801
325
 
802
- // ---------------------------------------------------------------------------
803
- // Module exports
804
- // ---------------------------------------------------------------------------
805
-
806
326
  module.exports = {
807
- createCheckoutSession,
808
- provisionApiKey,
809
- validateApiKey,
810
- recordUsage,
811
- disableCustomerKeys,
812
- handleWebhook,
813
- verifyWebhookSignature,
814
- verifyGithubWebhookSignature,
815
- handleGithubWebhook,
816
- loadKeyStore,
817
- appendFunnelEvent,
818
- loadFunnelLedger,
819
- getFunnelAnalytics,
820
- readInstallIdFromConfig,
821
- // Expose for testing
822
- _API_KEYS_PATH: API_KEYS_PATH,
823
- _FUNNEL_LEDGER_PATH: FUNNEL_LEDGER_PATH,
824
- _LOCAL_MODE: () => LOCAL_MODE,
327
+ createCheckoutSession, getCheckoutSessionStatus, provisionApiKey, rotateApiKey, validateApiKey, recordUsage, reportUsageToStripe, disableCustomerKeys, handleWebhook, verifyGithubWebhookSignature, handleGithubWebhook, loadKeyStore, appendFunnelEvent, loadFunnelLedger, getFunnelAnalytics,
328
+ _API_KEYS_PATH: () => CONFIG.API_KEYS_PATH,
329
+ _FUNNEL_LEDGER_PATH: () => CONFIG.FUNNEL_LEDGER_PATH,
330
+ _LOCAL_CHECKOUT_SESSIONS_PATH: () => CONFIG.LOCAL_CHECKOUT_SESSIONS_PATH,
331
+ _LOCAL_MODE: () => LOCAL_MODE(),
825
332
  };