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.
- package/functions/api-v3/index.js +12 -1
- package/functions/api-v3/routes/computations.js +80 -14
- package/functions/api-v3/services/computationContract.js +120 -5
- package/functions/api-v3/websocket/verification.js +320 -0
- package/functions/computation-system-v3/docs/00-INDEX.md +1 -0
- package/functions/computation-system-v3/docs/17-IDE-VERIFICATION-PLAN.md +248 -0
- package/functions/computation-system-v3/framework/core/ContextBuilder.js +12 -2
- package/functions/computation-system-v3/framework/metadata/ContractHash.js +31 -0
- package/functions/computation-system-v3/framework/metadata/FunctionRegistry.js +3 -0
- package/functions/computation-system-v3/framework/tests/unit/LogQuota.test.js +3 -3
- package/functions/computation-system-v3/verification/runVerification.js +5 -3
- package/index.js +3 -3
- package/package.json +3 -2
|
@@ -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.
|
|
65
|
-
try {
|
|
66
|
-
const contract = buildUserComputationContract();
|
|
67
|
-
res.
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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 || {})
|
|
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 ~
|
|
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 <
|
|
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 =
|
|
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.
|
|
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",
|