bulltrackers-module 1.0.997 → 1.0.999

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.
@@ -3,11 +3,22 @@
3
3
  * Exports the API factory for use in index.js
4
4
  */
5
5
 
6
+ const http = require('http');
6
7
  const { createApiV3App, initialize, getServices, resetServices } = require('./core-api');
8
+ const { attachVerificationWebSocketServer } = require('./websocket/verification');
9
+
10
+ function createApiV3Server(apiConfig, dependencies) {
11
+ const app = createApiV3App(apiConfig, dependencies);
12
+ const server = http.createServer(app);
13
+ attachVerificationWebSocketServer(server, dependencies, apiConfig);
14
+ return { app, server };
15
+ }
7
16
 
8
17
  module.exports = {
9
18
  createApiV3App,
19
+ createApiV3Server,
10
20
  initialize,
11
21
  getServices,
12
- resetServices
22
+ resetServices,
23
+ attachVerificationWebSocketServer
13
24
  };
@@ -11,11 +11,13 @@ const { publishUploadStreamEvent, readUploadStream } = require('../middleware/up
11
11
  const { getRedis, checkVerificationUserLimit } = require('../middleware/upstashRateLimit');
12
12
  const { v4: uuidv4 } = require('uuid');
13
13
  const { writeComputationNotification } = require('../services/computationNotificationWriter');
14
- const { buildUserComputationContract } = require('../services/computationContract');
15
- const { buildSqlBlockFromRequires } = require('../../computation-system-v3/dev-tools/annotate-sql-core');
16
-
17
- const UPLOAD_SESSION_PREFIX = 'upload:session:';
18
- const UPLOAD_SESSION_TTL = 300;
14
+ const { buildUserComputationContract } = require('../services/computationContract');
15
+ const { buildSqlBlockFromRequires } = require('../../computation-system-v3/dev-tools/annotate-sql-core');
16
+
17
+ const UPLOAD_SESSION_PREFIX = 'upload:session:';
18
+ const UPLOAD_SESSION_TTL = 300;
19
+ const VERIFY_TICKET_PREFIX = 'verify:ticket:';
20
+ const VERIFY_TICKET_TTL_SECONDS = 30;
19
21
 
20
22
  // Helper to create router
21
23
  const createComputationsRouter = () => {
@@ -60,15 +62,79 @@ const createComputationsRouter = () => {
60
62
  }
61
63
  });
62
64
 
63
- // GET /contract: canonical frontend contract for user computations
64
- router.get('/contract', requireVerifiedUser, async (req, res, next) => {
65
- try {
66
- const contract = buildUserComputationContract();
67
- res.json({ success: true, data: contract });
68
- } catch (error) {
69
- next(error);
70
- }
71
- });
65
+ // GET /contract: canonical frontend contract for user computations
66
+ router.head('/contract', requireVerifiedUser, async (req, res, next) => {
67
+ try {
68
+ const contract = buildUserComputationContract();
69
+ res.setHeader('x-bt-sdk-version', contract.sdkVersion || '');
70
+ res.setHeader('x-bt-contract-hash', contract.contractHash || '');
71
+ res.setHeader('x-bt-generated-at', contract.generatedAt || '');
72
+ res.status(200).end();
73
+ } catch (error) {
74
+ next(error);
75
+ }
76
+ });
77
+
78
+ router.get('/contract', requireVerifiedUser, async (req, res, next) => {
79
+ try {
80
+ const contract = buildUserComputationContract();
81
+ res.json({ success: true, data: contract });
82
+ } catch (error) {
83
+ next(error);
84
+ }
85
+ });
86
+
87
+ // POST /verify/start: issue a single-use verification ticket
88
+ router.post('/verify/start', requireVerifiedUser, async (req, res, next) => {
89
+ try {
90
+ const userId = req.targetUserId;
91
+ const { code, name } = req.body || {};
92
+ if (!code || typeof code !== 'string') {
93
+ return res.status(400).json({ error: 'Missing or invalid "code"' });
94
+ }
95
+ if (!name || typeof name !== 'string') {
96
+ return res.status(400).json({ error: 'Missing or invalid "name"' });
97
+ }
98
+ const limitCheck = await checkVerificationUserLimit(userId);
99
+ const { allowed, retryAfterSeconds, remaining, limit } = limitCheck;
100
+ if (!allowed) {
101
+ return res.status(429).json({
102
+ success: false,
103
+ error: 'Too many verifications',
104
+ message: 'Verification rate limit exceeded. Please try again later.',
105
+ retryAfterSeconds: retryAfterSeconds || 60,
106
+ remainingVerifications: remaining ?? 0,
107
+ limit
108
+ });
109
+ }
110
+ const redis = getRedis();
111
+ if (!redis) {
112
+ return res.status(503).json({ error: 'Verification ticket unavailable' });
113
+ }
114
+ const verificationId = uuidv4();
115
+ const ticket = uuidv4();
116
+ const payload = {
117
+ code,
118
+ name,
119
+ userId,
120
+ verificationId,
121
+ requestId: req.requestId || null,
122
+ createdAt: new Date().toISOString()
123
+ };
124
+ await redis.set(VERIFY_TICKET_PREFIX + ticket, JSON.stringify(payload), 'EX', VERIFY_TICKET_TTL_SECONDS);
125
+ const expiresAt = new Date(Date.now() + VERIFY_TICKET_TTL_SECONDS * 1000).toISOString();
126
+ res.json({
127
+ success: true,
128
+ ticket,
129
+ verificationId,
130
+ expiresAt,
131
+ remainingVerifications: typeof remaining === 'number' ? remaining : undefined,
132
+ limit
133
+ });
134
+ } catch (error) {
135
+ next(error);
136
+ }
137
+ });
72
138
 
73
139
  // GET /: list user's computations (metadata only). Includes status, schedule; nextRun/lastRun when available from scheduler.
74
140
  router.get('/', requireVerifiedUser, async (req, res, next) => {
@@ -3,18 +3,110 @@
3
3
  * Backend verification remains the source of truth.
4
4
  */
5
5
 
6
+ const fs = require('fs');
7
+ const path = require('path');
6
8
  const systemConfig = require('../../computation-system-v3/config/bulltrackers.config');
7
9
  const { getForbiddenIdentifiers, getAllowedCtxKeys } = require('../../computation-system-v3/framework/core/manifest/AstValidator');
8
10
  const { getRegistry } = require('../../computation-system-v3/framework/metadata/FunctionRegistry');
11
+ const { hashContract } = require('../../computation-system-v3/framework/metadata/ContractHash');
12
+ const { ALL_VERIFICATION_CODES, ALL_PRODUCTION_CODES, ERROR_CODE_DOC } = require('../../computation-system-v3/verification/ErrorCodes');
13
+
14
+ const SDK_VERSION = '2026-03-13';
15
+ const ERROR_DOC_PATH = path.join(__dirname, '../../computation-system-v3/docs/16-ERROR-CODE-REFERENCE.md');
16
+
17
+ function parseErrorCodeExamples() {
18
+ if (!fs.existsSync(ERROR_DOC_PATH)) return {};
19
+ try {
20
+ const content = fs.readFileSync(ERROR_DOC_PATH, 'utf8');
21
+ const lines = content.split('\n');
22
+ const examples = {};
23
+ for (const line of lines) {
24
+ if (!line.trim().startsWith('|')) continue;
25
+ const cells = line.split('|').map(c => c.trim()).filter(Boolean);
26
+ if (cells.length < 3) continue;
27
+ const code = cells[0];
28
+ const example = cells[2];
29
+ if (code && code !== 'Code' && !/^-+$/.test(code) && example) {
30
+ examples[code] = example;
31
+ }
32
+ }
33
+ return examples;
34
+ } catch {
35
+ return {};
36
+ }
37
+ }
38
+
39
+ function buildSdk(registry) {
40
+ const modules = {};
41
+ const functionsById = registry.functionsById || {};
42
+
43
+ const fnList = Object.values(functionsById).sort((a, b) => String(a.id).localeCompare(String(b.id)));
44
+ for (const fn of fnList) {
45
+ if (!fn || !fn.moduleName || !fn.functionName) continue;
46
+ if (!modules[fn.moduleName]) {
47
+ modules[fn.moduleName] = {
48
+ name: fn.moduleName,
49
+ moduleType: fn.moduleType || 'lib',
50
+ functions: []
51
+ };
52
+ }
53
+ modules[fn.moduleName].functions.push({
54
+ id: fn.id,
55
+ name: fn.functionName,
56
+ moduleType: fn.moduleType || 'lib',
57
+ description: fn.description || '',
58
+ example: fn.example || '',
59
+ scope: fn.scope,
60
+ tables: fn.tables || [],
61
+ fields: fn.fields || [],
62
+ lookback: fn.lookback || 0,
63
+ filterRequired: !!fn.filterRequired,
64
+ uses: fn.uses || '',
65
+ visibility: fn.visibility || 'public',
66
+ contract: fn.contract || null
67
+ });
68
+ }
69
+
70
+ for (const mod of Object.values(modules)) {
71
+ mod.functions.sort((a, b) => String(a.name).localeCompare(String(b.name)));
72
+ }
73
+
74
+ const wrapperFlat = Object.values(modules)
75
+ .filter(m => m.moduleType === 'wrapper')
76
+ .flatMap(m => (m.functions || []).map(f => f.name));
77
+
78
+ return {
79
+ modules,
80
+ wrapperFlat
81
+ };
82
+ }
9
83
 
10
84
  function buildUserComputationContract() {
11
85
  const libModules = Array.isArray(systemConfig?.skills?.lib?.allowedModules)
12
86
  ? [...systemConfig.skills.lib.allowedModules]
13
87
  : [];
14
88
  const registry = getRegistry();
89
+ const errorExamples = parseErrorCodeExamples();
90
+ const sdk = buildSdk(registry);
15
91
 
16
- return {
17
- version: '2026-03-08',
92
+ const errorCodes = {
93
+ verification: [...ALL_VERIFICATION_CODES],
94
+ production: [...ALL_PRODUCTION_CODES],
95
+ docs: Object.fromEntries(
96
+ Object.entries(ERROR_CODE_DOC).map(([code, doc]) => [
97
+ code,
98
+ {
99
+ description: doc.description || '',
100
+ remediation: doc.remediation || '',
101
+ example: errorExamples[code] || ''
102
+ }
103
+ ])
104
+ )
105
+ };
106
+
107
+ const baseContract = {
108
+ version: '2026-03-13',
109
+ sdkVersion: SDK_VERSION,
18
110
  exportShape: {
19
111
  root: 'module.exports',
20
112
  required: ['config', 'process']
@@ -45,9 +137,10 @@ function buildUserComputationContract() {
45
137
  },
46
138
  requiresTableKeys: Object.keys(systemConfig?.tables || {}),
47
139
  metadata: {
48
- generatedAt: registry.generatedAt,
49
140
  modules: Object.keys(registry.modules || {}),
50
- functions: Object.values(registry.functionsById || {}).map((f) => ({
141
+ functions: Object.values(registry.functionsById || {})
142
+ .sort((a, b) => String(a.id).localeCompare(String(b.id)))
143
+ .map((f) => ({
51
144
  id: f.id,
52
145
  moduleType: f.moduleType,
53
146
  moduleName: f.moduleName,
@@ -58,9 +151,31 @@ function buildUserComputationContract() {
58
151
  lookback: f.lookback,
59
152
  filterRequired: f.filterRequired,
60
153
  uses: f.uses,
154
+ description: f.description || '',
155
+ example: f.example || '',
61
156
  visibility: f.visibility
62
157
  }))
63
- }
158
+ },
159
+ sdk: {
160
+ modules: sdk.modules,
161
+ wrapperFlat: sdk.wrapperFlat,
162
+ ctx: { allowedKeys: Array.from(getAllowedCtxKeys()) },
163
+ security: { forbiddenIdentifiers: Array.from(getForbiddenIdentifiers()) },
164
+ tables: Object.keys(systemConfig?.tables || {})
165
+ },
166
+ errorCodes
167
+ };
168
+
169
+ const contractHash = hashContract(baseContract);
170
+
171
+ return {
172
+ ...baseContract,
173
+ metadata: {
174
+ ...baseContract.metadata,
175
+ generatedAt: registry.generatedAt
176
+ },
177
+ generatedAt: registry.generatedAt,
178
+ contractHash
64
179
  };
65
180
  }
66
181
 
@@ -0,0 +1,320 @@
1
+ 'use strict';
2
+ /**
3
+ * @fileoverview Verification WebSocket server for ticketed sandbox logs.
4
+ */
5
+
6
+ const { WebSocketServer } = require('ws');
7
+ const { getAuth } = require('firebase-admin/auth');
8
+ const { z } = require('zod');
9
+ const { Manifest } = require('../../computation-system-v3/framework/core/Manifest');
10
+ const { runVerification } = require('../../computation-system-v3/verification/runVerification');
11
+ const { getRedis } = require('../middleware/upstashRateLimit');
12
+ const { isDevTenantRequest } = require('../middleware/identity');
13
+ const { initialize } = require('../core-api');
14
+
15
+ const VERIFY_TICKET_PREFIX = 'verify:ticket:';
16
+ const VERIFY_WS_PATH = '/computations/verify/ws';
17
+ const MAX_LOG_BYTES = 1024 * 1024;
18
+ const MAX_MESSAGES_PER_SECOND = 50;
19
+
20
+ const LogSchema = z.object({
21
+ level: z.enum(['info', 'warn', 'error', 'system']),
22
+ text: z.string().max(2000),
23
+ timestamp: z.string()
24
+ });
25
+ const StageSchema = z.object({
26
+ id: z.string(),
27
+ status: z.enum(['passed', 'failed', 'running', 'pending']),
28
+ message: z.string().optional(),
29
+ errorCode: z.string().optional()
30
+ });
31
+ const FinalSchema = z.object({
32
+ passed: z.boolean(),
33
+ errorCodes: z.array(z.string()).optional(),
34
+ errors: z.array(z.string()).optional()
35
+ });
36
+
37
+ async function resolveFirebaseUser(req) {
38
+ const authHeader = req.headers.authorization;
39
+ const testAuthHeader = req.headers['x-test-firebase-user'];
40
+ const allowTestAuth = process.env.ENABLE_TEST_AUTH === 'true' || process.env.NODE_ENV === 'test';
41
+
42
+ if (authHeader && authHeader.startsWith('Bearer ')) {
43
+ const token = authHeader.split('Bearer ')[1];
44
+ try {
45
+ const decodedToken = await getAuth().verifyIdToken(token);
46
+ return {
47
+ uid: decodedToken.uid,
48
+ email: decodedToken.email || null,
49
+ emailVerified: decodedToken.email_verified || false
50
+ };
51
+ } catch (_) {
52
+ if (!allowTestAuth) return null;
53
+ }
54
+ }
55
+
56
+ if (allowTestAuth && testAuthHeader) {
57
+ try {
58
+ let parsed;
59
+ try {
60
+ parsed = JSON.parse(testAuthHeader);
61
+ } catch {
62
+ parsed = testAuthHeader;
63
+ }
64
+ if (typeof parsed === 'object' && parsed !== null) {
65
+ return {
66
+ uid: String(parsed.uid),
67
+ email: parsed.email || null,
68
+ emailVerified: true
69
+ };
70
+ }
71
+ const val = String(parsed);
72
+ return {
73
+ uid: val.includes('@') ? 'mock_uid' : val,
74
+ email: val.includes('@') ? val : `user${val}@test.com`,
75
+ emailVerified: true
76
+ };
77
+ } catch (_) {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ return null;
83
+ }
84
+
85
+ function resolveTargetUserId(firebaseUser, userCid, req) {
86
+ const isDev = isDevTenantRequest(req);
87
+ if (isDev) {
88
+ if (userCid && String(userCid).startsWith('dev-')) {
89
+ return String(userCid);
90
+ }
91
+ const baseId = userCid || firebaseUser?.uid || 'anonymous';
92
+ return `dev_${baseId}`;
93
+ }
94
+ return userCid || firebaseUser?.uid || null;
95
+ }
96
+
97
+ function buildWsUrlBase(req) {
98
+ const host = req.headers.host || 'localhost';
99
+ return `http://${host}`;
100
+ }
101
+
102
+ function attachVerificationWebSocketServer(server, dependencies, apiConfig = {}) {
103
+ const wss = new WebSocketServer({ noServer: true });
104
+ let servicesPromise = null;
105
+
106
+ function getServices() {
107
+ if (!servicesPromise) {
108
+ servicesPromise = initialize({
109
+ bigquery: dependencies.bigquery,
110
+ firestore: dependencies.db,
111
+ storage: dependencies.storage,
112
+ pubsub: dependencies.pubsub,
113
+ logger: dependencies.logger
114
+ }, apiConfig);
115
+ }
116
+ return servicesPromise;
117
+ }
118
+
119
+ server.on('upgrade', async (req, socket, head) => {
120
+ try {
121
+ const url = new URL(req.url, buildWsUrlBase(req));
122
+ if (url.pathname !== VERIFY_WS_PATH) {
123
+ return;
124
+ }
125
+ wss.handleUpgrade(req, socket, head, (ws) => {
126
+ wss.emit('connection', ws, req);
127
+ });
128
+ } catch (e) {
129
+ socket.destroy();
130
+ }
131
+ });
132
+
133
+ wss.on('connection', async (ws, req) => {
134
+ let totalBytes = 0;
135
+ let msgWindowStart = Date.now();
136
+ let msgCount = 0;
137
+ const sendJson = (payload) => {
138
+ if (ws.readyState !== ws.OPEN) return;
139
+ const now = Date.now();
140
+ if (now - msgWindowStart >= 1000) {
141
+ msgWindowStart = now;
142
+ msgCount = 0;
143
+ }
144
+ msgCount += 1;
145
+ if (msgCount > MAX_MESSAGES_PER_SECOND) {
146
+ ws.close(1008, 'Rate limit exceeded');
147
+ return;
148
+ }
149
+ const json = JSON.stringify(payload);
150
+ totalBytes += Buffer.byteLength(json, 'utf8');
151
+ if (totalBytes > MAX_LOG_BYTES) {
152
+ try {
153
+ ws.send(JSON.stringify({
154
+ type: 'log',
155
+ level: 'system',
156
+ text: 'Log output exceeded allowed size. Closing connection.',
157
+ timestamp: new Date().toISOString()
158
+ }));
159
+ } catch (_) { }
160
+ ws.close(1008, 'Log quota exceeded');
161
+ return;
162
+ }
163
+ ws.send(json);
164
+ };
165
+
166
+ ws.on('message', () => {
167
+ ws.close(1008, 'Read-only verification stream');
168
+ });
169
+
170
+ try {
171
+ const url = new URL(req.url, buildWsUrlBase(req));
172
+ const ticket = url.searchParams.get('ticket');
173
+ if (!ticket) {
174
+ ws.close(1008, 'Missing ticket');
175
+ return;
176
+ }
177
+
178
+ const redis = getRedis();
179
+ if (!redis) {
180
+ ws.close(1011, 'Ticket store unavailable');
181
+ return;
182
+ }
183
+
184
+ const raw = await redis.get(VERIFY_TICKET_PREFIX + ticket);
185
+ if (!raw) {
186
+ ws.close(1008, 'Invalid or expired ticket');
187
+ return;
188
+ }
189
+ await redis.del(VERIFY_TICKET_PREFIX + ticket);
190
+
191
+ const payload = JSON.parse(raw);
192
+ const firebaseUser = await resolveFirebaseUser(req);
193
+ if (!firebaseUser) {
194
+ ws.close(1008, 'Authentication required');
195
+ return;
196
+ }
197
+
198
+ const services = await getServices();
199
+ let userCid = null;
200
+ if (firebaseUser.email) {
201
+ try {
202
+ const lookup = await services.authService.lookupCidByEmail(firebaseUser.email, { isDevTenant: isDevTenantRequest(req) });
203
+ if (lookup?.cid != null) userCid = String(lookup.cid);
204
+ } catch (_) { }
205
+ }
206
+ const targetUserId = resolveTargetUserId(firebaseUser, userCid, req);
207
+ if (!targetUserId || targetUserId !== payload.userId) {
208
+ ws.close(1008, 'Ticket user mismatch');
209
+ return;
210
+ }
211
+
212
+ const verificationId = payload.verificationId || '';
213
+ const startedAt = new Date().toISOString();
214
+ const auditRef = services.db
215
+ ? services.db.collection('users').doc(targetUserId)
216
+ .collection('verification_sessions').doc(verificationId)
217
+ : null;
218
+
219
+ if (auditRef) {
220
+ await auditRef.set({
221
+ verificationId,
222
+ userId: targetUserId,
223
+ name: payload.name || null,
224
+ startedAt,
225
+ requestId: payload.requestId || null,
226
+ status: 'running'
227
+ }, { merge: true });
228
+ }
229
+
230
+ const stageEmitter = (stagePayload) => {
231
+ const parsed = StageSchema.safeParse(stagePayload);
232
+ if (!parsed.success) {
233
+ sendJson({
234
+ type: 'log',
235
+ level: 'system',
236
+ text: 'Verification emitted an invalid stage update.',
237
+ timestamp: new Date().toISOString()
238
+ });
239
+ return;
240
+ }
241
+ sendJson({ type: 'stage', ...parsed.data });
242
+ };
243
+
244
+ const logEmitter = (entry) => {
245
+ let text = String(entry.message || '');
246
+ if (text.length > 2000) text = text.slice(0, 2000);
247
+ const logPayload = {
248
+ level: entry.level === 'info' || entry.level === 'warn' || entry.level === 'error' ? entry.level : 'info',
249
+ text,
250
+ timestamp: new Date(entry.ts || Date.now()).toISOString()
251
+ };
252
+ const parsed = LogSchema.safeParse(logPayload);
253
+ if (!parsed.success) {
254
+ sendJson({
255
+ type: 'log',
256
+ level: 'system',
257
+ text: 'Verification emitted an invalid log entry.',
258
+ timestamp: new Date().toISOString()
259
+ });
260
+ return;
261
+ }
262
+ sendJson({ type: 'log', ...parsed.data });
263
+ };
264
+
265
+ const userComputationNames = [];
266
+ try {
267
+ const ref = services.db.collection('users').doc(targetUserId).collection('computations');
268
+ const snap = await ref.get();
269
+ snap.docs.forEach(d => {
270
+ const n = d.data()?.name || d.id;
271
+ if (n) userComputationNames.push(Manifest.normalize(n));
272
+ });
273
+ } catch (_) { }
274
+
275
+ const result = await runVerification(payload.code, payload.name, targetUserId, {
276
+ userComputationNames,
277
+ logger: services.logger || console,
278
+ uploadSessionId: verificationId || 'verify-session',
279
+ onStageProgress: (_sid, stage) => stageEmitter(stage),
280
+ onLog: (entry) => logEmitter(entry)
281
+ });
282
+
283
+ const finalPayload = {
284
+ passed: result.passed === true,
285
+ errorCodes: result.errorCodes || undefined,
286
+ errors: result.errors || undefined
287
+ };
288
+ const finalParsed = FinalSchema.safeParse(finalPayload);
289
+ if (finalParsed.success) {
290
+ sendJson({ type: 'final', ...finalParsed.data });
291
+ }
292
+
293
+ if (auditRef) {
294
+ await auditRef.set({
295
+ finishedAt: new Date().toISOString(),
296
+ status: result.passed === true ? 'passed' : 'failed',
297
+ errorCodes: result.errorCodes || null
298
+ }, { merge: true });
299
+ }
300
+
301
+ ws.close(1000, 'Verification complete');
302
+ } catch (err) {
303
+ try {
304
+ ws.send(JSON.stringify({
305
+ type: 'log',
306
+ level: 'system',
307
+ text: 'Verification failed unexpectedly.',
308
+ timestamp: new Date().toISOString()
309
+ }));
310
+ } catch (_) { }
311
+ ws.close(1011, 'Verification error');
312
+ }
313
+ });
314
+
315
+ return wss;
316
+ }
317
+
318
+ module.exports = {
319
+ attachVerificationWebSocketServer
320
+ };
@@ -20,3 +20,4 @@ One-line description and primary audience for each doc. Source of truth: codebas
20
20
  | [13-OBSERVABILITY-LOG-SINKS](13-OBSERVABILITY-LOG-SINKS.md) | Telemetry sink implementation, Logs Explorer and BigQuery query recipes, revision-based charting and dashboard-as-code workflow | Developer, ops |
21
21
  | [14-HOW-EXECUTION-WORKS](14-HOW-EXECUTION-WORKS.md) | Dependencies and execution order (DAG), what runs when, planner and dispatcher | Users, developer |
22
22
  | [15-CREDITS-AND-BILLING](15-CREDITS-AND-BILLING.md) | How credits are consumed, balance and negative balance, cost of 7-day overwrite | Users, developer |
23
+ | [17-IDE-VERIFICATION-PLAN](17-IDE-VERIFICATION-PLAN.md) | Studio IDE DX plan: templates, IntelliSense, verification terminal, websocket security | Developer, frontend, ops |
@@ -0,0 +1,248 @@
1
+ # IDE verification plan (Studio DX)
2
+
3
+ This document formalizes the plan to make computation authoring in the VS Code Web Studio fast, safe, and guided. It covers the IntelliSense experience, file templating, and the verification sandbox terminal and websocket security.
4
+
5
+ ## Goals
6
+
7
+ - Make authoring computations feel effortless with rich IntelliSense, doc hovers, and safe diagnostics.
8
+ - Provide a read-only verification terminal that streams sandbox logs without leaking internal data.
9
+ - Enforce strict security controls for websocket ingress and log egress.
10
+ - Keep frontend and backend contracts explicit and testable.
11
+
12
+ ## Non-goals
13
+
14
+ - Replacing the computation-system-v3 execution engine.
15
+ - Providing a general purpose shell inside the IDE.
16
+ - Requiring users to install local extensions or desktop VS Code.
17
+
18
+ ## Scope by repository
19
+
20
+ Computation system v3
21
+ - Define the canonical SDK surface for libs and wrappers.
22
+ - Provide a machine-readable SDK manifest for the frontend.
23
+ - Enforce structured log egress from sandbox execution.
24
+ - Add log validation and policy enforcement in verification websocket bridge.
25
+
26
+ API v3
27
+ - Provide ticketed verification start endpoint and auth enforcement.
28
+ - Broker websocket connection to the verification sandbox.
29
+ - Provide observable audit events for verification sessions.
30
+
31
+ Frontend web2.0 (VS Code Web build)
32
+ - Generate and inject `bulltrackers.d.ts` into the workspace.
33
+ - Auto-template new computation files in `/computations`.
34
+ - Provide custom completion and diagnostics based on libs, wrappers, and user code.
35
+ - Open a read-only pseudoterminal on verification and persist logs to `/output`.
36
+
37
+ ## Current implementation (code references)
38
+
39
+ Frontend web2.0
40
+ - Soft checks are regex-based in `Bulltrackers-Frontend/web2.0/ide-extension/src/extension.ts` (`doSoftChecks`).
41
+ - Verification runs via REST in `Bulltrackers-Frontend/web2.0/ide-extension/src/terminal.ts` using `verifyComputation` from `Bulltrackers-Frontend/web2.0/ide-extension/src/api.ts` (`POST /computations/verify`).
42
+ - Terminal output is sanitized with `Bulltrackers-Frontend/web2.0/ide-extension/src/sanitize.ts`.
43
+ - SDK placeholders live in `Bulltrackers-Frontend/web2.0/ide-extension/src/libsContent.ts` but are not wired into the filesystem provider.
44
+
45
+ Computation system v3
46
+ - Lib + wrapper injection occurs in `Bulltrackers-Backend/Core/bulltrackers-module/functions/computation-system-v3/framework/core/ContextBuilder.js` via `buildWrappers`.
47
+ - Contracts are loaded from `__contracts` in `Bulltrackers-Backend/Core/bulltrackers-module/functions/computation-system-v3/framework/core/ContractRegistry.js`.
48
+ - Contract metadata is flattened for frontend use in `Bulltrackers-Backend/Core/bulltrackers-module/functions/computation-system-v3/framework/metadata/FunctionRegistry.js`.
49
+ - AST validation rules are in `Bulltrackers-Backend/Core/bulltrackers-module/functions/computation-system-v3/framework/core/manifest/AstValidator.js` and `.../framework/core/Manifest.js`.
50
+ - Verification run output, log buffer quota, and error codes are in `Bulltrackers-Backend/Core/bulltrackers-module/functions/computation-system-v3/verification/runVerification.js` and `.../verification/ErrorCodes.js`.
51
+
52
+ API v3
53
+ - Canonical computation contract endpoint is `GET /computations/contract` in `Bulltrackers-Backend/Core/bulltrackers-module/functions/api-v3/routes/computations.js` using `.../services/computationContract.js`.
54
+
55
+ ## End-to-end flow (target)
56
+
57
+ 1. User creates a new `.js` file in `/computations`.
58
+ 2. Studio inserts the minimal computation template.
59
+ 3. TypeScript IntelliSense and custom completions guide authoring.
60
+ 4. User clicks Verify.
61
+ 5. Studio requests a single-use verification ticket from API v3.
62
+ 6. Studio opens a read-only terminal and connects to the verification websocket.
63
+ 7. Sandbox streams structured logs, validated and sanitized by the backend.
64
+ 8. Terminal remains open; logs are persisted to `/output/verification-<timestamp>.txt`.
65
+
66
+ ## Ambient intelligence (IntelliSense + templates)
67
+
68
+ ### SDK declaration file (`bulltrackers.d.ts`)
69
+
70
+ Plan
71
+ - On extension activation, write a generated `bulltrackers.d.ts` to the root workspace.
72
+ - Use JSDoc comments to power hover docs and parameter hints.
73
+ - Include `ComputationContext`, builtin types, and all libs/wrappers.
74
+
75
+ Source of truth
76
+ - Use the existing computation contract from API v3 (`GET /computations/contract`) and extend it as needed.
77
+ - Back contract metadata by `ContractRegistry` + `FunctionRegistry` so wrappers and libs stay in sync with injection.
78
+ - Backend is the canonical source of truth; frontend caches the manifest for performance and offline startup.
79
+ - Include explicit SDK version metadata: `sdkVersion`, `generatedAt`, and `contractHash` (checksum).
80
+
81
+ Cache invalidation policy
82
+ - Cache the contract locally on first fetch, keyed by `sdkVersion` + `contractHash`.
83
+ - On Studio startup, compare cached `sdkVersion`/`contractHash` with a lightweight `HEAD` or `GET /computations/contract` metadata fetch.
84
+ - If mismatch, refresh full contract and regenerate `bulltrackers.d.ts`.
85
+ - If network is unavailable, continue using cached contract and mark editor status as "offline contract".
86
+ - Optional: revalidate every 24 hours in the background to pick up backend SDK changes.
87
+
88
+ Required capabilities
89
+ - Autocomplete for namespace paths, function names, and parameter lists.
90
+ - Type errors when required params are missing or invalid types are passed.
91
+ - Warnings when a lib or wrapper symbol does not exist.
92
+
93
+ ### Auto-templating new computations
94
+
95
+ Trigger
96
+ - When a `.js` file is created under `/computations`, insert a minimal template if empty.
97
+
98
+ Template content
99
+ - `export const config = { name, requires, schedule }`
100
+ - `export function process(ctx) { /* user logic */ }`
101
+ - A small placeholder comment for context
102
+
103
+ Safety
104
+ - Only apply to brand-new or empty files.
105
+ - Do not overwrite user content.
106
+
107
+ ### Custom completion and diagnostics
108
+
109
+ Baseline
110
+ - TypeScript language service provides standard JS completions from `bulltrackers.d.ts`.
111
+
112
+ Enhancements
113
+ - A completion provider that prioritizes common libs/wrappers and recently used symbols.
114
+ - Completion suggestions that include short docstrings from the manifest.
115
+ - Diagnostics for invalid table/field names using bulltrackers config schema.
116
+
117
+ Local personalization
118
+ - Rank suggestions using symbols used in the current file and workspace.
119
+ - Optionally use a lightweight static indexer over `/computations`.
120
+
121
+ ## Verification terminal (frontend)
122
+
123
+ ### Read-only pseudoterminal
124
+
125
+ Requirements
126
+ - The terminal is read-only and does not forward user input to any backend.
127
+ - The terminal does not auto-close on websocket close.
128
+ - The terminal can replay and display past logs.
129
+
130
+ Implementation outline
131
+ - Create a `vscode.Pseudoterminal` and ignore `handleInput`.
132
+ - Connect to the verification websocket and stream logs into the terminal.
133
+ - Buffer logs and write them to `workspace:/output/verification-<timestamp>.txt`.
134
+
135
+ ### Log persistence
136
+
137
+ Behavior
138
+ - Ensure `/output` exists in the workspace.
139
+ - Write `verification-<timestamp>.txt` on websocket close or on terminal dispose (timestamp is UTC).
140
+ - Keep per-run uniqueness by timestamp (no overwrite).
141
+ - Enforce a 1 MB max log file size (truncate and append a final system line when exceeded).
142
+ - Retain logs indefinitely unless the user deletes them.
143
+ - Optional (if low effort): add a TTL cleanup command or scheduled cleanup to delete logs older than 7 days.
144
+
145
+ TTL cleanup (optional, low effort)
146
+ - Provide a manual command in the IDE extension (e.g., `bulltrackers.logs.cleanupVerification`).
147
+ - Implementation location: `Bulltrackers-Frontend/web2.0/ide-extension/src/extension.ts` (command registration + invocation).
148
+ - Implementation approach:
149
+ - List files in `workspace:/output/` and match `verification-*.txt`.
150
+ - Parse UTC timestamps from filenames; ignore files that do not match expected pattern.
151
+ - Delete files older than 7 days.
152
+ - Log a summary line to the verification terminal or show a toast with counts.
153
+ - Optional background cleanup:
154
+ - Run once per Studio start (or every 24h) if a config flag is enabled.
155
+ - Track last-cleanup timestamp in extension global state.
156
+
157
+ ## Verification websocket security (backend)
158
+
159
+ ### Single-use ticket handshake
160
+
161
+ Policy
162
+ - Verification starts via `POST /api/verify/start`.
163
+ - The response includes a single-use ticket bound to user ID and execution ID.
164
+ - Ticket TTL is short and enforced (example: 30 seconds).
165
+ - Websocket connects using `wss://.../verify?ticket=...`.
166
+ - Ticket is consumed on successful connect and cannot be reused.
167
+
168
+ ### Strict egress schema (allowlist)
169
+
170
+ Policy
171
+ - Sandbox output must be JSON log records, not raw stdout.
172
+ - Only structured log events pass through to the websocket.
173
+ - All log messages are validated against an allowlisted schema.
174
+
175
+ Recommended shape
176
+ - `level`: enum `info | warn | error | system`
177
+ - `text`: string, max length 2000
178
+ - `timestamp`: ISO string
179
+ - `code`: optional, enum from documented error code list (all codes allowed if docs are provided)
180
+
181
+ Implementation notes
182
+ - Use a schema validator like Zod for strict parsing.
183
+ - Unknown fields are stripped and invalid payloads are dropped.
184
+ - On invalid payload, send a safe generic system log line.
185
+
186
+ ### Ingress policy (read-only)
187
+
188
+ Policy
189
+ - Any inbound websocket message from the client is a violation.
190
+ - Log the event and close with policy code 1008.
191
+
192
+ ### Sanitization
193
+
194
+ Policy
195
+ - Do not attempt to regex-strip secrets from arbitrary text.
196
+ - Only structured logs make it to the client.
197
+ - Any error emitted outside schema is replaced with a generic error and an internal trace ID.
198
+
199
+ ### Rate limits and abuse handling
200
+
201
+ Policy
202
+ - Rate limit websocket messages per connection.
203
+ - Cap log line length and total bytes per run (1 MB).
204
+ - Enforce connection timeout and idle timeout.
205
+
206
+ ## Interfaces and contracts
207
+
208
+ Frontend to API v3
209
+ - `POST /api/verify/start` returns `{ ticket, verificationId, expiresAt }`.
210
+ - Errors return documented error codes only.
211
+
212
+ API v3 to verification system
213
+ - Start job with `verificationId` and user binding.
214
+ - Bind websocket connection to a single job.
215
+
216
+ Verification system to frontend
217
+ - Only structured log schema.
218
+ - No internal paths, raw stack traces, or environment info.
219
+
220
+ ## Implementation milestones (ordered, with code references)
221
+
222
+ 1. Upgrade backend contract metadata to match current injection model.
223
+ - Use `framework/core/ContractRegistry.js`, `framework/metadata/FunctionRegistry.js`, and `framework/core/ContextBuilder.js` as the source of truth.
224
+ - Ensure wrappers are exposed as `ctx.lib.<module>.<fn>` with docs/params for IDE usage.
225
+ 2. Extend API v3 computation contract to expose rich SDK metadata.
226
+ - Update `api-v3/services/computationContract.js` and `api-v3/routes/computations.js` (`GET /computations/contract`).
227
+ - Include error code references from `computation-system-v3/verification/ErrorCodes.js` and doc source `.../docs/16-ERROR-CODE-REFERENCE.md`.
228
+ - Add `sdkVersion`, `generatedAt`, and `contractHash` to the response for frontend cache validation.
229
+ 3. Replace the frontend SDK placeholders with contract-driven d.ts generation.
230
+ - Replace/extend `Bulltrackers-Frontend/web2.0/ide-extension/src/libsContent.ts`.
231
+ - Generate `bulltrackers.d.ts` in the workspace on activation in `.../src/extension.ts`.
232
+ 4. Upgrade frontend diagnostics to use contract metadata instead of regex.
233
+ - Replace `doSoftChecks` in `.../src/extension.ts` with AST + contract checks (forbidden identifiers, ctx.lib, ctx.data).
234
+ - Use table/field sets from the contract endpoint.
235
+ 5. Implement ticketed verification websocket and read-only terminal.
236
+ - Add start/ticket endpoint in API v3 (new route alongside `routes/computations.js`).
237
+ - Add websocket bridge in verification layer (new module under `computation-system-v3/verification`).
238
+ - Add read-only terminal in `Bulltrackers-Frontend/web2.0/ide-extension/src/extension.ts` (separate from `BulltrackersPty` in `.../src/terminal.ts`).
239
+ 6. Persist verification logs per run.
240
+ - Frontend: write `workspace:/output/verification-<timestamp>.txt` with a 1 MB cap.
241
+ - Backend: enforce log byte caps in `verification/runVerification.js` (increase from 8 KB to 1 MB for sandbox logs).
242
+ 7. Tests and hardening.
243
+ - Unit tests for contract generation, log egress schema, ticket reuse, websocket ingress policy.
244
+ - Regression tests for diagnostics and d.ts generation.
245
+
246
+ ## Open questions
247
+
248
+ - None.
@@ -109,9 +109,10 @@ class ContextBuilder {
109
109
  * @param {UsageCollector} [params.usageCollector] - Optional collector for billing stats
110
110
  * @param {Array<{ts:number,level:string,message:string}>} [params.logBuffer] - Optional array to collect structured log entries (user computations). When used with logMaxBytes, quota is enforced.
111
111
  * @param {number} [params.logMaxBytes] - Max total bytes for logBuffer. When exceeded, logBuffer.quotaExceeded is set and a system line is appended.
112
+ * @param {(entry: { ts: number, level: string, message: string }) => void} [params.onLog] - Optional callback for streaming structured logs.
112
113
  * @returns {Object} The 'context' passed to process()
113
114
  */
114
- build({ computationName, date, data, config, entityId, jobId = null, traceAlways = false, usageCollector = null, logBuffer = null, logMaxBytes = 0 }) {
115
+ build({ computationName, date, data, config, entityId, jobId = null, traceAlways = false, usageCollector = null, logBuffer = null, logMaxBytes = 0, onLog = null }) {
115
116
  let lib;
116
117
  if (config) {
117
118
  lib = buildLib(config);
@@ -199,12 +200,21 @@ class ContextBuilder {
199
200
  const sysMsg = { ts: Date.now(), level: 'info', message: '[SYSTEM] Log quota exceeded. Subsequent logs suppressed.\n' };
200
201
  logBuffer.push(sysMsg);
201
202
  logBuffer._currentBytes += Buffer.byteLength(JSON.stringify(sysMsg), 'utf8');
203
+ if (typeof onLog === 'function') {
204
+ try { onLog(sysMsg); } catch (_) { }
205
+ }
202
206
  return;
203
207
  }
204
208
  logBuffer.push(entry);
205
209
  logBuffer._currentBytes += entryBytes;
210
+ if (typeof onLog === 'function') {
211
+ try { onLog(entry); } catch (_) { }
212
+ }
206
213
  } else {
207
214
  console.log(`[${computationName}] ${line.trim()}`);
215
+ if (typeof onLog === 'function') {
216
+ try { onLog({ ts: Date.now(), level, message: sanitized }); } catch (_) { }
217
+ }
208
218
  }
209
219
  }
210
220
  const logFn = (msg) => append('info', msg);
@@ -232,4 +242,4 @@ class ContextBuilder {
232
242
  }
233
243
  }
234
244
 
235
- module.exports = { ContextBuilder };
245
+ module.exports = { ContextBuilder };
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+ /**
3
+ * @fileoverview Stable JSON hashing for contract payloads.
4
+ */
5
+
6
+ const crypto = require('crypto');
7
+
8
+ function stableStringify(value) {
9
+ if (value === null || value === undefined) return JSON.stringify(value);
10
+ const t = typeof value;
11
+ if (t === 'string' || t === 'number' || t === 'boolean') return JSON.stringify(value);
12
+ if (Array.isArray(value)) {
13
+ return '[' + value.map(stableStringify).join(',') + ']';
14
+ }
15
+ if (t === 'object') {
16
+ const keys = Object.keys(value).sort();
17
+ const parts = keys.map((k) => JSON.stringify(k) + ':' + stableStringify(value[k]));
18
+ return '{' + parts.join(',') + '}';
19
+ }
20
+ return JSON.stringify(String(value));
21
+ }
22
+
23
+ function hashContract(payload) {
24
+ const json = stableStringify(payload);
25
+ return crypto.createHash('sha256').update(json).digest('hex');
26
+ }
27
+
28
+ module.exports = {
29
+ stableStringify,
30
+ hashContract
31
+ };
@@ -52,6 +52,9 @@ function getRegistry() {
52
52
  lookback: contract.lookback || 0,
53
53
  filterRequired: !!(contract.filter && Object.keys(contract.filter).length > 0),
54
54
  uses: contract.description || '',
55
+ description: contract.description || '',
56
+ example: contract.example || '',
57
+ contract,
55
58
  visibility: 'public'
56
59
  };
57
60
  }
@@ -11,12 +11,12 @@ const { VERIFICATION: VerifCodes } = require('../../../verification/ErrorCodes')
11
11
  describe('Log quota', () => {
12
12
  describe('verification: LOG_QUOTA_EXCEEDED', () => {
13
13
  it('fails dry_run when ctx.log() output exceeds limit', async () => {
14
- // Log lines of ~100 bytes each; 100 lines = 10k bytes > 8192
14
+ // Log lines of ~120 bytes each; 12000 lines ~ 1.4MB > 1MB quota
15
15
  const code = `
16
16
  module.exports = {
17
17
  config: { name: 'LogFlood', skills: ['lib'] },
18
18
  process: (ctx) => {
19
- for (let i = 0; i < 100; i++) {
19
+ for (let i = 0; i < 12000; i++) {
20
20
  ctx.log('line ' + i + ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
21
21
  }
22
22
  return { ok: true };
@@ -98,4 +98,4 @@ describe('Log quota', () => {
98
98
  assert.ok(logBuffer.length >= 2);
99
99
  });
100
100
  });
101
- });
101
+ });
@@ -48,7 +48,7 @@ function getStaticComputationNames() {
48
48
  * @param {string} code - User computation source code
49
49
  * @param {string} name - Computation name (for metadata)
50
50
  * @param {string} userId - Owner ID (for dependency resolution: allow own computations)
51
- * @param {object} options - { userComputationNames?: string[], fixtureData?: object, uploadSessionId?: string, onStageProgress?: (sessionId, payload) => void, forceMockBq?: boolean }
51
+ * @param {object} options - { userComputationNames?: string[], fixtureData?: object, uploadSessionId?: string, onStageProgress?: (sessionId, payload) => void, onLog?: (entry) => void, forceMockBq?: boolean }
52
52
  * @returns {Promise<{ stages: Array<{ id: string, status: string, message?: string, errorCode?: string }>, passed: boolean, errors?: string[], errorCodes?: string[], logOutput?: Array<{ ts: string|number, level: string, message: string }> }>}
53
53
  */
54
54
  async function runVerification(code, name, userId, options = {}) {
@@ -58,6 +58,7 @@ async function runVerification(code, name, userId, options = {}) {
58
58
  const logger = options.logger || console;
59
59
  const uploadSessionId = options.uploadSessionId || null;
60
60
  const onStageProgress = typeof options.onStageProgress === 'function' ? options.onStageProgress : null;
61
+ const onLog = typeof options.onLog === 'function' ? options.onLog : null;
61
62
 
62
63
  const userComputationNames = new Set(
63
64
  (options.userComputationNames || []).map(n => String(n).toLowerCase())
@@ -205,7 +206,7 @@ async function runVerification(code, name, userId, options = {}) {
205
206
  Object.assign(combinedData, fetched);
206
207
  }
207
208
 
208
- const LOG_MAX_BYTES_VERIFY = 8192;
209
+ const LOG_MAX_BYTES_VERIFY = 1024 * 1024;
209
210
  const logBuffer = [];
210
211
  const ctx = contextBuilder.build({
211
212
  computationName: entry.originalName || entry.name,
@@ -216,7 +217,8 @@ async function runVerification(code, name, userId, options = {}) {
216
217
  jobId: 'verify-sandbox',
217
218
  usageCollector: null,
218
219
  logBuffer,
219
- logMaxBytes: LOG_MAX_BYTES_VERIFY
220
+ logMaxBytes: LOG_MAX_BYTES_VERIFY,
221
+ onLog
220
222
  });
221
223
 
222
224
  // When not using WASM, recipe must have a callable process; dynamic entries only have processSource
package/index.js CHANGED
@@ -12,7 +12,7 @@ const firestoreUtils = require('./functions/core/utils/firestore_utils');
12
12
 
13
13
  class FirestoreBatchManager { constructor(_db, _headerManager, _logger, _config) { } async flushBatches() { } } // No-op taskengineutils removed.
14
14
  const computationSystemV2 = require('./functions/computation-system-v3/index');
15
- const { createApiV3App } = require('./functions/api-v3/index');
15
+ const { createApiV3App, createApiV3Server, attachVerificationWebSocketServer } = require('./functions/api-v3/index');
16
16
  const { createDataFetcherSystem } = require('./functions/data-fetcher-system-v1');
17
17
  const {
18
18
  runUpdateOrchestrator,
@@ -57,7 +57,7 @@ const taskEngine = {
57
57
  }, dependencies).handlers.taskEngine(message, context)
58
58
  };
59
59
  const computationSystem = computationSystemV2;
60
- const api = { createApiV3App, };
60
+ const api = { createApiV3App, createApiV3Server, attachVerificationWebSocketServer };
61
61
  const dataFetcherSystem = { create: ({ config, dependencies, middlewares } = {}) => createDataFetcherSystem({ config, dependencies, middlewares }) };
62
62
  const maintenance = {
63
63
  runFetchInsights: (config, dependencies) => buildFetcherRuntime({ fetchInsights: config }, dependencies).handlers.insightsFetcher(),
@@ -76,4 +76,4 @@ const alertSystem = {
76
76
  };
77
77
  module.exports = {
78
78
  pipe: { core, orchestrator, dispatcher, taskEngine, computationSystem, api, maintenance, proxy, alertSystem, dataFetcherSystem },
79
- };
79
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.997",
3
+ "version": "1.0.999",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -39,7 +39,8 @@
39
39
  "sharedsetup": "latest",
40
40
  "stripe": "^17.5.0",
41
41
  "supertest": "^7.2.2",
42
- "zod": "^4.3.6"
42
+ "zod": "^4.3.6",
43
+ "ws": "^8.18.1"
43
44
  },
44
45
  "devDependencies": {
45
46
  "bulltracker-deployer": "file:../bulltracker-deployer",