bulltrackers-module 1.0.1002 → 1.0.1003

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.
@@ -121,7 +121,7 @@ const createComputationsRouter = () => {
121
121
  requestId: req.requestId || null,
122
122
  createdAt: new Date().toISOString()
123
123
  };
124
- await redis.set(VERIFY_TICKET_PREFIX + ticket, JSON.stringify(payload), 'EX', VERIFY_TICKET_TTL_SECONDS);
124
+ await redis.set(VERIFY_TICKET_PREFIX + ticket, JSON.stringify(payload), { ex: VERIFY_TICKET_TTL_SECONDS });
125
125
  const expiresAt = new Date(Date.now() + VERIFY_TICKET_TTL_SECONDS * 1000).toISOString();
126
126
  res.json({
127
127
  success: true,
@@ -242,11 +242,11 @@ const createComputationsRouter = () => {
242
242
  }
243
243
  });
244
244
 
245
- // POST /verify: Run verification sandbox (no GCS write). Returns staged results.
246
- router.post('/verify', requireVerifiedUser, async (req, res, next) => {
247
- try {
248
- const userId = req.targetUserId;
249
- const { code, name } = req.body || {};
245
+ // POST /verify: Run verification sandbox (no GCS write). Returns staged results.
246
+ router.post('/verify', requireVerifiedUser, async (req, res, next) => {
247
+ try {
248
+ const userId = req.targetUserId;
249
+ const { code, name } = req.body || {};
250
250
  if (!code || typeof code !== 'string') {
251
251
  return res.status(400).json({ error: 'Missing or invalid "code"' });
252
252
  }
@@ -277,14 +277,15 @@ const createComputationsRouter = () => {
277
277
  // non-fatal: allow verify with only static deps
278
278
  }
279
279
  const sandboxUrl = config.computationVerification?.url || '';
280
- const sandboxResult = await callVerificationSandbox(sandboxUrl, { code, name, userId, userComputationNames }, { logger: req.services.logger || console });
281
- const result = sandboxResult !== null
282
- ? sandboxResult
283
- : await runVerification(code, name, userId, { userComputationNames, logger: req.services.logger || console });
284
- if (result.passed === true && req.services.db) {
285
- writeComputationNotification(
286
- req.services.db,
287
- userId,
280
+ const sandboxResult = await callVerificationSandbox(sandboxUrl, { code, name, userId, userComputationNames }, { logger: req.services.logger || console });
281
+ const result = sandboxResult !== null
282
+ ? sandboxResult
283
+ : await runVerification(code, name, userId, { userComputationNames, logger: req.services.logger || console });
284
+ const codeHash = crypto.createHash('sha256').update(code).digest('hex');
285
+ if (result.passed === true && req.services.db) {
286
+ writeComputationNotification(
287
+ req.services.db,
288
+ userId,
288
289
  'COMPUTATION_VERIFIED',
289
290
  'Computation verified',
290
291
  `"${name}" passed verification.`,
@@ -292,21 +293,51 @@ const createComputationsRouter = () => {
292
293
  'success'
293
294
  ).catch(() => { });
294
295
  }
295
- res.json({
296
- success: result.passed,
297
- stages: result.stages,
298
- passed: result.passed,
299
- errors: result.errors || null,
300
- errorCodes: result.errorCodes || null,
301
- logOutput: result.logOutput || null,
302
- origin: 'verification',
303
- remainingVerifications: typeof remaining === 'number' ? remaining : undefined,
304
- limit
305
- });
306
- } catch (error) {
307
- next(error);
308
- }
309
- });
296
+ res.json({
297
+ success: result.passed,
298
+ stages: result.stages,
299
+ passed: result.passed,
300
+ errors: result.errors || null,
301
+ errorCodes: result.errorCodes || null,
302
+ codeHash,
303
+ resultSummary: result.resultSummary || null,
304
+ resultPreview: result.resultPreview || null,
305
+ logOutput: result.logOutput || null,
306
+ origin: 'verification',
307
+ remainingVerifications: typeof remaining === 'number' ? remaining : undefined,
308
+ limit
309
+ });
310
+ } catch (error) {
311
+ next(error);
312
+ }
313
+ });
314
+
315
+ // GET /verification/status?name=... — latest verification status for a computation name
316
+ router.get('/verification/status', requireVerifiedUser, async (req, res, next) => {
317
+ try {
318
+ const userId = req.targetUserId;
319
+ const rawName = (req.query.name && String(req.query.name).trim()) || '';
320
+ if (!rawName) {
321
+ return res.status(400).json({ error: 'Missing or invalid "name"' });
322
+ }
323
+ if (!req.services.db) {
324
+ return res.status(503).json({ error: 'Verification status unavailable' });
325
+ }
326
+ const normalizedName = Manifest.normalize(rawName);
327
+ if (!normalizedName) {
328
+ return res.status(400).json({ error: 'Invalid computation name' });
329
+ }
330
+ const statusRef = req.services.db.collection('users').doc(userId)
331
+ .collection('verification_status').doc(normalizedName);
332
+ const snap = await statusRef.get();
333
+ if (!snap.exists) {
334
+ return res.status(404).json({ error: 'Verification status not found' });
335
+ }
336
+ return res.json({ success: true, data: snap.data() });
337
+ } catch (error) {
338
+ next(error);
339
+ }
340
+ });
310
341
 
311
342
  // POST /annotate-sql — stateless SQL annotation for a computation source (no writes).
312
343
  router.post('/annotate-sql', requireVerifiedUser, async (req, res, next) => {
@@ -715,8 +746,8 @@ const createComputationsRouter = () => {
715
746
  router.post('/upload', requireVerifiedUser, requireSubscription({ billingUrl: '/billing' }), async (req, res, next) => {
716
747
  try {
717
748
  const userId = req.targetUserId;
718
- const { services } = req;
719
- const { code, name, overwriteLast7Days } = req.body;
749
+ const { services } = req;
750
+ const { code, name, overwriteLast7Days } = req.body;
720
751
 
721
752
  if (!code || typeof code !== 'string') {
722
753
  return res.status(400).json({ error: 'Missing or invalid "code"' });
@@ -737,9 +768,9 @@ const createComputationsRouter = () => {
737
768
  }
738
769
  }
739
770
 
740
- // Upload failure backoff: block if user is in exponential backoff from recent verification failures
741
- const backoff = await checkUploadBackoff(userId);
742
- if (backoff.blocked) {
771
+ // Upload failure backoff: block if user is in exponential backoff from recent verification failures
772
+ const backoff = await checkUploadBackoff(userId);
773
+ if (backoff.blocked) {
743
774
  const retryAfter = backoff.retryAfterSeconds ?? 60;
744
775
  res.setHeader('Retry-After', String(retryAfter));
745
776
  return res.status(429).json({
@@ -749,57 +780,69 @@ const createComputationsRouter = () => {
749
780
  });
750
781
  }
751
782
 
752
- // Optional: stream verification progress via SSE (client then GET /computations/upload/stream/:id)
753
- if (req.body.stream === true) {
754
- const uploadSessionId = uuidv4();
755
- const redis = getRedis();
756
- if (redis) {
757
- await redis.set(UPLOAD_SESSION_PREFIX + uploadSessionId, userId, 'EX', UPLOAD_SESSION_TTL);
758
- }
759
- res.status(200).json({ uploadSessionId });
760
- setImmediate(async () => {
761
- try {
762
- let userComputationNames = [];
763
- try {
764
- const ref = services.db.collection('users').doc(userId).collection('computations');
765
- const snap = await ref.get();
766
- snap.docs.forEach(d => {
767
- const n = d.data()?.name || d.id;
768
- if (n) userComputationNames.push(Manifest.normalize(n));
769
- });
770
- } catch (e) { /* non-fatal */ }
771
- const verifyResult = await runVerification(code, name, userId, {
772
- userComputationNames,
773
- logger: services.logger || console,
774
- uploadSessionId,
775
- onStageProgress: (sid, payload) => publishUploadStreamEvent(sid, payload)
776
- });
777
- if (verifyResult.passed !== true) {
778
- await recordUploadFailure(userId);
779
- publishUploadStreamEvent(uploadSessionId, { type: 'final', passed: false, stages: verifyResult.stages, errors: verifyResult.errors, errorCodes: verifyResult.errorCodes || null, origin: 'verification' });
780
- return;
781
- }
782
- let extractedConfig;
783
- try {
784
- extractedConfig = Manifest.extractConfig(code);
785
- } catch (e) {
786
- await recordUploadFailure(userId);
787
- publishUploadStreamEvent(uploadSessionId, { type: 'final', passed: false, error: `Config extraction failed: ${e.message}` });
788
- return;
789
- }
790
- if (!extractedConfig || typeof extractedConfig !== 'object') {
791
- await recordUploadFailure(userId);
792
- publishUploadStreamEvent(uploadSessionId, { type: 'final', passed: false, error: 'Config could not be extracted' });
793
- return;
794
- }
795
- const hash = crypto.createHash('sha256').update(code).digest('hex');
796
- const bucket = services.storage.bucket(config.codeBucket);
797
- const blobName = `blobs/${hash}.js`;
798
- const blob = bucket.file(blobName);
799
- const [exists] = await blob.exists();
800
- if (!exists) {
801
- await blob.save(code, {
802
- metadata: {
783
+ // Optional: stream verification progress via SSE (client then GET /computations/upload/stream/:id)
784
+ if (req.body.stream === true) {
785
+ const uploadSessionId = uuidv4();
786
+ const redis = getRedis();
787
+ if (redis) {
788
+ await redis.set(UPLOAD_SESSION_PREFIX + uploadSessionId, userId, { ex: UPLOAD_SESSION_TTL });
789
+ }
790
+ res.status(200).json({ uploadSessionId });
791
+ setImmediate(async () => {
792
+ try {
793
+ if (!services.db) {
794
+ publishUploadStreamEvent(uploadSessionId, {
795
+ type: 'final',
796
+ passed: false,
797
+ error: 'Upload unavailable',
798
+ origin: 'system'
799
+ });
800
+ return;
801
+ }
802
+ const normalizedName = Manifest.normalize(name);
803
+ const codeHash = crypto.createHash('sha256').update(code).digest('hex');
804
+ const statusRef = services.db.collection('users').doc(userId)
805
+ .collection('verification_status').doc(normalizedName);
806
+ const statusSnap = await statusRef.get();
807
+ const statusData = statusSnap.exists ? statusSnap.data() : null;
808
+ if (!statusData || statusData.lastHash !== codeHash) {
809
+ publishUploadStreamEvent(uploadSessionId, {
810
+ type: 'final',
811
+ passed: false,
812
+ error: 'Verification required. Please verify this computation before uploading.',
813
+ origin: 'verification'
814
+ });
815
+ return;
816
+ }
817
+ publishUploadStreamEvent(uploadSessionId, {
818
+ type: 'stage',
819
+ stages: [{ id: 'verification', status: 'passed', message: 'Verified hash matched' }]
820
+ });
821
+ let extractedConfig;
822
+ try {
823
+ extractedConfig = Manifest.extractConfig(code);
824
+ } catch (e) {
825
+ await recordUploadFailure(userId);
826
+ publishUploadStreamEvent(uploadSessionId, { type: 'final', passed: false, error: `Config extraction failed: ${e.message}` });
827
+ return;
828
+ }
829
+ if (!extractedConfig || typeof extractedConfig !== 'object') {
830
+ await recordUploadFailure(userId);
831
+ publishUploadStreamEvent(uploadSessionId, { type: 'final', passed: false, error: 'Config could not be extracted' });
832
+ return;
833
+ }
834
+ const hash = codeHash;
835
+ const bucket = services.storage.bucket(config.codeBucket);
836
+ const blobName = `blobs/${hash}.js`;
837
+ const blob = bucket.file(blobName);
838
+ publishUploadStreamEvent(uploadSessionId, {
839
+ type: 'stage',
840
+ stages: [{ id: 'upload', status: 'running', message: 'Uploading computation code' }]
841
+ });
842
+ const [exists] = await blob.exists();
843
+ if (!exists) {
844
+ await blob.save(code, {
845
+ metadata: {
803
846
  contentType: 'application/javascript',
804
847
  cacheControl: 'public, max-age=31536000, immutable',
805
848
  metadata: { uploadedBy: userId, originalName: name, verifiedAt: new Date().toISOString() }
@@ -829,57 +872,50 @@ const createComputationsRouter = () => {
829
872
  } else if (!currentData.status) dataToSet.status = 'paused';
830
873
  t.set(userCompRef, dataToSet, { merge: true });
831
874
  });
832
- await clearUploadFailure(userId);
833
- if (services.db) {
834
- writeComputationNotification(
835
- services.db,
836
- userId,
875
+ await clearUploadFailure(userId);
876
+ if (services.db) {
877
+ writeComputationNotification(
878
+ services.db,
879
+ userId,
837
880
  'COMPUTATION_UPLOADED',
838
881
  'Computation uploaded',
839
882
  `"${name}" uploaded (${hash.substring(0, 8)}).`,
840
883
  { computationName: name, versionHash: hash },
841
884
  'success'
842
885
  ).catch(() => { });
843
- }
844
- publishUploadStreamEvent(uploadSessionId, { type: 'final', passed: true, hash, computationId });
845
- } catch (e) {
846
- console.error('[Upload stream] background error:', e);
847
- if (uploadSessionId) {
886
+ }
887
+ publishUploadStreamEvent(uploadSessionId, {
888
+ type: 'final',
889
+ passed: true,
890
+ hash,
891
+ computationId
892
+ });
893
+ } catch (e) {
894
+ console.error('[Upload stream] background error:', e);
895
+ if (uploadSessionId) {
848
896
  publishUploadStreamEvent(uploadSessionId, { type: 'final', passed: false, error: e.message });
849
897
  }
850
898
  }
851
899
  });
852
900
  return;
853
- }
854
-
855
- // Hard barrier: sandbox verification must pass before any GCS write or Firestore update
856
- let userComputationNames = [];
857
- try {
858
- const ref = services.db.collection('users').doc(userId).collection('computations');
859
- const snap = await ref.get();
860
- snap.docs.forEach(d => {
861
- const n = d.data()?.name || d.id;
862
- if (n) userComputationNames.push(Manifest.normalize(n));
863
- });
864
- } catch (e) {
865
- // non-fatal: allow upload with only static deps
866
- }
867
- const sandboxUrl = config.computationVerification?.url || '';
868
- const sandboxResult = await callVerificationSandbox(sandboxUrl, { code, name, userId, userComputationNames }, { logger: services.logger || console });
869
- const verifyResult = sandboxResult !== null
870
- ? sandboxResult
871
- : await runVerification(code, name, userId, { userComputationNames, logger: services.logger || console });
872
- if (verifyResult.passed !== true) {
873
- await recordUploadFailure(userId);
874
- return res.status(422).json({
875
- error: 'Verification failed',
876
- message: 'Computation must pass sandbox verification before upload.',
877
- stages: verifyResult.stages,
878
- errors: verifyResult.errors || null,
879
- errorCodes: verifyResult.errorCodes || null,
880
- origin: 'verification'
881
- });
882
- }
901
+ }
902
+
903
+ // Hard barrier: sandbox verification must pass before any GCS write or Firestore update
904
+ if (!services.db) {
905
+ return res.status(503).json({ error: 'Upload unavailable' });
906
+ }
907
+ const normalizedName = Manifest.normalize(name);
908
+ const codeHash = crypto.createHash('sha256').update(code).digest('hex');
909
+ const statusRef = services.db.collection('users').doc(userId)
910
+ .collection('verification_status').doc(normalizedName);
911
+ const statusSnap = await statusRef.get();
912
+ const statusData = statusSnap.exists ? statusSnap.data() : null;
913
+ if (!statusData || statusData.lastHash !== codeHash) {
914
+ return res.status(422).json({
915
+ error: 'Verification required',
916
+ message: 'Computation must pass sandbox verification before upload (hash mismatch or missing verification).'
917
+ });
918
+ }
883
919
 
884
920
  // Extract config for schedule; fail upload if config cannot be parsed (scheduling metadata must be valid).
885
921
  let extractedConfig;
@@ -899,7 +935,7 @@ const createComputationsRouter = () => {
899
935
  }
900
936
 
901
937
  // Hash Code
902
- const hash = crypto.createHash('sha256').update(code).digest('hex');
938
+ const hash = codeHash;
903
939
 
904
940
  // Write to GCS (CAS) — only reached after verification passed
905
941
  const bucket = services.storage.bucket(config.codeBucket);
@@ -45,7 +45,9 @@ async function callVerificationSandbox(sandboxUrl, payload, opts = {}) {
45
45
  passed: Boolean(body.passed),
46
46
  stages: Array.isArray(body.stages) ? body.stages : [],
47
47
  errors: body.errors ?? null,
48
- errorCodes: Array.isArray(body.errorCodes) ? body.errorCodes : (body.errorCodes ?? null)
48
+ errorCodes: Array.isArray(body.errorCodes) ? body.errorCodes : (body.errorCodes ?? null),
49
+ resultSummary: body.resultSummary ?? null,
50
+ resultPreview: body.resultPreview ?? null
49
51
  };
50
52
  } catch (err) {
51
53
  logger.log('ERROR', '[VerificationSandbox] Request failed', { message: err.message, url: sandboxUrl });
@@ -6,6 +6,7 @@
6
6
  const { WebSocketServer } = require('ws');
7
7
  const { getAuth } = require('firebase-admin/auth');
8
8
  const { z } = require('zod');
9
+ const crypto = require('crypto');
9
10
  const { Manifest } = require('../../computation-system-v3/framework/core/Manifest');
10
11
  const { runVerification } = require('../../computation-system-v3/verification/runVerification');
11
12
  const { getRedis } = require('../middleware/upstashRateLimit');
@@ -31,7 +32,10 @@ const StageSchema = z.object({
31
32
  const FinalSchema = z.object({
32
33
  passed: z.boolean(),
33
34
  errorCodes: z.array(z.string()).optional(),
34
- errors: z.array(z.string()).optional()
35
+ errors: z.array(z.string()).optional(),
36
+ codeHash: z.string().optional(),
37
+ resultSummary: z.any().optional(),
38
+ resultPreview: z.any().optional()
35
39
  });
36
40
 
37
41
  async function resolveFirebaseUser(req) {
@@ -280,10 +284,14 @@ function attachVerificationWebSocketServer(server, dependencies, apiConfig = {})
280
284
  onLog: (entry) => logEmitter(entry)
281
285
  });
282
286
 
287
+ const codeHash = crypto.createHash('sha256').update(payload.code).digest('hex');
283
288
  const finalPayload = {
284
289
  passed: result.passed === true,
285
290
  errorCodes: result.errorCodes || undefined,
286
- errors: result.errors || undefined
291
+ errors: result.errors || undefined,
292
+ codeHash: result.passed === true ? codeHash : undefined,
293
+ resultSummary: result.passed === true ? (result.resultSummary || undefined) : undefined,
294
+ resultPreview: result.passed === true ? (result.resultPreview || undefined) : undefined
287
295
  };
288
296
  const finalParsed = FinalSchema.safeParse(finalPayload);
289
297
  if (finalParsed.success) {
@@ -294,9 +302,26 @@ function attachVerificationWebSocketServer(server, dependencies, apiConfig = {})
294
302
  await auditRef.set({
295
303
  finishedAt: new Date().toISOString(),
296
304
  status: result.passed === true ? 'passed' : 'failed',
297
- errorCodes: result.errorCodes || null
305
+ errorCodes: result.errorCodes || null,
306
+ codeHash: result.passed === true ? codeHash : null,
307
+ resultSummary: result.passed === true ? (result.resultSummary || null) : null
298
308
  }, { merge: true });
299
309
  }
310
+ if (result.passed === true && services.db) {
311
+ const normalizedName = Manifest.normalize(payload.name || '');
312
+ if (normalizedName) {
313
+ const statusRef = services.db.collection('users').doc(targetUserId)
314
+ .collection('verification_status').doc(normalizedName);
315
+ await statusRef.set({
316
+ lastHash: codeHash,
317
+ lastVerificationId: verificationId || null,
318
+ lastVerifiedAt: new Date().toISOString(),
319
+ status: 'passed',
320
+ name: payload.name || null,
321
+ resultSummary: result.resultSummary || null
322
+ }, { merge: true });
323
+ }
324
+ }
300
325
 
301
326
  ws.close(1000, 'Verification complete');
302
327
  } catch (err) {
@@ -28,6 +28,43 @@ const STAGE_DEPENDENCIES = 'dependencies';
28
28
  const STAGE_GCS_RULES = 'gcs_rules';
29
29
  const STAGE_DRY_RUN = 'dry_run';
30
30
 
31
+ function summarizeResultValue(value) {
32
+ const type = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
33
+ if (type === 'array') {
34
+ const arr = Array.isArray(value) ? value : [];
35
+ const firstNonNull = arr.find((v) => v !== null && v !== undefined);
36
+ const itemType = firstNonNull === null || firstNonNull === undefined
37
+ ? 'null'
38
+ : Array.isArray(firstNonNull) ? 'array' : typeof firstNonNull;
39
+ const summary = {
40
+ type: 'array',
41
+ length: arr.length,
42
+ itemType
43
+ };
44
+ if (itemType === 'object' && firstNonNull && typeof firstNonNull === 'object' && !Array.isArray(firstNonNull)) {
45
+ const keys = Object.keys(firstNonNull).slice(0, 50);
46
+ const fieldTypes = {};
47
+ for (const key of keys) {
48
+ const val = firstNonNull[key];
49
+ fieldTypes[key] = val === null ? 'null' : Array.isArray(val) ? 'array' : typeof val;
50
+ }
51
+ summary.keys = keys;
52
+ summary.fieldTypes = fieldTypes;
53
+ }
54
+ return summary;
55
+ }
56
+ if (type === 'object' && value && !Array.isArray(value)) {
57
+ const keys = Object.keys(value).slice(0, 50);
58
+ const fieldTypes = {};
59
+ for (const key of keys) {
60
+ const val = value[key];
61
+ fieldTypes[key] = val === null ? 'null' : Array.isArray(val) ? 'array' : typeof val;
62
+ }
63
+ return { type: 'object', keys, fieldTypes };
64
+ }
65
+ return { type };
66
+ }
67
+
31
68
  /**
32
69
  * Get static computation names from built manifest (for dependency allow-list).
33
70
  */
@@ -242,7 +279,9 @@ async function runVerification(code, name, userId, options = {}) {
242
279
  const logOutput = Array.isArray(logBuffer)
243
280
  ? logBuffer.filter((e) => e && typeof e === 'object' && 'ts' in e && 'message' in e).map((e) => ({ ts: e.ts, level: e.level || 'info', message: e.message }))
244
281
  : [];
245
- return { stages, passed: true, logOutput };
282
+ const resultPreview = result.result;
283
+ const resultSummary = summarizeResultValue(resultPreview);
284
+ return { stages, passed: true, logOutput, resultPreview, resultSummary };
246
285
  }
247
286
  let errMsg = 'Run did not complete successfully';
248
287
  if (result && result.error) {
@@ -68,7 +68,9 @@ async function handleVerificationSandbox(req, res) {
68
68
  passed: result.passed === true,
69
69
  stages: result.stages || [],
70
70
  errors: result.errors || null,
71
- errorCodes: result.errorCodes || null
71
+ errorCodes: result.errorCodes || null,
72
+ resultSummary: result.resultSummary || null,
73
+ resultPreview: result.resultPreview || null
72
74
  }));
73
75
  } catch (e) {
74
76
  console.error('[VerificationSandbox]', e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.1002",
3
+ "version": "1.0.1003",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [