neoagent 3.0.1-beta.6 → 3.0.1-beta.8

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 (52) hide show
  1. package/.env.example +19 -0
  2. package/README.md +2 -2
  3. package/docs/billing.md +203 -0
  4. package/docs/configuration.md +13 -0
  5. package/docs/index.md +1 -0
  6. package/landing/index.html +2 -2
  7. package/lib/schema_migrations.js +84 -0
  8. package/package.json +7 -4
  9. package/server/admin/admin.js +2 -1
  10. package/server/admin/billing.js +292 -0
  11. package/server/admin/index.html +37 -0
  12. package/server/db/database.js +9 -11
  13. package/server/db/sessions_db.js +8 -0
  14. package/server/http/middleware.js +4 -4
  15. package/server/http/routes.js +9 -0
  16. package/server/middleware/requireBilling.js +12 -0
  17. package/server/public/.last_build_id +1 -1
  18. package/server/public/flutter_bootstrap.js +1 -1
  19. package/server/public/main.dart.js +4 -4
  20. package/server/routes/admin.js +144 -16
  21. package/server/routes/billing.js +122 -0
  22. package/server/routes/billing_webhook.js +43 -0
  23. package/server/routes/memory.js +1 -4
  24. package/server/routes/settings.js +1 -2
  25. package/server/routes/skills.js +1 -1
  26. package/server/routes/triggers.js +2 -2
  27. package/server/services/account/sessions.js +1 -9
  28. package/server/services/ai/loop/agent_engine_core.js +1 -1
  29. package/server/services/ai/loop/completion_judge.js +2 -9
  30. package/server/services/ai/loop/conversation_loop.js +30 -29
  31. package/server/services/ai/loopPolicy.js +2 -39
  32. package/server/services/ai/models.js +18 -0
  33. package/server/services/ai/rate_limits.js +28 -4
  34. package/server/services/ai/systemPrompt.js +17 -0
  35. package/server/services/ai/tools.js +38 -11
  36. package/server/services/billing/billing_email.js +106 -0
  37. package/server/services/billing/config.js +17 -0
  38. package/server/services/billing/plans.js +120 -0
  39. package/server/services/billing/stripe_client.js +21 -0
  40. package/server/services/billing/subscriptions.js +462 -0
  41. package/server/services/billing/trial_guard.js +111 -0
  42. package/server/services/desktop/gateway.js +7 -4
  43. package/server/services/integrations/secrets.js +7 -4
  44. package/server/services/manager.js +11 -0
  45. package/server/services/messaging/telnyx.js +3 -3
  46. package/server/services/messaging/whatsapp.js +1 -1
  47. package/server/services/recordings/manager.js +13 -7
  48. package/server/services/tasks/runtime.js +59 -13
  49. package/server/services/voice/runtimeManager.js +19 -10
  50. package/server/services/wearable/gateway.js +7 -4
  51. package/server/services/websocket.js +5 -5
  52. package/server/services/workspace/manager.js +23 -0
@@ -0,0 +1,292 @@
1
+ 'use strict';
2
+
3
+ let _billingPlans = [];
4
+ let _billingSubsOffset = 0;
5
+ const BILLING_SUBS_LIMIT = 50;
6
+
7
+ async function loadBilling() {
8
+ // Check if billing is enabled by probing the plans endpoint.
9
+ try {
10
+ const r = await fetch('/admin/api/billing/plans');
11
+ if (!r.ok) {
12
+ document.getElementById('billing-plans-content').innerHTML =
13
+ '<div class="empty">Billing is not enabled on this server. Set <code>NEOAGENT_BILLING_ENABLED=1</code> to enable it.</div>';
14
+ document.getElementById('billing-subs-content').innerHTML = '';
15
+ document.getElementById('nav-billing').style.display = '';
16
+ return;
17
+ }
18
+ const data = await r.json();
19
+ _billingPlans = data.plans || [];
20
+ renderPlansTable(_billingPlans);
21
+ } catch {
22
+ document.getElementById('billing-plans-content').innerHTML = '<div class="empty">Failed to load billing data.</div>';
23
+ }
24
+ document.getElementById('nav-billing').style.display = '';
25
+ _billingSubsOffset = 0;
26
+ await loadBillingSubscriptions();
27
+ }
28
+
29
+ // Show the billing nav item on load if billing is enabled.
30
+ (async function checkBillingEnabled() {
31
+ try {
32
+ const r = await fetch('/admin/api/billing/plans');
33
+ if (r.ok) document.getElementById('nav-billing').style.display = '';
34
+ } catch {}
35
+ })();
36
+
37
+ function planRowHtml(plan) {
38
+ const id = escAttr(plan.id);
39
+ const intervalLabel = plan.interval ? `/ ${plan.interval}` : '';
40
+ const price = plan.price_cents === 0 ? 'Free' : `${(plan.price_cents / 100).toFixed(2)} ${(plan.currency || 'usd').toUpperCase()} ${intervalLabel}`;
41
+ const status = plan.is_active ? '<span style="color:var(--success)">Active</span>' : '<span style="color:var(--muted)">Inactive</span>';
42
+ return `
43
+ <tr data-plan-id="${id}">
44
+ <td><strong>${esc(plan.name)}</strong><br><small style="color:var(--muted)">${esc(plan.id)}</small></td>
45
+ <td>${esc(price)}</td>
46
+ <td>${plan.token_limit_4h != null ? fmtTokens(plan.token_limit_4h) : '<span style="color:var(--muted)">Default</span>'}</td>
47
+ <td>${plan.token_limit_weekly != null ? fmtTokens(plan.token_limit_weekly) : '<span style="color:var(--muted)">Default</span>'}</td>
48
+ <td><code style="font-size:11px">${esc(plan.stripe_price_id || '—')}</code></td>
49
+ <td>${status}</td>
50
+ <td>
51
+ <button class="btn btn-sm" onclick="billingEditPlan('${id}')">Edit</button>
52
+ <button class="btn btn-sm btn-danger" onclick="billingDeletePlan('${id}')">Delete</button>
53
+ </td>
54
+ </tr>`;
55
+ }
56
+
57
+ function renderPlansTable(plans) {
58
+ const el = document.getElementById('billing-plans-content');
59
+ if (!plans.length) {
60
+ el.innerHTML = '<div class="empty">No plans yet. Create one above.</div>';
61
+ return;
62
+ }
63
+ el.innerHTML = `
64
+ <table class="data-table">
65
+ <thead><tr>
66
+ <th>Plan</th><th>Price</th><th>4h Tokens</th><th>Weekly Tokens</th><th>Stripe Price ID</th><th>Status</th><th>Actions</th>
67
+ </tr></thead>
68
+ <tbody>${plans.map(planRowHtml).join('')}</tbody>
69
+ </table>`;
70
+ }
71
+
72
+ function billingShowNewPlanForm() {
73
+ billingOpenPlanModal(null);
74
+ }
75
+
76
+ function billingEditPlan(planId) {
77
+ const plan = _billingPlans.find((p) => p.id === planId);
78
+ billingOpenPlanModal(plan);
79
+ }
80
+
81
+ function billingOpenPlanModal(plan) {
82
+ const isNew = !plan;
83
+ const title = isNew ? 'New Plan' : `Edit Plan: ${plan.name}`;
84
+ const html = `
85
+ <div id="billing-plan-modal" style="position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:1000">
86
+ <div style="background:var(--card-bg);border-radius:12px;padding:28px;width:540px;max-height:90vh;overflow-y:auto;box-shadow:0 8px 32px rgba(0,0,0,0.3)">
87
+ <h2 style="margin:0 0 20px">${esc(title)}</h2>
88
+ <form id="billing-plan-form" onsubmit="billingPlanFormSubmit(event)">
89
+ <input type="hidden" id="bp-id" value="${esc(plan?.id || '')}">
90
+ <label class="form-label">Plan ID <small>(set once, used as identifier)</small></label>
91
+ <input class="form-input" id="bp-slug" value="${esc(plan?.id || '')}" ${isNew ? '' : 'disabled'} placeholder="plan_pro" style="margin-bottom:12px">
92
+ <label class="form-label">Name</label>
93
+ <input class="form-input" id="bp-name" value="${esc(plan?.name || '')}" required placeholder="Pro" style="margin-bottom:12px">
94
+ <label class="form-label">Description</label>
95
+ <input class="form-input" id="bp-desc" value="${esc(plan?.description || '')}" placeholder="Optional description" style="margin-bottom:12px">
96
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px">
97
+ <div>
98
+ <label class="form-label">Price (cents)</label>
99
+ <input class="form-input" id="bp-price" type="number" min="0" value="${plan?.price_cents ?? 0}" required>
100
+ </div>
101
+ <div>
102
+ <label class="form-label">Currency</label>
103
+ <input class="form-input" id="bp-currency" value="${esc(plan?.currency || 'usd')}" placeholder="usd">
104
+ </div>
105
+ </div>
106
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px">
107
+ <div>
108
+ <label class="form-label">Billing interval</label>
109
+ <select class="form-input" id="bp-interval">
110
+ <option value="month" ${plan?.interval === 'month' ? 'selected' : ''}>Monthly</option>
111
+ <option value="year" ${plan?.interval === 'year' ? 'selected' : ''}>Yearly</option>
112
+ <option value="" ${!plan?.interval ? 'selected' : ''}>One-time / Free</option>
113
+ </select>
114
+ </div>
115
+ <div>
116
+ <label class="form-label">Sort order</label>
117
+ <input class="form-input" id="bp-sort" type="number" value="${plan?.sort_order ?? 0}">
118
+ </div>
119
+ </div>
120
+ <label class="form-label">Stripe Price ID</label>
121
+ <input class="form-input" id="bp-stripe-price" value="${esc(plan?.stripe_price_id || '')}" placeholder="price_..." style="margin-bottom:12px">
122
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px">
123
+ <div>
124
+ <label class="form-label">4h token limit <small>(blank = default)</small></label>
125
+ <input class="form-input" id="bp-tok-4h" type="number" min="0" value="${plan?.token_limit_4h ?? ''}" placeholder="2500000">
126
+ </div>
127
+ <div>
128
+ <label class="form-label">Weekly token limit <small>(blank = default)</small></label>
129
+ <input class="form-input" id="bp-tok-weekly" type="number" min="0" value="${plan?.token_limit_weekly ?? ''}" placeholder="10000000">
130
+ </div>
131
+ </div>
132
+ <label class="form-label">Allowed model IDs <small>(comma-separated; blank = all)</small></label>
133
+ <input class="form-input" id="bp-models" value="${esc((plan?.allowed_models || []).join(', '))}" placeholder="claude-opus-4-8, gpt-4o" style="margin-bottom:12px">
134
+ <label class="form-label">Features <small>(comma-separated, shown on pricing page)</small></label>
135
+ <input class="form-input" id="bp-features" value="${esc((plan?.features || []).join(', '))}" placeholder="Unlimited agents, Priority support" style="margin-bottom:16px">
136
+ <label style="display:flex;align-items:center;gap:8px;margin-bottom:20px;cursor:pointer">
137
+ <input type="checkbox" id="bp-active" ${!plan || plan.is_active ? 'checked' : ''}> Active
138
+ </label>
139
+ <div style="display:flex;gap:8px;justify-content:flex-end">
140
+ <button type="button" class="btn" onclick="billingClosePlanModal()">Cancel</button>
141
+ <button type="submit" class="btn btn-primary">${isNew ? 'Create' : 'Save'}</button>
142
+ </div>
143
+ </form>
144
+ </div>
145
+ </div>`;
146
+ document.body.insertAdjacentHTML('beforeend', html);
147
+ }
148
+
149
+ function billingClosePlanModal() {
150
+ document.getElementById('billing-plan-modal')?.remove();
151
+ }
152
+
153
+ async function billingPlanFormSubmit(e) {
154
+ e.preventDefault();
155
+ const id = document.getElementById('bp-id').value;
156
+ const slug = document.getElementById('bp-slug').value.trim();
157
+ const isNew = !id;
158
+
159
+ const parseTokenLimit = (val) => {
160
+ const n = parseInt(val, 10);
161
+ return isNaN(n) || val === '' ? null : n;
162
+ };
163
+ const parseModels = (val) => val.split(',').map((s) => s.trim()).filter(Boolean);
164
+ const parseFeatures = (val) => val.split(',').map((s) => s.trim()).filter(Boolean);
165
+
166
+ const body = {
167
+ id: isNew ? slug : undefined,
168
+ name: document.getElementById('bp-name').value.trim(),
169
+ description: document.getElementById('bp-desc').value.trim(),
170
+ price_cents: parseInt(document.getElementById('bp-price').value, 10),
171
+ currency: document.getElementById('bp-currency').value.trim() || 'usd',
172
+ interval: document.getElementById('bp-interval').value || null,
173
+ stripe_price_id: document.getElementById('bp-stripe-price').value.trim() || null,
174
+ token_limit_4h: parseTokenLimit(document.getElementById('bp-tok-4h').value),
175
+ token_limit_weekly: parseTokenLimit(document.getElementById('bp-tok-weekly').value),
176
+ allowed_models: parseModels(document.getElementById('bp-models').value),
177
+ features: parseFeatures(document.getElementById('bp-features').value),
178
+ sort_order: parseInt(document.getElementById('bp-sort').value, 10) || 0,
179
+ is_active: document.getElementById('bp-active').checked,
180
+ };
181
+
182
+ try {
183
+ const url = isNew ? '/admin/api/billing/plans' : `/admin/api/billing/plans/${encodeURIComponent(id)}`;
184
+ const method = isNew ? 'POST' : 'PUT';
185
+ const r = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
186
+ const data = await r.json();
187
+ if (!r.ok) { alert(data.error || 'Failed to save plan.'); return; }
188
+ billingClosePlanModal();
189
+ await loadBilling();
190
+ } catch (err) {
191
+ alert('Error: ' + err.message);
192
+ }
193
+ }
194
+
195
+ async function billingDeletePlan(planId) {
196
+ if (!confirm(`Delete plan "${planId}"? It will be deactivated (not hard-deleted).`)) return;
197
+ try {
198
+ const r = await fetch(`/admin/api/billing/plans/${encodeURIComponent(planId)}`, { method: 'DELETE' });
199
+ if (!r.ok) { const d = await r.json(); alert(d.error || 'Failed.'); return; }
200
+ await loadBilling();
201
+ } catch (err) {
202
+ alert('Error: ' + err.message);
203
+ }
204
+ }
205
+
206
+ async function loadBillingSubscriptions() {
207
+ const statusFilter = document.getElementById('billing-sub-status-filter')?.value || '';
208
+ const el = document.getElementById('billing-subs-content');
209
+ el.innerHTML = '<div class="empty"><span class="spinner"></span></div>';
210
+ try {
211
+ const params = new URLSearchParams({ limit: BILLING_SUBS_LIMIT, offset: _billingSubsOffset });
212
+ if (statusFilter) params.set('status', statusFilter);
213
+ const r = await fetch('/admin/api/billing/subscriptions?' + params);
214
+ if (!r.ok) { el.innerHTML = '<div class="empty">Failed to load subscriptions.</div>'; return; }
215
+ const data = await r.json();
216
+ renderSubsTable(data.subscriptions, data.total);
217
+ } catch {
218
+ el.innerHTML = '<div class="empty">Failed to load subscriptions.</div>';
219
+ }
220
+ }
221
+
222
+ function renderSubsTable(rows, total) {
223
+ const el = document.getElementById('billing-subs-content');
224
+ if (!rows.length) { el.innerHTML = '<div class="empty">No subscriptions found.</div>'; return; }
225
+ el.innerHTML = `
226
+ <table class="data-table">
227
+ <thead><tr>
228
+ <th>User</th><th>Plan</th><th>Status</th><th>Period end</th><th>Actions</th>
229
+ </tr></thead>
230
+ <tbody>${rows.map(subRowHtml).join('')}</tbody>
231
+ </table>`;
232
+
233
+ const pag = document.getElementById('billing-subs-pagination');
234
+ const hasPrev = _billingSubsOffset > 0;
235
+ const hasNext = _billingSubsOffset + BILLING_SUBS_LIMIT < total;
236
+ pag.innerHTML = `
237
+ <span style="color:var(--muted);font-size:13px">${total} total</span>
238
+ ${hasPrev ? `<button class="btn btn-sm" onclick="_billingSubsOffset-=${BILLING_SUBS_LIMIT};loadBillingSubscriptions()">← Prev</button>` : ''}
239
+ ${hasNext ? `<button class="btn btn-sm" onclick="_billingSubsOffset+=${BILLING_SUBS_LIMIT};loadBillingSubscriptions()">Next →</button>` : ''}`;
240
+ }
241
+
242
+ function subRowHtml(sub) {
243
+ const userLabel = esc(sub.display_name || sub.username || String(sub.user_id));
244
+ const email = sub.email ? `<br><small style="color:var(--muted)">${esc(sub.email)}</small>` : '';
245
+ const statusColor = { active: 'var(--success)', trialing: 'var(--warning, #f59e0b)', past_due: 'var(--error)', canceled: 'var(--muted)' }[sub.status] || 'var(--muted)';
246
+ const periodEnd = sub.current_period_end ? sub.current_period_end.slice(0, 10) : '—';
247
+ return `
248
+ <tr>
249
+ <td>${userLabel}${email}</td>
250
+ <td>${esc(sub.plan_name)}</td>
251
+ <td><span style="color:${statusColor}">${esc(sub.status)}</span></td>
252
+ <td>${periodEnd}</td>
253
+ <td><button class="btn btn-sm" onclick="billingOverrideSub(${sub.user_id})">Override</button></td>
254
+ </tr>`;
255
+ }
256
+
257
+ async function billingOverrideSub(userId) {
258
+ if (!_billingPlans.length) { alert('No plans available.'); return; }
259
+ const options = _billingPlans.filter((p) => p.is_active).map((p) => `${p.id} — ${p.name}`).join('\n');
260
+ const planId = prompt(`Enter plan ID to assign:\n\n${options}`);
261
+ if (!planId) return;
262
+ try {
263
+ const r = await fetch(`/admin/api/billing/users/${userId}/subscription`, {
264
+ method: 'POST',
265
+ headers: { 'Content-Type': 'application/json' },
266
+ body: JSON.stringify({ planId: planId.trim() }),
267
+ });
268
+ const data = await r.json();
269
+ if (!r.ok) { alert(data.error || 'Failed.'); return; }
270
+ await loadBillingSubscriptions();
271
+ } catch (err) {
272
+ alert('Error: ' + err.message);
273
+ }
274
+ }
275
+
276
+ // ── Helpers ───────────────────────────────────────────────────────────────────
277
+
278
+ function esc(str) {
279
+ return String(str ?? '')
280
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
281
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
282
+ }
283
+
284
+ function escAttr(str) {
285
+ return esc(str);
286
+ }
287
+
288
+ function fmtTokens(n) {
289
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
290
+ if (n >= 1_000) return (n / 1_000).toFixed(0) + 'K';
291
+ return String(n);
292
+ }
@@ -900,6 +900,14 @@
900
900
  Users
901
901
  </button>
902
902
 
903
+ <button class="nav-item" id="nav-billing" data-page="billing" onclick="showPage('billing',this)" style="display:none">
904
+ <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
905
+ <path d="M4 4a2 2 0 00-2 2v1h16V6a2 2 0 00-2-2H4z"/>
906
+ <path fill-rule="evenodd" d="M18 9H2v5a2 2 0 002 2h12a2 2 0 002-2V9zM4 13a1 1 0 011-1h1a1 1 0 110 2H5a1 1 0 01-1-1zm5-1a1 1 0 100 2h1a1 1 0 100-2H9z" clip-rule="evenodd"/>
907
+ </svg>
908
+ Billing
909
+ </button>
910
+
903
911
  <button class="nav-item" data-page="sql" onclick="showPage('sql',this)">
904
912
  <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
905
913
  <path fill-rule="evenodd" d="M3 5a2 2 0 012-2h10a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2V5zm11 1H6v8l4-2 4 2V6z" clip-rule="evenodd"/>
@@ -1189,6 +1197,34 @@
1189
1197
  </div>
1190
1198
  </section>
1191
1199
 
1200
+ <section id="page-billing" class="page" aria-label="Billing">
1201
+ <div class="page-header"><h1>Billing</h1></div>
1202
+ <div class="cards">
1203
+ <div class="card">
1204
+ <div class="card-title">Subscription Plans</div>
1205
+ <div style="margin-bottom:12px">
1206
+ <button class="btn btn-primary" onclick="billingShowNewPlanForm()">+ New Plan</button>
1207
+ </div>
1208
+ <div id="billing-plans-content"><div class="empty"><span class="spinner"></span></div></div>
1209
+ </div>
1210
+ <div class="card">
1211
+ <div class="card-title">User Subscriptions</div>
1212
+ <div style="margin-bottom:12px;display:flex;gap:8px;align-items:center">
1213
+ <select id="billing-sub-status-filter" onchange="loadBillingSubscriptions()" style="padding:6px 10px;border-radius:6px;border:1px solid var(--border);background:var(--input-bg);color:var(--text)">
1214
+ <option value="">All statuses</option>
1215
+ <option value="active">Active</option>
1216
+ <option value="trialing">Trialing</option>
1217
+ <option value="past_due">Past due</option>
1218
+ <option value="canceled">Canceled</option>
1219
+ </select>
1220
+ <button class="btn" onclick="loadBillingSubscriptions()">Refresh</button>
1221
+ </div>
1222
+ <div id="billing-subs-content"><div class="empty"><span class="spinner"></span></div></div>
1223
+ <div id="billing-subs-pagination" style="margin-top:12px;display:flex;gap:8px;align-items:center"></div>
1224
+ </div>
1225
+ </div>
1226
+ </section>
1227
+
1192
1228
  </main>
1193
1229
 
1194
1230
  <script src="/admin/admin.js"></script>
@@ -1196,5 +1232,6 @@
1196
1232
  <script src="/admin/users.js"></script>
1197
1233
  <script src="/admin/sql.js"></script>
1198
1234
  <script src="/admin/access.js"></script>
1235
+ <script src="/admin/billing.js"></script>
1199
1236
  </body>
1200
1237
  </html>
@@ -1328,6 +1328,7 @@ for (const col of [
1328
1328
  "ALTER TABLE memory_facts ADD COLUMN invalidated_at TEXT",
1329
1329
  "ALTER TABLE memory_facts ADD COLUMN status TEXT DEFAULT 'active'",
1330
1330
  "ALTER TABLE memory_facts ADD COLUMN supersedes_fact_id TEXT",
1331
+ "ALTER TABLE users ADD COLUMN billing_override_plan_id TEXT",
1331
1332
  ]) {
1332
1333
  try { db.exec(col); } catch { /* column already exists */ }
1333
1334
  }
@@ -1879,7 +1880,7 @@ function migrateIntegrationProviderConfigsTable() {
1879
1880
  }
1880
1881
 
1881
1882
  function migrateIntegrationSecretStorage() {
1882
- try {
1883
+ const migrate = db.transaction(() => {
1883
1884
  const connectionRows = db
1884
1885
  .prepare('SELECT id, credentials_json FROM integration_connections')
1885
1886
  .all();
@@ -1891,11 +1892,7 @@ function migrateIntegrationSecretStorage() {
1891
1892
  if (!current || isEncryptedValue(current)) continue;
1892
1893
  updateConnection.run(encryptValue(current), row.id);
1893
1894
  }
1894
- } catch {
1895
- // Preserve startup even if a row cannot be re-encrypted.
1896
- }
1897
1895
 
1898
- try {
1899
1896
  const stateRows = db
1900
1897
  .prepare('SELECT id, code_verifier FROM integration_oauth_states')
1901
1898
  .all();
@@ -1907,11 +1904,7 @@ function migrateIntegrationSecretStorage() {
1907
1904
  if (!current || isEncryptedValue(current)) continue;
1908
1905
  updateState.run(encryptValue(current), row.id);
1909
1906
  }
1910
- } catch {
1911
- // Preserve startup even if a row cannot be re-encrypted.
1912
- }
1913
1907
 
1914
- try {
1915
1908
  const providerRows = db
1916
1909
  .prepare('SELECT id, config_json FROM integration_provider_configs')
1917
1910
  .all();
@@ -1923,8 +1916,13 @@ function migrateIntegrationSecretStorage() {
1923
1916
  if (!current || isEncryptedValue(current)) continue;
1924
1917
  updateProviderConfig.run(encryptValue(current), row.id);
1925
1918
  }
1926
- } catch {
1927
- // Preserve startup even if a row cannot be re-encrypted.
1919
+ });
1920
+
1921
+ try {
1922
+ migrate();
1923
+ } catch (err) {
1924
+ console.error('[DB] Integration secret migration failed — startup cannot continue with partially migrated secrets:', err.message);
1925
+ throw err;
1928
1926
  }
1929
1927
  }
1930
1928
 
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ const Sqlite = require('better-sqlite3');
4
+ const { DATA_DIR } = require('../../runtime/paths');
5
+
6
+ const sessionsDb = new Sqlite(`${DATA_DIR}/sessions.db`);
7
+
8
+ module.exports = sessionsDb;
@@ -1,15 +1,13 @@
1
1
  'use strict';
2
2
 
3
3
  const session = require('express-session');
4
- const Sqlite = require('better-sqlite3');
5
4
  const SQLiteStore = require('better-sqlite3-session-store')(session);
6
5
  const helmet = require('helmet');
7
6
  const cors = require('cors');
8
- const { DATA_DIR } = require('../../runtime/paths');
9
7
  const { logRequestSummary } = require('../utils/logger');
10
8
  const { getSessionSecret } = require('../services/account/session_secret');
11
9
 
12
- const sessionsDb = new Sqlite(`${DATA_DIR}/sessions.db`);
10
+ const sessionsDb = require('../db/sessions_db');
13
11
  const LEGACY_SESSION_EXPIRE_FALLBACK = 0;
14
12
 
15
13
  function boolEnv(name, fallback = false) {
@@ -89,7 +87,9 @@ function buildHelmetOptions({ secureCookies }) {
89
87
  }
90
88
 
91
89
  return {
92
- strictTransportSecurity: false,
90
+ strictTransportSecurity: secureCookies
91
+ ? { maxAge: Number(process.env.NEOAGENT_HSTS_MAX_AGE ?? 86400), includeSubDomains: false }
92
+ : false,
93
93
  crossOriginOpenerPolicy: false,
94
94
  crossOriginResourcePolicy: { policy: 'same-site' },
95
95
  originAgentCluster: false,
@@ -48,6 +48,15 @@ function registerApiRoutes(app) {
48
48
  }
49
49
  }
50
50
 
51
+ // Billing routes are mounted conditionally — only when billing is enabled.
52
+ // The webhook must be mounted before express.json() consumes the raw body;
53
+ // billing_webhook.js applies express.raw() inline for its own route.
54
+ const { isBillingEnabled } = require('../services/billing/config');
55
+ if (isBillingEnabled()) {
56
+ app.use('/api/billing/webhook', require('../routes/billing_webhook'));
57
+ app.use('/api/billing', require('../routes/billing'));
58
+ }
59
+
51
60
  setupTelnyxWebhook(app);
52
61
 
53
62
  app.get('/api/health', requireAuth, (req, res) => {
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ const { isBillingEnabled } = require('../services/billing/config');
4
+
5
+ function requireBilling(req, res, next) {
6
+ if (!isBillingEnabled()) {
7
+ return res.status(404).json({ error: 'Billing is not enabled on this server.' });
8
+ }
9
+ next();
10
+ }
11
+
12
+ module.exports = { requireBilling };
@@ -1 +1 @@
1
- 504d057957ef960afb5bd462407c2c9f
1
+ dc90ac758721f68d16ff899fbce82971
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"77e2e94772b6eb43759e34ed1ad7da4674e19c
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "4074570140" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "3960040413" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });
@@ -135597,7 +135597,7 @@ r===$&&A.b()
135597
135597
  p.push(A.jV(q,A.jd(!1,new A.a_(B.uM,A.db(new A.cC(B.jA,new A.a89(r,q),q),q,q),q),!1,B.I,!0),q,q,0,0,0,q))}r=!1
135598
135598
  if(!s.ay)if(!s.ch){r=s.e
135599
135599
  r===$&&A.b()
135600
- r=B.b.u("mqin9jwm-c5e3286").length!==0&&r.b}if(r){r=s.d
135600
+ r=B.b.u("mqjy0ig5-61a79c0").length!==0&&r.b}if(r){r=s.d
135601
135601
  r===$&&A.b()
135602
135602
  r=r.aP&&!r.ai?84:0
135603
135603
  s=s.e
@@ -141319,7 +141319,7 @@ $S:0}
141319
141319
  A.a_p.prototype={}
141320
141320
  A.T7.prototype={
141321
141321
  nd(a){var s=this
141322
- if(B.b.u("mqin9jwm-c5e3286").length===0||s.a!=null)return
141322
+ if(B.b.u("mqjy0ig5-61a79c0").length===0||s.a!=null)return
141323
141323
  s.B_()
141324
141324
  s.a=A.ot(B.S2,new A.bde(s))},
141325
141325
  B_(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f
@@ -141337,7 +141337,7 @@ if(!t.f.b(k)){s=1
141337
141337
  break}i=J.a3(k,"buildId")
141338
141338
  h=i==null?null:B.b.u(J.p(i))
141339
141339
  j=h==null?"":h
141340
- if(J.bj(j)===0||J.e(j,"mqin9jwm-c5e3286")){s=1
141340
+ if(J.bj(j)===0||J.e(j,"mqjy0ig5-61a79c0")){s=1
141341
141341
  break}n.b=!0
141342
141342
  n.F()
141343
141343
  p=2
@@ -141354,7 +141354,7 @@ case 2:return A.i(o.at(-1),r)}})
141354
141354
  return A.k($async$B_,r)},
141355
141355
  vI(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1
141356
141356
  var $async$vI=A.h(function(a2,a3){if(a2===1){o.push(a3)
141357
- s=p}for(;;)switch(s){case 0:if(B.b.u("mqin9jwm-c5e3286").length===0||n.c){s=1
141357
+ s=p}for(;;)switch(s){case 0:if(B.b.u("mqjy0ig5-61a79c0").length===0||n.c){s=1
141358
141358
  break}n.c=!0
141359
141359
  n.F()
141360
141360
  p=4