bulltrackers-module 1.0.996 → 1.0.998

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 |