rlhf-feedback-loop 0.6.10 → 0.6.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +120 -74
- package/adapters/README.md +3 -3
- package/adapters/amp/skills/rlhf-feedback/SKILL.md +2 -0
- package/adapters/chatgpt/INSTALL.md +6 -3
- package/adapters/chatgpt/openapi.yaml +5 -2
- package/adapters/claude/.mcp.json +3 -3
- package/adapters/codex/config.toml +3 -3
- package/adapters/gemini/function-declarations.json +2 -2
- package/adapters/mcp/server-stdio.js +19 -5
- package/bin/cli.js +295 -25
- package/openapi/openapi.yaml +5 -2
- package/package.json +25 -9
- package/scripts/a2ui-engine.js +73 -0
- package/scripts/adk-consolidator.js +267 -0
- package/scripts/billing.js +192 -681
- package/scripts/code-reasoning.js +26 -1
- package/scripts/context-engine.js +86 -4
- package/scripts/contextfs.js +130 -0
- package/scripts/disagreement-mining.js +315 -0
- package/scripts/export-kto-pairs.js +310 -0
- package/scripts/feedback-ingest-watcher.js +290 -0
- package/scripts/feedback-loop.js +153 -8
- package/scripts/feedback-quality.js +139 -0
- package/scripts/feedback-schema.js +31 -5
- package/scripts/feedback-to-memory.js +13 -1
- package/scripts/hook-auto-capture.sh +6 -0
- package/scripts/hook-stop-self-score.sh +51 -0
- package/scripts/install-mcp.js +168 -0
- package/scripts/intent-router.js +88 -0
- package/scripts/jsonl-watcher.js +151 -0
- package/scripts/local-model-profile.js +207 -0
- package/scripts/pr-manager.js +112 -0
- package/scripts/prove-adapters.js +137 -15
- package/scripts/prove-attribution.js +6 -6
- package/scripts/prove-automation.js +41 -8
- package/scripts/prove-data-quality.js +16 -8
- package/scripts/prove-intelligence.js +7 -4
- package/scripts/prove-lancedb.js +7 -7
- package/scripts/prove-local-intelligence.js +244 -0
- package/scripts/prove-loop-closure.js +16 -8
- package/scripts/prove-training-export.js +7 -4
- package/scripts/prove-workflow-contract.js +116 -0
- package/scripts/reminder-engine.js +132 -0
- package/scripts/risk-scorer.js +458 -0
- package/scripts/rlaif-self-audit.js +7 -1
- package/scripts/self-heal.js +24 -4
- package/scripts/status-dashboard.js +155 -0
- package/scripts/sync-version.js +159 -0
- package/scripts/test-coverage.js +76 -0
- package/scripts/validate-workflow-contract.js +287 -0
- package/scripts/vector-store.js +115 -17
- package/src/api/server.js +372 -25
package/src/api/server.js
CHANGED
|
@@ -32,9 +32,11 @@ const {
|
|
|
32
32
|
} = require('../../scripts/intent-router');
|
|
33
33
|
const {
|
|
34
34
|
createCheckoutSession,
|
|
35
|
+
getCheckoutSessionStatus,
|
|
35
36
|
provisionApiKey,
|
|
36
37
|
validateApiKey,
|
|
37
38
|
recordUsage,
|
|
39
|
+
rotateApiKey,
|
|
38
40
|
handleWebhook,
|
|
39
41
|
verifyWebhookSignature,
|
|
40
42
|
verifyGithubWebhookSignature,
|
|
@@ -42,6 +44,8 @@ const {
|
|
|
42
44
|
getFunnelAnalytics,
|
|
43
45
|
} = require('../../scripts/billing');
|
|
44
46
|
|
|
47
|
+
const LANDING_PAGE_PATH = path.resolve(__dirname, '../../docs/landing-page.html');
|
|
48
|
+
|
|
45
49
|
function getSafeDataDir() {
|
|
46
50
|
const { FEEDBACK_LOG_PATH } = getFeedbackPaths();
|
|
47
51
|
return path.resolve(path.dirname(FEEDBACK_LOG_PATH));
|
|
@@ -70,6 +74,284 @@ function sendText(res, statusCode, text) {
|
|
|
70
74
|
res.end(text);
|
|
71
75
|
}
|
|
72
76
|
|
|
77
|
+
function sendHtml(res, statusCode, html) {
|
|
78
|
+
res.writeHead(statusCode, {
|
|
79
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
80
|
+
'Content-Length': Buffer.byteLength(html),
|
|
81
|
+
});
|
|
82
|
+
res.end(html);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getPublicOrigin(req) {
|
|
86
|
+
const proto = String(req.headers['x-forwarded-proto'] || '').split(',')[0].trim() || 'http';
|
|
87
|
+
const host = String(req.headers['x-forwarded-host'] || req.headers.host || '').split(',')[0].trim() || 'localhost';
|
|
88
|
+
return `${proto}://${host}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function wantsJson(req, parsed) {
|
|
92
|
+
if (parsed.searchParams.get('format') === 'json') {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const accept = String(req.headers.accept || '');
|
|
97
|
+
return accept.includes('application/json') && !accept.includes('text/html');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function fillTemplate(template, replacements) {
|
|
101
|
+
let output = template;
|
|
102
|
+
for (const [token, value] of Object.entries(replacements)) {
|
|
103
|
+
output = output.split(token).join(String(value));
|
|
104
|
+
}
|
|
105
|
+
return output;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function loadLandingPageHtml(origin) {
|
|
109
|
+
const template = fs.readFileSync(LANDING_PAGE_PATH, 'utf-8');
|
|
110
|
+
return fillTemplate(template, {
|
|
111
|
+
'__PACKAGE_VERSION__': pkg.version,
|
|
112
|
+
'__APP_ORIGIN__': origin,
|
|
113
|
+
'__CHECKOUT_ENDPOINT__': '/v1/billing/checkout',
|
|
114
|
+
'__CHECKOUT_FALLBACK_URL__': 'https://buy.stripe.com/bJe14neyU4r4f0leOD3sI02',
|
|
115
|
+
'__VERIFICATION_URL__': 'https://github.com/IgorGanapolsky/mcp-memory-gateway/blob/main/docs/VERIFICATION_EVIDENCE.md',
|
|
116
|
+
'__COMPATIBILITY_REPORT_URL__': 'https://github.com/IgorGanapolsky/mcp-memory-gateway/blob/main/proof/compatibility/report.json',
|
|
117
|
+
'__AUTOMATION_REPORT_URL__': 'https://github.com/IgorGanapolsky/mcp-memory-gateway/blob/main/proof/automation/report.json',
|
|
118
|
+
'__GTM_PLAN_URL__': 'https://github.com/IgorGanapolsky/mcp-memory-gateway/blob/main/docs/GO_TO_MARKET_REVENUE_WEDGE_2026-03.md',
|
|
119
|
+
'__GITHUB_URL__': 'https://github.com/IgorGanapolsky/mcp-memory-gateway',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function renderCheckoutSuccessPage() {
|
|
124
|
+
return `<!DOCTYPE html>
|
|
125
|
+
<html lang="en">
|
|
126
|
+
<head>
|
|
127
|
+
<meta charset="UTF-8" />
|
|
128
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
129
|
+
<title>Cloud Pro Activated</title>
|
|
130
|
+
<style>
|
|
131
|
+
:root {
|
|
132
|
+
--bg: #f6f1e8;
|
|
133
|
+
--ink: #1d1b18;
|
|
134
|
+
--muted: #625a4d;
|
|
135
|
+
--line: #d7cfbf;
|
|
136
|
+
--accent: #b85c2d;
|
|
137
|
+
--accent-dark: #8f451f;
|
|
138
|
+
--card: #fffdf9;
|
|
139
|
+
--success: #2f7d4b;
|
|
140
|
+
--radius: 14px;
|
|
141
|
+
}
|
|
142
|
+
* { box-sizing: border-box; }
|
|
143
|
+
body {
|
|
144
|
+
margin: 0;
|
|
145
|
+
font-family: Georgia, 'Times New Roman', serif;
|
|
146
|
+
background: linear-gradient(180deg, #fcfaf5 0%, var(--bg) 100%);
|
|
147
|
+
color: var(--ink);
|
|
148
|
+
line-height: 1.6;
|
|
149
|
+
}
|
|
150
|
+
main {
|
|
151
|
+
max-width: 860px;
|
|
152
|
+
margin: 0 auto;
|
|
153
|
+
padding: 48px 20px 80px;
|
|
154
|
+
}
|
|
155
|
+
.eyebrow {
|
|
156
|
+
display: inline-block;
|
|
157
|
+
padding: 6px 12px;
|
|
158
|
+
border-radius: 999px;
|
|
159
|
+
background: #efe3d5;
|
|
160
|
+
color: var(--accent-dark);
|
|
161
|
+
font-size: 12px;
|
|
162
|
+
letter-spacing: 0.08em;
|
|
163
|
+
text-transform: uppercase;
|
|
164
|
+
font-weight: 700;
|
|
165
|
+
}
|
|
166
|
+
h1 {
|
|
167
|
+
margin: 18px 0 12px;
|
|
168
|
+
font-size: clamp(32px, 6vw, 56px);
|
|
169
|
+
line-height: 1.05;
|
|
170
|
+
letter-spacing: -0.04em;
|
|
171
|
+
}
|
|
172
|
+
p.lead {
|
|
173
|
+
max-width: 700px;
|
|
174
|
+
font-size: 19px;
|
|
175
|
+
color: var(--muted);
|
|
176
|
+
margin: 0 0 28px;
|
|
177
|
+
}
|
|
178
|
+
.card {
|
|
179
|
+
background: var(--card);
|
|
180
|
+
border: 1px solid var(--line);
|
|
181
|
+
border-radius: var(--radius);
|
|
182
|
+
padding: 24px;
|
|
183
|
+
margin-top: 22px;
|
|
184
|
+
box-shadow: 0 10px 30px rgba(29, 27, 24, 0.08);
|
|
185
|
+
}
|
|
186
|
+
.status {
|
|
187
|
+
color: var(--success);
|
|
188
|
+
font-weight: 700;
|
|
189
|
+
margin-bottom: 8px;
|
|
190
|
+
}
|
|
191
|
+
pre {
|
|
192
|
+
white-space: pre-wrap;
|
|
193
|
+
word-break: break-word;
|
|
194
|
+
background: #171411;
|
|
195
|
+
color: #f5efe6;
|
|
196
|
+
padding: 16px;
|
|
197
|
+
border-radius: 12px;
|
|
198
|
+
overflow-x: auto;
|
|
199
|
+
font-family: 'SFMono-Regular', Consolas, monospace;
|
|
200
|
+
font-size: 13px;
|
|
201
|
+
}
|
|
202
|
+
.actions {
|
|
203
|
+
display: flex;
|
|
204
|
+
gap: 12px;
|
|
205
|
+
flex-wrap: wrap;
|
|
206
|
+
margin-top: 18px;
|
|
207
|
+
}
|
|
208
|
+
a.button {
|
|
209
|
+
display: inline-block;
|
|
210
|
+
text-decoration: none;
|
|
211
|
+
background: var(--accent);
|
|
212
|
+
color: white;
|
|
213
|
+
padding: 12px 18px;
|
|
214
|
+
border-radius: 10px;
|
|
215
|
+
font-weight: 700;
|
|
216
|
+
}
|
|
217
|
+
a.button.secondary {
|
|
218
|
+
background: transparent;
|
|
219
|
+
color: var(--ink);
|
|
220
|
+
border: 1px solid var(--line);
|
|
221
|
+
}
|
|
222
|
+
.muted {
|
|
223
|
+
color: var(--muted);
|
|
224
|
+
font-size: 14px;
|
|
225
|
+
}
|
|
226
|
+
</style>
|
|
227
|
+
</head>
|
|
228
|
+
<body>
|
|
229
|
+
<main>
|
|
230
|
+
<span class="eyebrow">Cloud Pro</span>
|
|
231
|
+
<h1>Your hosted API key is ready.</h1>
|
|
232
|
+
<p class="lead">This page verifies your Stripe session, provisions the key if needed, and gives you a copy-paste onboarding snippet for the hosted API.</p>
|
|
233
|
+
|
|
234
|
+
<div class="card">
|
|
235
|
+
<div class="status" id="status">Verifying payment and provisioning your key...</div>
|
|
236
|
+
<p class="muted" id="summary">Do not close this tab until the key appears.</p>
|
|
237
|
+
<pre id="key-block">Waiting for checkout session...</pre>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<div class="card">
|
|
241
|
+
<h2>Next steps</h2>
|
|
242
|
+
<ol>
|
|
243
|
+
<li>Copy the environment block below into your workflow runner.</li>
|
|
244
|
+
<li>Use the curl example to confirm the hosted API captures an event.</li>
|
|
245
|
+
<li>Keep your key private and rotate by repurchasing or contacting support if needed.</li>
|
|
246
|
+
</ol>
|
|
247
|
+
<pre id="env-block">Waiting for provisioning...</pre>
|
|
248
|
+
<pre id="curl-block">Waiting for provisioning...</pre>
|
|
249
|
+
<div class="actions">
|
|
250
|
+
<a class="button" href="/">Back to landing page</a>
|
|
251
|
+
<a class="button secondary" href="https://github.com/IgorGanapolsky/rlhf-feedback-loop/blob/main/docs/VERIFICATION_EVIDENCE.md" target="_blank" rel="noreferrer">Verification evidence</a>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</main>
|
|
255
|
+
|
|
256
|
+
<script>
|
|
257
|
+
const params = new URLSearchParams(window.location.search);
|
|
258
|
+
const sessionId = params.get('session_id');
|
|
259
|
+
const statusEl = document.getElementById('status');
|
|
260
|
+
const summaryEl = document.getElementById('summary');
|
|
261
|
+
const keyBlock = document.getElementById('key-block');
|
|
262
|
+
const envBlock = document.getElementById('env-block');
|
|
263
|
+
const curlBlock = document.getElementById('curl-block');
|
|
264
|
+
|
|
265
|
+
async function run() {
|
|
266
|
+
if (!sessionId) {
|
|
267
|
+
statusEl.textContent = 'Missing checkout session.';
|
|
268
|
+
summaryEl.textContent = 'Open the landing page and start a new checkout.';
|
|
269
|
+
keyBlock.textContent = 'No session_id was provided in the URL.';
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const res = await fetch('/v1/billing/session?sessionId=' + encodeURIComponent(sessionId));
|
|
275
|
+
const body = await res.json();
|
|
276
|
+
if (!res.ok) {
|
|
277
|
+
throw new Error(body.error || 'Unable to load checkout session.');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!body.paid) {
|
|
281
|
+
statusEl.textContent = 'Payment is still processing.';
|
|
282
|
+
summaryEl.textContent = 'Refresh this page in a few seconds if Stripe has already confirmed payment.';
|
|
283
|
+
keyBlock.textContent = JSON.stringify(body, null, 2);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
statusEl.textContent = 'Cloud Pro activated.';
|
|
288
|
+
summaryEl.textContent = 'Your API key is ready. Copy the snippets below into your workflow project.';
|
|
289
|
+
keyBlock.textContent = body.apiKey || 'Provisioned, but no key was returned.';
|
|
290
|
+
envBlock.textContent = body.nextSteps && body.nextSteps.env ? body.nextSteps.env : 'Environment snippet unavailable.';
|
|
291
|
+
curlBlock.textContent = body.nextSteps && body.nextSteps.curl ? body.nextSteps.curl : 'curl snippet unavailable.';
|
|
292
|
+
} catch (err) {
|
|
293
|
+
statusEl.textContent = 'Provisioning lookup failed.';
|
|
294
|
+
summaryEl.textContent = 'You can retry this page. If it keeps failing, inspect the hosted API logs.';
|
|
295
|
+
keyBlock.textContent = err && err.message ? err.message : 'Unknown error';
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
run();
|
|
300
|
+
</script>
|
|
301
|
+
</body>
|
|
302
|
+
</html>`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function renderCheckoutCancelledPage() {
|
|
306
|
+
return `<!DOCTYPE html>
|
|
307
|
+
<html lang="en">
|
|
308
|
+
<head>
|
|
309
|
+
<meta charset="UTF-8" />
|
|
310
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
311
|
+
<title>Checkout Cancelled</title>
|
|
312
|
+
<style>
|
|
313
|
+
body {
|
|
314
|
+
margin: 0;
|
|
315
|
+
font-family: Georgia, 'Times New Roman', serif;
|
|
316
|
+
background: #f6f1e8;
|
|
317
|
+
color: #1d1b18;
|
|
318
|
+
}
|
|
319
|
+
main {
|
|
320
|
+
max-width: 720px;
|
|
321
|
+
margin: 0 auto;
|
|
322
|
+
padding: 64px 20px 80px;
|
|
323
|
+
}
|
|
324
|
+
h1 {
|
|
325
|
+
font-size: clamp(32px, 6vw, 52px);
|
|
326
|
+
line-height: 1.05;
|
|
327
|
+
margin: 0 0 14px;
|
|
328
|
+
}
|
|
329
|
+
p {
|
|
330
|
+
font-size: 18px;
|
|
331
|
+
color: #625a4d;
|
|
332
|
+
margin: 0 0 20px;
|
|
333
|
+
}
|
|
334
|
+
a {
|
|
335
|
+
display: inline-block;
|
|
336
|
+
text-decoration: none;
|
|
337
|
+
background: #b85c2d;
|
|
338
|
+
color: white;
|
|
339
|
+
padding: 12px 18px;
|
|
340
|
+
border-radius: 10px;
|
|
341
|
+
font-weight: 700;
|
|
342
|
+
}
|
|
343
|
+
</style>
|
|
344
|
+
</head>
|
|
345
|
+
<body>
|
|
346
|
+
<main>
|
|
347
|
+
<h1>Checkout cancelled.</h1>
|
|
348
|
+
<p>No charge was made. You can return to the landing page and restart checkout whenever you are ready.</p>
|
|
349
|
+
<a href="/">Return to Cloud Pro</a>
|
|
350
|
+
</main>
|
|
351
|
+
</body>
|
|
352
|
+
</html>`;
|
|
353
|
+
}
|
|
354
|
+
|
|
73
355
|
function parseJsonBody(req, maxBytes = 1024 * 1024) {
|
|
74
356
|
return new Promise((resolve, reject) => {
|
|
75
357
|
let total = 0;
|
|
@@ -193,16 +475,36 @@ function createApiServer() {
|
|
|
193
475
|
return http.createServer(async (req, res) => {
|
|
194
476
|
const parsed = new URL(req.url, 'http://localhost');
|
|
195
477
|
const pathname = parsed.pathname;
|
|
478
|
+
const publicOrigin = getPublicOrigin(req);
|
|
196
479
|
|
|
197
480
|
// Public endpoints — no auth required
|
|
198
481
|
if (req.method === 'GET' && pathname === '/') {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
482
|
+
if (wantsJson(req, parsed)) {
|
|
483
|
+
sendJson(res, 200, {
|
|
484
|
+
name: 'rlhf-feedback-loop',
|
|
485
|
+
version: pkg.version,
|
|
486
|
+
status: 'ok',
|
|
487
|
+
docs: 'https://github.com/IgorGanapolsky/rlhf-feedback-loop',
|
|
488
|
+
endpoints: ['/health', '/v1/feedback/capture', '/v1/feedback/stats', '/v1/dpo/export'],
|
|
489
|
+
});
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
sendHtml(res, 200, loadLandingPageHtml(publicOrigin));
|
|
495
|
+
} catch (err) {
|
|
496
|
+
sendText(res, 500, err.message || 'Landing page unavailable');
|
|
497
|
+
}
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (req.method === 'GET' && pathname === '/success') {
|
|
502
|
+
sendHtml(res, 200, renderCheckoutSuccessPage());
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (req.method === 'GET' && pathname === '/cancel') {
|
|
507
|
+
sendHtml(res, 200, renderCheckoutCancelledPage());
|
|
206
508
|
return;
|
|
207
509
|
}
|
|
208
510
|
|
|
@@ -213,7 +515,7 @@ function createApiServer() {
|
|
|
213
515
|
version: pkg.version,
|
|
214
516
|
tools: [
|
|
215
517
|
{ name: 'recall', description: 'Recall relevant past feedback for current task' },
|
|
216
|
-
{ name: 'capture_feedback', description: 'Capture
|
|
518
|
+
{ name: 'capture_feedback', description: 'Capture an up/down signal plus one line of why' },
|
|
217
519
|
{ name: 'feedback_stats', description: 'Feedback analytics' },
|
|
218
520
|
{ name: 'feedback_summary', description: 'Human-readable feedback summary' },
|
|
219
521
|
{ name: 'prevention_rules', description: 'Generate prevention rules from failures' },
|
|
@@ -260,21 +562,8 @@ function createApiServer() {
|
|
|
260
562
|
});
|
|
261
563
|
|
|
262
564
|
const sig = req.headers['stripe-signature'] || '';
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
let event;
|
|
269
|
-
try {
|
|
270
|
-
event = JSON.parse(rawBody.toString('utf-8'));
|
|
271
|
-
} catch {
|
|
272
|
-
sendJson(res, 400, { error: 'Invalid JSON in webhook body' });
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const result = handleWebhook(event);
|
|
277
|
-
sendJson(res, 200, result);
|
|
565
|
+
const result = await handleWebhook(rawBody, sig);
|
|
566
|
+
sendJson(res, result.handled ? 200 : 400, result);
|
|
278
567
|
} catch (err) {
|
|
279
568
|
if (err.statusCode) {
|
|
280
569
|
sendJson(res, err.statusCode, { error: err.message });
|
|
@@ -326,8 +615,8 @@ function createApiServer() {
|
|
|
326
615
|
try {
|
|
327
616
|
const body = await parseJsonBody(req);
|
|
328
617
|
const result = await createCheckoutSession({
|
|
329
|
-
successUrl: body.successUrl
|
|
330
|
-
cancelUrl: body.cancelUrl
|
|
618
|
+
successUrl: body.successUrl || `${publicOrigin}/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
619
|
+
cancelUrl: body.cancelUrl || `${publicOrigin}/cancel`,
|
|
331
620
|
customerEmail: body.customerEmail,
|
|
332
621
|
installId: body.installId,
|
|
333
622
|
metadata: body.metadata,
|
|
@@ -343,6 +632,36 @@ function createApiServer() {
|
|
|
343
632
|
return;
|
|
344
633
|
}
|
|
345
634
|
|
|
635
|
+
if (req.method === 'GET' && pathname === '/v1/billing/session') {
|
|
636
|
+
try {
|
|
637
|
+
const sessionId = parsed.searchParams.get('sessionId');
|
|
638
|
+
if (!sessionId) {
|
|
639
|
+
throw createHttpError(400, 'sessionId is required');
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const result = await getCheckoutSessionStatus(sessionId);
|
|
643
|
+
if (!result.found) {
|
|
644
|
+
throw createHttpError(404, 'Checkout session not found');
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
sendJson(res, 200, {
|
|
648
|
+
...result,
|
|
649
|
+
apiBaseUrl: publicOrigin,
|
|
650
|
+
nextSteps: {
|
|
651
|
+
env: `RLHF_API_KEY=${result.apiKey || ''}\nRLHF_API_BASE_URL=${publicOrigin}`,
|
|
652
|
+
curl: `curl -X POST ${publicOrigin}/v1/feedback/capture \\\n -H 'Authorization: Bearer ${result.apiKey || ''}' \\\n -H 'Content-Type: application/json' \\\n -d '{"signal":"down","context":"example","whatWentWrong":"example","whatToChange":"example"}'`,
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
} catch (err) {
|
|
656
|
+
if (err.statusCode) {
|
|
657
|
+
sendJson(res, err.statusCode, { error: err.message });
|
|
658
|
+
} else {
|
|
659
|
+
sendJson(res, 500, { error: err.message || 'Internal Server Error' });
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
346
665
|
if (!isAuthorized(req, expectedApiKey)) {
|
|
347
666
|
sendJson(res, 401, { error: 'Unauthorized' });
|
|
348
667
|
return;
|
|
@@ -553,6 +872,34 @@ function createApiServer() {
|
|
|
553
872
|
return;
|
|
554
873
|
}
|
|
555
874
|
|
|
875
|
+
// POST /v1/billing/rotate-key — rotate the authenticated key, preserving subscription
|
|
876
|
+
if (req.method === 'POST' && pathname === '/v1/billing/rotate-key') {
|
|
877
|
+
const currentKey = extractBearerToken(req);
|
|
878
|
+
if (!currentKey) {
|
|
879
|
+
sendJson(res, 401, { error: 'Unauthorized' });
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const validation = validateApiKey(currentKey);
|
|
883
|
+
if (!validation.valid) {
|
|
884
|
+
sendJson(res, 400, { error: 'Key not found or already disabled' });
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
try {
|
|
888
|
+
const result = rotateApiKey(currentKey);
|
|
889
|
+
if (!result.rotated) {
|
|
890
|
+
sendJson(res, 400, { error: result.reason || 'Key rotation failed' });
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
sendJson(res, 200, {
|
|
894
|
+
newKey: result.newKey,
|
|
895
|
+
message: 'Key rotated. Update your configuration.',
|
|
896
|
+
});
|
|
897
|
+
} catch (err) {
|
|
898
|
+
sendJson(res, 500, { error: err.message || 'Internal Server Error' });
|
|
899
|
+
}
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
556
903
|
// GET /v1/analytics/funnel — aggregate acquisition/activation/paid funnel metrics
|
|
557
904
|
if (req.method === 'GET' && pathname === '/v1/analytics/funnel') {
|
|
558
905
|
const summary = getFunnelAnalytics();
|