bulltrackers-module 1.0.1002 → 1.0.1004
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/routes/computations.js +162 -126
- package/functions/api-v3/services/verificationSandboxClient.js +3 -1
- package/functions/api-v3/testing/routes.integration.test.js +59 -51
- package/functions/api-v3/websocket/verification.js +28 -3
- package/functions/computation-system-v3/verification/runVerification.js +40 -1
- package/functions/computation-system-v3/verification/sandbox-http.js +3 -1
- package/package.json +1 -1
|
@@ -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),
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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,
|
|
758
|
-
}
|
|
759
|
-
res.status(200).json({ uploadSessionId });
|
|
760
|
-
setImmediate(async () => {
|
|
761
|
-
try {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
}
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
if (
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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, {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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 =
|
|
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 });
|
|
@@ -946,57 +946,65 @@ describe('Cross-Cutting', () => {
|
|
|
946
946
|
'Content-Type': 'application/json',
|
|
947
947
|
});
|
|
948
948
|
|
|
949
|
-
it('returns 422 and does not write to GCS when code fails verification (process.env)', async () => {
|
|
950
|
-
const res = await request(app)
|
|
951
|
-
.post('/computations/upload')
|
|
952
|
-
.set(auth())
|
|
953
|
-
.send({
|
|
954
|
-
code: `module.exports = { config: { name: 'Evil', skills: [], requires: {} }, process(ctx) { return process.env; } };`,
|
|
955
|
-
name: 'Evil',
|
|
956
|
-
});
|
|
957
|
-
expect(res.status).toBe(422);
|
|
958
|
-
expect(res.body.error).toBe('Verification
|
|
959
|
-
expect(
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
.
|
|
966
|
-
.
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
expect(res.
|
|
972
|
-
expect(
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
.
|
|
979
|
-
.
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
expect(res.
|
|
985
|
-
expect(
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
949
|
+
it('returns 422 and does not write to GCS when code fails verification (process.env)', async () => {
|
|
950
|
+
const res = await request(app)
|
|
951
|
+
.post('/computations/upload')
|
|
952
|
+
.set(auth())
|
|
953
|
+
.send({
|
|
954
|
+
code: `module.exports = { config: { name: 'Evil', skills: [], requires: {} }, process(ctx) { return process.env; } };`,
|
|
955
|
+
name: 'Evil',
|
|
956
|
+
});
|
|
957
|
+
expect(res.status).toBe(422);
|
|
958
|
+
expect(res.body.error).toBe('Verification required');
|
|
959
|
+
expect(storage._saveCalls.length).toBe(0);
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
it('returns 422 and does not write to GCS when code fails verification (require)', async () => {
|
|
963
|
+
const res = await request(app)
|
|
964
|
+
.post('/computations/upload')
|
|
965
|
+
.set(auth())
|
|
966
|
+
.send({
|
|
967
|
+
code: `module.exports = { config: { name: 'Evil', skills: [], requires: {} }, process(ctx) { require('fs'); return ctx; } };`,
|
|
968
|
+
name: 'Evil',
|
|
969
|
+
});
|
|
970
|
+
expect(res.status).toBe(422);
|
|
971
|
+
expect(res.body.error).toBe('Verification required');
|
|
972
|
+
expect(storage._saveCalls.length).toBe(0);
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
it('returns 422 and does not write to GCS when code fails verification (eval)', async () => {
|
|
976
|
+
const res = await request(app)
|
|
977
|
+
.post('/computations/upload')
|
|
978
|
+
.set(auth())
|
|
979
|
+
.send({
|
|
980
|
+
code: `module.exports = { config: { name: 'Evil', skills: [], requires: {} }, process(ctx) { return eval('1+1'); } };`,
|
|
981
|
+
name: 'Evil',
|
|
982
|
+
});
|
|
983
|
+
expect(res.status).toBe(422);
|
|
984
|
+
expect(res.body.error).toBe('Verification required');
|
|
985
|
+
expect(storage._saveCalls.length).toBe(0);
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
it('returns 200 and writes to GCS when code passes verification', async () => {
|
|
989
|
+
// Use process: (ctx) => ... so extracted process source re-parses when wrapped in parens (method shorthand process(ctx){ } is invalid as a standalone expression).
|
|
990
|
+
const validCode = `module.exports = {
|
|
991
|
+
config: { name: 'SafeComp', typeOverride: 'global' },
|
|
992
|
+
process: (ctx) => ({ ok: true, date: ctx.date })
|
|
993
|
+
};`;
|
|
994
|
+
const crypto = require('crypto');
|
|
995
|
+
const codeHash = crypto.createHash('sha256').update(validCode).digest('hex');
|
|
996
|
+
db.seed('users/user-123/verification_status/safecomp', {
|
|
997
|
+
lastHash: codeHash,
|
|
998
|
+
lastVerificationId: 'verify-1',
|
|
999
|
+
lastVerifiedAt: new Date().toISOString(),
|
|
1000
|
+
status: 'passed',
|
|
1001
|
+
name: 'SafeComp',
|
|
1002
|
+
});
|
|
1003
|
+
const res = await request(app)
|
|
1004
|
+
.post('/computations/upload')
|
|
1005
|
+
.set(auth())
|
|
1006
|
+
.send({ code: validCode, name: 'SafeComp' });
|
|
1007
|
+
expect(res.status).toBe(200);
|
|
1000
1008
|
expect(res.body.success).toBe(true);
|
|
1001
1009
|
expect(res.body.hash).toBeDefined();
|
|
1002
1010
|
expect(storage._saveCalls.length).toBe(1);
|
|
@@ -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
|
-
|
|
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);
|