@syngrisi/syngrisi
Version:
Syngrisi - Visual Testing Tool
5,966 lines • 185 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// src/server/utils/pick.ts
var pick, pick_default;
var init_pick = __esm({
"src/server/utils/pick.ts"() {
"use strict";
pick = (object, keys) => {
return keys.reduce((obj, key) => {
if (object && Object.prototype.hasOwnProperty.call(object, key)) {
if (object[key] !== void 0) obj[key] = object[key];
}
return obj;
}, {});
};
pick_default = pick;
}
});
// src/server/utils/isJSON.ts
var isJSON, isJSON_default;
var init_isJSON = __esm({
"src/server/utils/isJSON.ts"() {
"use strict";
isJSON = (text) => {
if (!text) return false;
const isValid = /^[\],:{}\s]*$/.test(
text.replace(/\\["\\\/bfnrtu]/g, "@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, "]").replace(/(?:^|:|,)(?:\s*\[)+/g, "")
);
return isValid;
};
isJSON_default = isJSON;
}
});
// src/server/utils/catchAsync.ts
var catchAsync, catchAsync_default;
var init_catchAsync = __esm({
"src/server/utils/catchAsync.ts"() {
"use strict";
catchAsync = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch((err) => {
return next(err);
});
};
catchAsync_default = catchAsync;
}
});
// src/server/utils/dateToISO8601.ts
var init_dateToISO8601 = __esm({
"src/server/utils/dateToISO8601.ts"() {
"use strict";
}
});
// src/server/utils/ProgressBar.ts
var init_ProgressBar = __esm({
"src/server/utils/ProgressBar.ts"() {
"use strict";
}
});
// src/server/utils/ApiError.ts
var ApiError, ApiError_default;
var init_ApiError = __esm({
"src/server/utils/ApiError.ts"() {
"use strict";
ApiError = class extends Error {
statusCode;
isOperational;
constructor(statusCode, message, isOperational = true, stack = "") {
super(message);
this.statusCode = statusCode;
this.isOperational = isOperational;
if (stack) {
this.stack = stack;
} else {
Error.captureStackTrace(this, this.constructor);
}
}
};
ApiError_default = ApiError;
}
});
// src/server/utils/removeEmptyProperties.ts
var removeEmptyProperties;
var init_removeEmptyProperties = __esm({
"src/server/utils/removeEmptyProperties.ts"() {
"use strict";
removeEmptyProperties = (obj) => Object.fromEntries(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Object.entries(obj).filter(([_, v]) => v != null && v !== "")
);
}
});
// src/server/utils/deserializeIfJSON.ts
import mongoose from "mongoose";
var EJSON, EXTENDED_JSON_KEYS, containsExtendedJsonMarkers, deserializeIfJSON, deserializeIfJSON_default;
var init_deserializeIfJSON = __esm({
"src/server/utils/deserializeIfJSON.ts"() {
"use strict";
init_utils();
({ EJSON } = mongoose.mongo.BSON);
EXTENDED_JSON_KEYS = [
"$oid",
"$date",
"$numberInt",
"$numberLong",
"$numberDouble",
"$numberDecimal",
"$regularExpression",
"$binary",
"$timestamp"
];
containsExtendedJsonMarkers = (text) => EXTENDED_JSON_KEYS.some((marker) => text.includes(`"${marker}"`));
deserializeIfJSON = (text) => {
if (isJSON_default(text)) {
if (containsExtendedJsonMarkers(text)) {
return EJSON.parse(text) || void 0;
}
return JSON.parse(text) || void 0;
}
return text;
};
deserializeIfJSON_default = deserializeIfJSON;
}
});
// src/server/utils/prettyCheckParams.ts
var init_prettyCheckParams = __esm({
"src/server/utils/prettyCheckParams.ts"() {
"use strict";
}
});
// src/server/utils/waitUntil.ts
var init_waitUntil = __esm({
"src/server/utils/waitUntil.ts"() {
"use strict";
}
});
// src/server/utils/imageUtils.ts
var init_imageUtils = __esm({
"src/server/utils/imageUtils.ts"() {
"use strict";
}
});
// src/server/utils/paramsGuard.ts
var init_paramsGuard = __esm({
"src/server/utils/paramsGuard.ts"() {
"use strict";
}
});
// src/server/utils/ident.ts
var ident;
var init_ident = __esm({
"src/server/utils/ident.ts"() {
"use strict";
ident = ["name", "viewport", "browserName", "os", "app", "branch"];
}
});
// src/server/utils/buildIdentObject.ts
var MissingIdentFieldError, buildIdentObject;
var init_buildIdentObject = __esm({
"src/server/utils/buildIdentObject.ts"() {
"use strict";
init_ident();
MissingIdentFieldError = class extends Error {
constructor(field) {
super(`Missing required ident field: ${field}`);
this.name = "MissingIdentFieldError";
}
};
buildIdentObject = (params) => {
const result = {};
for (const key of ident) {
if (key in params && params[key] !== void 0) {
result[key] = params[key];
} else {
throw new MissingIdentFieldError(key);
}
}
return result;
};
}
});
// src/server/models/plugins/paginate.plugin.ts
var paginate, paginate_plugin_default;
var init_paginate_plugin = __esm({
"src/server/models/plugins/paginate.plugin.ts"() {
"use strict";
paginate = (schema) => {
schema.statics.paginate = async function(filter, options) {
let sort;
if (options.sortBy) {
const sortingCriteria = [];
let primaryOrder = "desc";
options.sortBy.split(",").forEach((sortOption, index) => {
const [key, order] = sortOption.split(":");
if (index === 0) primaryOrder = order || "asc";
sortingCriteria.push((order === "desc" ? "-" : "") + key);
});
if (!sortingCriteria.some((s) => s === "_id" || s === "-_id")) {
sortingCriteria.push((primaryOrder === "desc" ? "-" : "") + "_id");
}
sort = sortingCriteria.join(" ");
} else {
sort = { _id: -1 };
}
const limit = options.limit && parseInt(options.limit.toString(), 10) >= 0 ? parseInt(options.limit.toString(), 10) : 10;
const page = options.page && parseInt(options.page.toString(), 10) > 0 ? parseInt(options.page.toString(), 10) : 1;
const skip = (page - 1) * limit;
const countStrategy = options.countStrategy ?? (filter && Object.keys(filter).length === 0 ? "estimated" : "exact");
const countPromise = countStrategy === "estimated" ? this.estimatedDocumentCount().exec() : this.countDocuments(filter).exec();
let docsPromise = this.find(filter).sort(sort).skip(skip).limit(limit);
if (options.populate) {
options.populate.split(",").forEach((populateOption) => {
docsPromise = docsPromise.populate(
populateOption.split(".").reverse().reduce((a, b) => ({ path: b, populate: a }))
);
});
}
docsPromise = docsPromise.exec();
return Promise.all([countPromise, docsPromise]).then((values) => {
const [totalResults, results] = values;
const totalPages = Math.ceil(totalResults / limit);
const result = {
results,
page,
limit,
totalPages,
totalResults,
timestamp: Number(Date.now() + String(process.hrtime()[1]).slice(3, 6))
};
return Promise.resolve(result);
});
};
};
paginate_plugin_default = paginate;
}
});
// src/server/models/plugins/toJSON.plugin.ts
var deleteAtPath, toJSON, toJSON_plugin_default;
var init_toJSON_plugin = __esm({
"src/server/models/plugins/toJSON.plugin.ts"() {
"use strict";
deleteAtPath = (obj, path8, index) => {
if (index === path8.length - 1) {
delete obj[path8[index]];
return;
}
deleteAtPath(obj[path8[index]], path8, index + 1);
};
toJSON = (schema) => {
let transform;
if (schema.options.toJSON && schema.options.toJSON.transform) {
transform = schema.options.toJSON.transform;
}
schema.options.toJSON = Object.assign(schema.options.toJSON || {}, {
transform(doc, ret, options) {
Object.keys(schema.paths).forEach((path8) => {
if (schema.paths[path8].options && schema.paths[path8].options.private) {
deleteAtPath(ret, path8.split("."), 0);
}
});
ret.id = ret._id.toString();
delete ret.__v;
delete ret.createdAt;
delete ret.updatedAt;
if (transform) {
return transform(doc, ret, options);
}
}
});
};
toJSON_plugin_default = toJSON;
}
});
// src/server/models/plugins/paginateDistinct.plugin.ts
var init_paginateDistinct_plugin = __esm({
"src/server/models/plugins/paginateDistinct.plugin.ts"() {
"use strict";
init_utils();
}
});
// src/server/models/plugins/index.ts
var init_plugins = __esm({
"src/server/models/plugins/index.ts"() {
"use strict";
init_paginate_plugin();
init_toJSON_plugin();
init_paginateDistinct_plugin();
}
});
// src/server/models/Check.model.ts
import mongoose2, { Schema } from "mongoose";
var CheckSchema, Check, Check_model_default;
var init_Check_model = __esm({
"src/server/models/Check.model.ts"() {
"use strict";
init_plugins();
CheckSchema = new Schema({
name: {
type: String,
required: [true, 'CheckSchema: The "name" field must be required']
},
test: {
type: Schema.Types.ObjectId,
ref: "VRSTest",
required: [true, 'CheckSchema: The "test" field must be required']
},
suite: {
type: Schema.Types.ObjectId,
ref: "VRSSuite",
required: [true, 'CheckSchema: The "suite" field must be required']
},
app: {
type: Schema.Types.ObjectId,
ref: "VRSApp",
required: [true, 'CheckSchema: The "app" field must be required']
},
branch: {
type: String
},
realBaselineId: {
type: Schema.Types.ObjectId,
ref: "VRSBaseline"
},
baselineId: {
type: Schema.Types.ObjectId,
ref: "VRSSnapshot"
},
actualSnapshotId: {
type: Schema.Types.ObjectId,
ref: "VRSSnapshot"
},
diffId: {
type: Schema.Types.ObjectId,
ref: "VRSSnapshot"
},
createdDate: {
type: Date,
required: true,
default: Date.now
},
updatedDate: {
type: Date
},
status: {
type: [{
type: String,
enum: {
values: ["new", "pending", "approved", "running", "passed", "failed", "aborted"],
message: "status is required"
}
}],
default: ["new"]
},
browserName: {
type: String
},
browserVersion: {
type: String
},
browserFullVersion: {
type: String
},
viewport: {
type: String
},
os: {
type: String
},
domDump: {
type: String
},
result: {
type: String,
default: "{}"
},
run: {
type: Schema.Types.ObjectId
},
markedAs: {
type: String,
enum: ["bug", "accepted"]
},
markedDate: {
type: Date
},
markedById: {
type: Schema.Types.ObjectId,
ref: "VRSUser"
},
markedByUsername: {
type: String
},
markedBugComment: {
type: String
},
creatorId: {
type: Schema.Types.ObjectId,
ref: "VRSUser"
},
creatorUsername: {
type: String
},
failReasons: {
type: [String]
},
vOffset: {
type: String
},
topStablePixels: {
type: String
},
toleranceThreshold: {
type: Number,
min: 0,
max: 100
},
meta: {
type: Object
}
});
CheckSchema.plugin(toJSON_plugin_default);
CheckSchema.plugin(paginate_plugin_default);
Check = mongoose2.model("VRSCheck", CheckSchema);
Check_model_default = Check;
}
});
// src/server/models/Log.model.ts
import mongoose3, { Schema as Schema2 } from "mongoose";
var LogSchema, Log;
var init_Log_model = __esm({
"src/server/models/Log.model.ts"() {
"use strict";
init_plugins();
LogSchema = new Schema2({
timestamp: {
type: Date
},
level: {
type: String
},
message: {
type: String
},
meta: {
type: Object
},
hostname: {
type: Object
}
});
LogSchema.plugin(toJSON_plugin_default);
LogSchema.plugin(paginate_plugin_default);
Log = mongoose3.model("VRSLog", LogSchema);
}
});
// src/server/models/App.model.ts
import mongoose4, { Schema as Schema3 } from "mongoose";
var AppSchema, App;
var init_App_model = __esm({
"src/server/models/App.model.ts"() {
"use strict";
init_plugins();
AppSchema = new Schema3({
name: {
type: String,
default: "Others",
unique: true,
required: [true, 'AppSchema: The "name" field must be required']
},
description: {
type: String
},
version: {
type: String
},
updatedDate: {
type: Date
},
createdDate: {
type: Date
},
meta: {
type: Object
}
});
AppSchema.plugin(paginate_plugin_default);
AppSchema.plugin(toJSON_plugin_default);
App = mongoose4.model("VRSApp", AppSchema);
}
});
// src/server/models/Snapshot.model.ts
import mongoose5, { Schema as Schema4 } from "mongoose";
var SnapshotSchema, Snapshot, Snapshot_model_default;
var init_Snapshot_model = __esm({
"src/server/models/Snapshot.model.ts"() {
"use strict";
init_plugins();
SnapshotSchema = new Schema4({
name: {
type: String,
required: [true, 'SnapshotSchema: The "name" field must be required']
},
path: {
type: String
},
filename: {
type: String
},
imghash: {
type: String,
required: [true, 'SnapshotSchema: The "imghash" field must be required']
},
createdDate: {
type: Date,
default: Date.now
},
vOffset: {
type: Number
},
hOffset: {
type: Number
}
});
SnapshotSchema.plugin(toJSON_plugin_default);
SnapshotSchema.plugin(paginate_plugin_default);
Snapshot = mongoose5.model("VRSSnapshot", SnapshotSchema);
Snapshot_model_default = Snapshot;
}
});
// src/server/models/AppSettings.model.ts
import mongoose6, { Schema as Schema5 } from "mongoose";
var AppSettingsSchema, AppSettings, AppSettings_model_default;
var init_AppSettings_model = __esm({
"src/server/models/AppSettings.model.ts"() {
"use strict";
init_plugins();
AppSettingsSchema = new Schema5({
name: {
type: String,
unique: true,
required: [true, 'AppSettingsSchema: The "name" field must be required']
},
label: {
type: String,
required: [true, 'AppSettingsSchema: The "label" field must be required']
},
description: {
type: String
},
type: {
type: String,
required: [true, 'AppSettingsSchema: The "type" field must be required']
},
value: {
type: Schema5.Types.Mixed,
required: [true, 'AppSettingsSchema: The "value" field must be required']
},
env_variable: {
type: String
},
enabled: {
type: Boolean
}
});
AppSettingsSchema.plugin(toJSON_plugin_default);
AppSettings = mongoose6.model("VRSAppSettings", AppSettingsSchema);
AppSettings_model_default = AppSettings;
}
});
// src/server/models/Suite.model.ts
import mongoose7, { Schema as Schema6 } from "mongoose";
var SuiteSchema, Suite, Suite_model_default;
var init_Suite_model = __esm({
"src/server/models/Suite.model.ts"() {
"use strict";
init_plugins();
SuiteSchema = new Schema6({
name: {
type: String,
default: "Others",
unique: true,
required: [true, 'SuiteSchema: The "name" field must be required']
},
tags: {
type: [String]
},
app: {
type: Schema6.Types.ObjectId,
ref: "VRSApp",
required: [true, 'SuiteSchema: The "app" field must be required']
},
description: {
type: String
},
updatedDate: {
type: Date,
default: Date.now
},
createdDate: {
type: Date
},
meta: {
type: Object
}
});
SuiteSchema.plugin(paginate_plugin_default);
SuiteSchema.plugin(toJSON_plugin_default);
Suite = mongoose7.model("VRSSuite", SuiteSchema);
Suite_model_default = Suite;
}
});
// src/server/models/Run.model.ts
import mongoose8, { Schema as Schema7 } from "mongoose";
var RunSchema, Run, Run_model_default;
var init_Run_model = __esm({
"src/server/models/Run.model.ts"() {
"use strict";
init_plugins();
RunSchema = new Schema7({
name: {
type: String,
required: [true, 'RunSchema: The "name" field must be required']
},
app: {
type: Schema7.Types.ObjectId,
ref: "VRSApp",
required: [true, 'RunSchema: The "app" field must be required']
},
ident: {
type: String,
unique: true,
required: [true, 'RunSchema: The "ident" field must be required']
},
description: {
type: String
},
updatedDate: {
type: Date,
default: Date.now
},
createdDate: {
type: Date
},
parameters: {
type: [String]
},
meta: {
type: Object
}
});
RunSchema.plugin(paginate_plugin_default);
RunSchema.plugin(toJSON_plugin_default);
Run = mongoose8.model("VRSRun", RunSchema);
Run_model_default = Run;
}
});
// src/server/models/User.model.ts
import mongoose9, { Schema as Schema8 } from "mongoose";
import plm from "passport-local-mongoose";
var passportLocalMongoose, UserSchema, User, User_model_default;
var init_User_model = __esm({
"src/server/models/User.model.ts"() {
"use strict";
init_plugins();
passportLocalMongoose = plm.default || plm;
UserSchema = new Schema8({
username: {
type: String,
unique: true,
required: [true, 'UserSchema: The "username" field must be required']
},
firstName: {
type: String,
required: [true, 'UserSchema: The "firstName" field must be required']
},
lastName: {
type: String,
required: [true, 'UserSchema: The "lastName" field must be required']
},
role: {
type: String,
enum: ["admin", "reviewer", "user"],
required: [true, 'UserSchema: The "role" field must be required']
},
provider: {
type: String,
default: "local"
},
providerId: {
type: String
},
password: {
type: String
},
token: {
type: String
},
apiKey: {
type: String
},
authSource: {
type: String,
enum: ["local", "jwt", "ldap", "api"],
default: "local"
},
createdDate: {
type: Date
},
updatedDate: {
type: Date
},
expiration: {
type: Date
},
meta: {
type: Object
}
});
UserSchema.statics.isEmailTaken = async function(username, excludeUserId) {
const user = await this.findOne({ username, _id: { $ne: excludeUserId } });
return !!user;
};
UserSchema.plugin(toJSON_plugin_default);
UserSchema.plugin(paginate_plugin_default);
UserSchema.plugin(passportLocalMongoose, { hashField: "password" });
User = mongoose9.model("VRSUser", UserSchema);
User_model_default = User;
}
});
// src/server/models/Baseline.model.ts
import mongoose10, { Schema as Schema9 } from "mongoose";
var BaselineSchema, Baseline, Baseline_model_default;
var init_Baseline_model = __esm({
"src/server/models/Baseline.model.ts"() {
"use strict";
init_plugins();
BaselineSchema = new Schema9({
snapshootId: {
type: Schema9.Types.ObjectId,
ref: "VRSSnapshot"
},
name: {
type: String,
required: [true, 'VRSBaselineSchema: The "name" field must be required']
},
app: {
type: Schema9.Types.ObjectId,
ref: "VRSApp",
required: [true, 'VRSBaselineSchema: The "app" field must be required']
},
branch: {
type: String
},
browserName: {
type: String
},
browserVersion: {
type: String
},
browserFullVersion: {
type: String
},
viewport: {
type: String
},
os: {
type: String
},
markedAs: {
type: String,
enum: ["bug", "accepted"]
},
lastMarkedDate: {
type: Date
},
createdDate: {
type: Date
},
updatedDate: {
type: Date
},
markedById: {
type: Schema9.Types.ObjectId,
ref: "VRSUser"
},
markedByUsername: {
type: String
},
ignoreRegions: {
type: String
},
boundRegions: {
type: String
},
matchType: {
type: String,
enum: ["antialiasing", "nothing", "colors"]
},
toleranceThreshold: {
type: Number,
default: 0,
min: 0,
max: 100
},
meta: {
type: Object
}
});
BaselineSchema.set("autoIndex", false);
BaselineSchema.plugin(toJSON_plugin_default);
BaselineSchema.plugin(paginate_plugin_default);
BaselineSchema.index({
name: 1,
app: 1,
branch: 1,
browserName: 1,
viewport: 1,
os: 1,
snapshootId: 1
}, { unique: true, name: "baseline_ident_snapshot_idx" });
Baseline = mongoose10.model("VRSBaseline", BaselineSchema);
Baseline_model_default = Baseline;
}
});
// src/server/models/Test.model.ts
import mongoose11, { Schema as Schema10 } from "mongoose";
var TestSchema, Test, Test_model_default;
var init_Test_model = __esm({
"src/server/models/Test.model.ts"() {
"use strict";
init_plugins();
init_utils();
TestSchema = new Schema10(
{
name: {
type: String,
required: "TestSchema: the test name is empty"
},
description: {
type: String
},
status: {
type: String
},
browserName: {
type: String
},
browserVersion: {
type: String
},
branch: {
type: String
},
tags: {
type: [String]
},
viewport: {
type: String
},
calculatedViewport: {
type: String
},
os: {
type: String
},
app: {
type: Schema10.Types.ObjectId,
ref: "VRSApp",
required: [true, 'TestSchema: The "app" field must be required']
},
blinking: {
type: Number,
default: 0
},
updatedDate: {
type: Date
},
startDate: {
type: Date
},
checks: [
{
type: mongoose11.Schema.Types.ObjectId,
ref: "VRSCheck"
}
],
suite: {
type: Schema10.Types.ObjectId,
ref: "VRSSuite"
},
run: {
type: Schema10.Types.ObjectId,
ref: "VRSRun"
},
markedAs: {
type: String,
enum: ["Bug", "Accepted", "Unaccepted", "Partially"]
},
creatorId: {
type: Schema10.Types.ObjectId,
ref: "VRSUser"
},
creatorUsername: {
type: String
},
meta: {
type: Object
}
},
{ strictQuery: true }
);
TestSchema.plugin(toJSON_plugin_default);
TestSchema.plugin(paginate_plugin_default);
TestSchema.statics.paginateDistinct = async function(filter, options) {
let sort = { _id: -1 };
if (options.sortBy) {
sort = {};
options.sortBy.split(",").forEach((sortOption) => {
const [key, order] = sortOption.split(":");
sort[key] = order === "desc" ? -1 : 1;
});
}
let limit = options.limit && parseInt(options.limit.toString(), 10) >= 0 ? parseInt(options.limit.toString(), 10) : 10;
limit = limit === 0 ? Number.MAX_SAFE_INTEGER : limit;
const page = options.page && parseInt(options.page.toString(), 10) > 0 ? parseInt(options.page.toString(), 10) : 1;
const skip = (page - 1) * limit;
const groupAggregateObj = { $group: { _id: `$${options.field}` } };
const parsedFilter = typeof filter?.filter === "string" ? deserializeIfJSON_default(filter.filter) || {} : {};
const documentsCount = (await this.aggregate([{ $match: parsedFilter }, groupAggregateObj]).exec()).length;
const aggregatedDocs = (await this.aggregate([
{ $match: parsedFilter },
groupAggregateObj,
{ $sort: sort },
{ $skip: skip },
{ $limit: limit }
])).filter((x) => x._id).map((x) => {
const fieldValue = options.field ? x[options.field] : void 0;
if (Array.isArray(fieldValue) && fieldValue.length > 0) {
return fieldValue[0];
}
return { name: x._id };
});
const totalPages = Math.ceil(documentsCount / limit);
return {
results: aggregatedDocs,
page,
limit,
totalPages,
totalResults: documentsCount,
timestamp: Date.now()
};
};
Test = mongoose11.model("VRSTest", TestSchema);
Test_model_default = Test;
}
});
// src/server/models/Webhook.model.ts
import mongoose12, { Schema as Schema11 } from "mongoose";
var WebhookSchema, Webhook, Webhook_model_default;
var init_Webhook_model = __esm({
"src/server/models/Webhook.model.ts"() {
"use strict";
init_plugins();
WebhookSchema = new Schema11({
url: {
type: String,
required: [true, 'WebhookSchema: The "url" field must be required']
},
events: {
type: [String],
default: ["check.updated", "check.created"]
},
secret: {
type: String
},
createdDate: {
type: Date,
default: Date.now
},
meta: {
type: Object
}
});
WebhookSchema.plugin(paginate_plugin_default);
WebhookSchema.plugin(toJSON_plugin_default);
Webhook = mongoose12.model("VRSWebhook", WebhookSchema);
Webhook_model_default = Webhook;
}
});
// src/server/models/ShareToken.model.ts
import mongoose13, { Schema as Schema12 } from "mongoose";
var ShareTokenSchema, ShareToken;
var init_ShareToken_model = __esm({
"src/server/models/ShareToken.model.ts"() {
"use strict";
init_plugins();
ShareTokenSchema = new Schema12({
checkId: {
type: Schema12.Types.ObjectId,
ref: "VRSCheck",
required: [true, 'ShareTokenSchema: The "checkId" field is required'],
index: true
},
token: {
type: String,
required: [true, 'ShareTokenSchema: The "token" field is required'],
unique: true,
index: true
},
createdById: {
type: Schema12.Types.ObjectId,
ref: "VRSUser",
required: [true, 'ShareTokenSchema: The "createdById" field is required']
},
createdByUsername: {
type: String,
required: [true, 'ShareTokenSchema: The "createdByUsername" field is required']
},
createdDate: {
type: Date,
required: true,
default: Date.now
},
isRevoked: {
type: Boolean,
default: false
},
revokedDate: {
type: Date
},
revokedById: {
type: Schema12.Types.ObjectId,
ref: "VRSUser"
},
revokedByUsername: {
type: String
}
});
ShareTokenSchema.plugin(toJSON_plugin_default);
ShareTokenSchema.plugin(paginate_plugin_default);
ShareToken = mongoose13.model("VRSShareToken", ShareTokenSchema);
}
});
// src/server/models/DomSnapshot.model.ts
import mongoose14, { Schema as Schema13 } from "mongoose";
var DomSnapshotSchema, DomSnapshot, DomSnapshot_model_default;
var init_DomSnapshot_model = __esm({
"src/server/models/DomSnapshot.model.ts"() {
"use strict";
init_plugins();
DomSnapshotSchema = new Schema13({
checkId: {
type: Schema13.Types.ObjectId,
ref: "VRSCheck",
required: [true, 'DomSnapshotSchema: The "checkId" field is required'],
index: true
},
baselineId: {
type: Schema13.Types.ObjectId,
ref: "VRSBaseline",
index: true
},
type: {
type: String,
enum: ["actual", "baseline"],
required: [true, 'DomSnapshotSchema: The "type" field is required']
},
filename: {
type: String,
required: [true, 'DomSnapshotSchema: The "filename" field is required']
},
hash: {
type: String,
required: [true, 'DomSnapshotSchema: The "hash" field is required'],
index: true
},
compressed: {
type: Boolean,
default: false
},
originalSize: {
type: Number,
required: [true, 'DomSnapshotSchema: The "originalSize" field is required']
},
compressedSize: {
type: Number
},
createdDate: {
type: Date,
default: Date.now
}
});
DomSnapshotSchema.index({ checkId: 1, type: 1 });
DomSnapshotSchema.index({ baselineId: 1, type: 1 });
DomSnapshotSchema.plugin(toJSON_plugin_default);
DomSnapshotSchema.plugin(paginate_plugin_default);
DomSnapshot = mongoose14.model("VRSDomSnapshot", DomSnapshotSchema);
DomSnapshot_model_default = DomSnapshot;
}
});
// src/server/models/PluginSettings.model.ts
import mongoose15, { Schema as Schema14 } from "mongoose";
var PluginSettingSchemaDefinition, PluginSettingsSchema, PluginSettings;
var init_PluginSettings_model = __esm({
"src/server/models/PluginSettings.model.ts"() {
"use strict";
init_plugins();
PluginSettingSchemaDefinition = new Schema14({
key: { type: String, required: true },
label: { type: String, required: true },
description: { type: String },
type: {
type: String,
enum: ["string", "number", "boolean", "select", "password"],
required: true
},
defaultValue: { type: Schema14.Types.Mixed },
envVariable: { type: String },
options: [{ value: String, label: String }],
required: { type: Boolean, default: false }
}, { _id: false });
PluginSettingsSchema = new Schema14({
pluginName: {
type: String,
unique: true,
required: [true, "PluginSettings: pluginName is required"],
index: true
},
displayName: {
type: String,
required: [true, "PluginSettings: displayName is required"]
},
description: {
type: String
},
enabled: {
type: Boolean,
default: false
},
settings: {
type: Schema14.Types.Mixed,
default: {}
},
settingsSchema: {
type: [PluginSettingSchemaDefinition],
default: []
}
}, {
timestamps: true
});
PluginSettingsSchema.plugin(toJSON_plugin_default);
PluginSettingsSchema.statics.getEffectiveConfig = async function(pluginName, envPrefix = "SYNGRISI_PLUGIN_") {
const doc = await this.findOne({ pluginName });
const envPluginKey = pluginName.toUpperCase().replace(/-/g, "_");
const envEnabledKey = `${envPrefix}${envPluginKey}_ENABLED`;
const envEnabled = process.env[envEnabledKey]?.toLowerCase() === "true";
const dbEnabled = doc?.enabled;
const enabled = doc ? dbEnabled : envEnabled;
const config2 = {};
const schema = doc?.settingsSchema || [];
if (schema.length === 0 && doc?.settings && Object.keys(doc.settings).length > 0) {
for (const [key, value] of Object.entries(doc.settings)) {
config2[key] = { value, source: "db" };
}
return { config: config2, enabled };
}
for (const field of schema) {
const envKey = field.envVariable || `${envPrefix}${envPluginKey}_${field.key.toUpperCase()}`;
const envValue = process.env[envKey];
const dbValue = doc?.settings?.[field.key];
if (dbValue !== void 0) {
config2[field.key] = { value: dbValue, source: "db" };
} else if (envValue !== void 0) {
let parsedValue = envValue;
if (field.type === "boolean") {
parsedValue = envValue.toLowerCase() === "true";
} else if (field.type === "number") {
parsedValue = parseFloat(envValue);
}
config2[field.key] = { value: parsedValue, source: "env" };
} else if (field.defaultValue !== void 0) {
config2[field.key] = { value: field.defaultValue, source: "default" };
}
}
return { config: config2, enabled };
};
PluginSettingsSchema.statics.upsertSettings = async function(pluginName, updates) {
return this.findOneAndUpdate(
{ pluginName },
{ $set: updates },
{ upsert: true, new: true, runValidators: true }
);
};
PluginSettings = mongoose15.model(
"VRSPluginSettings",
PluginSettingsSchema
);
}
});
// src/server/models/index.ts
var init_models = __esm({
"src/server/models/index.ts"() {
"use strict";
init_Check_model();
init_Log_model();
init_App_model();
init_Snapshot_model();
init_AppSettings_model();
init_Suite_model();
init_Run_model();
init_User_model();
init_Baseline_model();
init_Test_model();
init_Webhook_model();
init_ShareToken_model();
init_DomSnapshot_model();
init_PluginSettings_model();
}
});
// src/server/utils/calculateAcceptedStatus.ts
var calculateAcceptedStatus;
var init_calculateAcceptedStatus = __esm({
"src/server/utils/calculateAcceptedStatus.ts"() {
"use strict";
init_models();
calculateAcceptedStatus = async function calculateAcceptedStatus2(testId) {
const checksInTest = await Check_model_default.find({ test: testId });
const statuses = checksInTest.map((x) => x.markedAs);
if (statuses.length < 1) {
return "Unaccepted";
}
let testCalculatedStatus = "Unaccepted";
if (statuses.some((x) => x === "accepted")) {
testCalculatedStatus = "Partially";
}
if (statuses.every((x) => x === "accepted")) {
testCalculatedStatus = "Accepted";
}
return testCalculatedStatus;
};
}
});
// src/server/utils/subDays.ts
var init_subDays = __esm({
"src/server/utils/subDays.ts"() {
"use strict";
}
});
// src/server/utils/errMsg.ts
var errMsg;
var init_errMsg = __esm({
"src/server/utils/errMsg.ts"() {
"use strict";
errMsg = (e) => {
return String(e instanceof Error ? e.stack : e);
};
}
});
// src/server/utils/hash.ts
import { createHash, randomUUID } from "crypto";
function hashSync(input) {
return createHash("sha512").update(input).digest("hex");
}
var init_hash = __esm({
"src/server/utils/hash.ts"() {
"use strict";
}
});
// src/server/utils/stringTable.ts
var init_stringTable = __esm({
"src/server/utils/stringTable.ts"() {
"use strict";
}
});
// src/server/utils/colors.ts
var RESET, colors;
var init_colors = __esm({
"src/server/utils/colors.ts"() {
"use strict";
RESET = "\x1B[0m";
colors = {
// Standard colors
blue: (s) => `\x1B[34m${s}${RESET}`,
green: (s) => `\x1B[32m${s}${RESET}`,
red: (s) => `\x1B[31m${s}${RESET}`,
yellow: (s) => `\x1B[33m${s}${RESET}`,
magenta: (s) => `\x1B[35m${s}${RESET}`,
cyan: (s) => `\x1B[36m${s}${RESET}`,
// Bright colors
gray: (s) => `\x1B[90m${s}${RESET}`,
whiteBright: (s) => `\x1B[97m${s}${RESET}`,
// Reset code for manual use
reset: RESET
};
}
});
// src/server/utils/httpStatus.ts
var HttpStatus, httpStatus_default;
var init_httpStatus = __esm({
"src/server/utils/httpStatus.ts"() {
"use strict";
HttpStatus = {
// 2xx Success
OK: 200,
CREATED: 201,
NO_CONTENT: 204,
// 4xx Client Errors
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
// 5xx Server Errors
INTERNAL_SERVER_ERROR: 500
};
httpStatus_default = HttpStatus;
}
});
// src/server/utils/cookieParser.ts
var init_cookieParser = __esm({
"src/server/utils/cookieParser.ts"() {
"use strict";
}
});
// src/server/utils/index.ts
var init_utils = __esm({
"src/server/utils/index.ts"() {
"use strict";
init_pick();
init_isJSON();
init_catchAsync();
init_dateToISO8601();
init_ProgressBar();
init_ApiError();
init_removeEmptyProperties();
init_deserializeIfJSON();
init_prettyCheckParams();
init_waitUntil();
init_imageUtils();
init_paramsGuard();
init_buildIdentObject();
init_calculateAcceptedStatus();
init_ident();
init_subDays();
init_errMsg();
init_hash();
init_stringTable();
init_colors();
init_httpStatus();
init_cookieParser();
}
});
// src/server/utils/formatISOToDateTime.ts
function formatISOToDateTime(isoDateString) {
const date = new Date(isoDateString);
return `${date.toISOString().slice(0, 10)} ${date.toTimeString().slice(0, 8)}`;
}
var formatISOToDateTime_default;
var init_formatISOToDateTime = __esm({
"src/server/utils/formatISOToDateTime.ts"() {
"use strict";
formatISOToDateTime_default = formatISOToDateTime;
}
});
// package.json
var version, gitHead;
var init_package = __esm({
"package.json"() {
version = "3.5.0";
gitHead = "12bfda406cbe5aaccf3f17fdab02a9bd1a9d6343";
}
});
// src/server/envConfig.ts
import { cleanEnv, host, num, port, str, bool } from "envalid";
import crypto from "crypto";
import path from "path";
import dotenv from "dotenv";
var env;
var init_envConfig = __esm({
"src/server/envConfig.ts"() {
"use strict";
dotenv.config({ quiet: true });
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = "production";
}
env = cleanEnv(process.env, {
NODE_ENV: str({ choices: ["development", "production", "test"] }),
SYNGRISI_DB_URI: str({ default: "mongodb://127.0.0.1:27017/SyngrisiDb" }),
SYNGRISI_APP_PORT: port({ default: 3e3 }),
SYNGRISI_IMAGES_PATH: str({ default: path.join(process.cwd(), "./.snapshots-images") }),
SYNGRISI_DOM_SNAPSHOTS_PATH: str({ default: "" }),
// If empty, uses SYNGRISI_IMAGES_PATH
SYNGRISI_TMP_DIR: str({ default: path.join(process.cwd(), ".tmp") }),
SYNGRISI_ADMIN_DATA_JOBS_PATH: str({ default: path.join(process.cwd(), ".tmp", "admin-data-jobs") }),
SYNGRISI_ADMIN_DATA_JOBS_TTL_MS: num({ default: 24 * 60 * 60 * 1e3 }),
SYNGRISI_ADMIN_DATA_MAX_CONCURRENT_JOBS: num({ default: 1 }),
SYNGRISI_ADMIN_DATA_UPLOAD_MAX_SIZE_MB: num({ default: 10240 }),
SYNGRISI_HTTP_LOG: bool({ default: false }),
SYNGRISI_COVERAGE: bool({ default: false }),
SYNGRISI_HOSTNAME: host({ default: "localhost" }),
SYNGRISI_AUTH: bool({ default: true }),
SYNGRISI_TEST_MODE: bool({ default: false }),
SYNGRISI_DISABLE_FIRST_RUN: bool({ default: false }),
MONGODB_ROOT_USERNAME: str({ default: "" }),
MONGODB_ROOT_PASSWORD: str({ default: "" }),
LOGLEVEL: str({ choices: ["error", "warn", "info", "verbose", "debug", "silly"], default: "debug" }),
// Legacy tests expect 20 rows per page; keep default aligned for e2e
SYNGRISI_PAGINATION_SIZE: num({ default: 20 }),
SYNGRISI_DISABLE_DEV_CORS: bool({ default: true, devDefault: true }),
SYNGRISI_SESSION_STORE_KEY: str({ default: crypto.randomBytes(64).toString("hex") }),
SYNGRISI_LOG_LEVEL: str({ default: "debug" }),
SYNGRISI_DISABLE_LOGS: bool({ default: false }),
SYNGRISI_AUTO_REMOVE_CHECKS_POLL_INTERVAL_MS: num({ default: 10 * 60 * 1e3 }),
// 10 minutes
SYNGRISI_AUTO_REMOVE_CHECKS_MIN_INTERVAL_MS: num({ default: 24 * 60 * 60 * 1e3 }),
SYNGRISI_ENABLE_SCHEDULERS_IN_TEST_MODE: bool({ default: false }),
// RCA
SYNGRISI_RCA: bool({ default: false }),
// trunk features
SYNGRISI_TRUNK_FEATURE_AI_SEVERITY: bool({ default: false }),
SYNGRISI_AI_KEY: str({ default: "" }),
OPENAI_API_BASE_URL: str({ default: "https://api.openai.com/v1" }),
OPENAI_API_KEY: str({ default: "" }),
SYNGRISI_V8_COVERAGE_ON_EXIT: bool({ default: false }),
// Rate Limiting
SYNGRISI_RATE_LIMIT_WINDOW_MS: num({ default: 15 * 60 * 1e3 }),
// 15 minutes
SYNGRISI_RATE_LIMIT_MAX: num({ default: 5e4 }),
SYNGRISI_AUTH_RATE_LIMIT_WINDOW_MS: num({ default: 15 * 60 * 1e3 }),
// 15 minutes
SYNGRISI_AUTH_RATE_LIMIT_MAX: num({ default: 200 }),
// Mongo tuneables for tests/CI flake reduction
SYNGRISI_MONGO_SOCKET_TIMEOUT_MS: num({ default: 6e4 }),
SYNGRISI_MONGO_MAX_POOL_SIZE: num({ default: 20 }),
SYNGRISI_MONGO_MIN_POOL_SIZE: num({ default: 2 }),
SYNGRISI_MONGO_MAX_IDLE_TIME_MS: num({ default: 3e4 }),
SYNGRISI_MONGO_WAIT_QUEUE_TIMEOUT_MS: num({ default: 3e4 }),
SYNGRISI_MONGO_SERVER_SELECTION_TIMEOUT_MS: num({ default: 1e4 }),
SYNGRISI_MONGO_CONNECT_TIMEOUT_MS: num({ default: 3e4 }),
// SSO Configuration
SSO_ENABLED: bool({ default: false }),
SSO_PROTOCOL: str({ choices: ["", "oauth2", "saml"], default: "" }),
SSO_CLIENT_ID: str({ default: "" }),
SSO_CLIENT_SECRET: str({ default: "" }),
SSO_AUTHORIZATION_URL: str({ default: "" }),
SSO_TOKEN_URL: str({ default: "" }),
SSO_USERINFO_URL: str({ default: "" }),
SSO_CALLBACK_URL: str({ default: "/v1/auth/sso/oauth/callback" }),
// SAML specific
SSO_ENTRY_POINT: str({ default: "" }),
SSO_ISSUER: str({ default: "" }),
SSO_CERT: str({ default: "" }),
SSO_IDP_ISSUER: str({ default: "" }),
SSO_IDP_METADATA_URL: str({ default: "" }),
// URL to fetch IdP metadata XML (alternative to manual SSO_ENTRY_POINT/SSO_CERT)
// SSO user settings
SSO_DEFAULT_ROLE: str({ choices: ["", "user", "admin", "reviewer"], default: "reviewer" }),
SSO_AUTO_CREATE_USERS: bool({ default: true }),
SSO_ALLOW_ACCOUNT_LINKING: bool({ default: true }),
// Plugin System
SYNGRISI_PLUGINS_ENABLED: str({ default: "" }),
// Comma-separated list of enabled plugins
SYNGRISI_PLUGINS_DIR: str({ default: "" }),
// Directory for external plugins
// Okta Auth Plugin
// Deprecated: Use SYNGRISI_PLUGIN_JWT_AUTH_* variables instead
OKTA_JWKS_URL: str({ default: "" }),
OKTA_ISSUER: str({ default: "" }),
OKTA_SERVICE_USER_ROLE: str({ default: "" }),
OKTA_AUTH_HEADER: str({ default: "" }),
// Custom Check Validator Plugin
CHECK_MISMATCH_THRESHOLD: str({ default: "0" }),
// Mismatch % below which checks pass
CHECK_VALIDATOR_SCRIPT: str({ default: "" })
// Path to custom validation script
});
}
});
// src/server/data/devices.json
var devices_default;
var init_devices = __esm({
"src/server/data/devices.json"() {
devices_default = [
{
os: "ios",
os_version: "16",
device: "iPhone 14 Pro Max",
realMobile: true
},
{
os: "ios",
os_version: "16",
device: "iPhone 14 Pro",
realMobile: true
},
{
os: "ios",
os_version: "16",
device: "iPhone 14 Plus",
realMobile: true
},
{
os: "ios",
os_version: "16",
device: "iPhone 14",
realMobile: true
},
{
os: "ios",
os_version: "16",
device: "iPhone 12 Pro Max",
realMobile: true
},
{
os: "ios",
os_version: "16",
device: "iPhone 12 Pro",
realMobile: true
},
{
os: "ios",
os_version: "16",
device: "iPhone 12 Mini",
realMobile: true
},
{
os: "ios",
os_version: "16",
device: "iPhone 11 Pro Max",
realMobile: true
},
{
os: "ios",
os_version: "15",
device: "iPhone XS",
realMobile: true
},
{
os: "ios",
os_version: "15",
device: "iPhone 13 Pro Max",
realMobile: true
},
{
os: "ios",
os_version: "15",
device: "iPhone 13 Pro",
realMobile: true
},
{
os: "ios",
os_version: "15",
device: "iPhone 13 Mini",
realMobile: true
},
{
os: "ios",
os_version: "15",
device: "iPhone 13",
realMobile: true
},
{
os: "ios",
os_version: "15",
device: "iPhone 11 Pro",
realMobile: true
},
{
os: "ios",
os_version: "15",
device: "iPhone 11",
realMobile: true
},
{
os: "ios",
os_version: "14",
device: "iPhone XS",
realMobile: true
},
{
os: "ios",
os_version: "14",
device: "iPhone 12 Pro Max",
realMobile: true
},
{
os: "ios",
os_version: "14",
device: "iPhone 12 Pro",
realMobile: true
},
{
os: "ios",
os_version: "14",
device: "iPhone 12 Mini",
realMobile: true
},
{
os: "ios",
os_version: "14",
device: "iPhone 12",
realMobile: true
},
{
os: "ios",
os_version: "14",
device: "iPhone 11 Pro Max",
realMobile: true
},
{
os: "ios",
os_version: "14",
device: "iPhone 11",
realMobile: true
},
{
os: "ios",
os_version: "13",
device: "iPhone XS",
realMobile: true
},
{
os: "ios",
os_version: "13",
device: "iPhone 11 Pro Max",
realMobile: true
},
{
os: "ios",
os_version: "13",
device: "iPhone 11 Pro",
realMobile: true
},
{
os: "ios",
os_version: "13",
device: "iPhone 11",
realMobile: true
},
{
os: "ios",
os_version: "12",
device: "iPhone XS",
realMobile: true
},
{
os: "ios",
os_version: "12",
device: "iPhone XS Max",
realMobile: true
},
{
os: "ios",
os_version: "15",
device: "iPhone XR",
realMobile: true
},
{
os: "ios",
os_version: "12",
device: "iPhone XR",
realMobile: true
},
{
os: "ios",
os_version: "11",
device: "iPhone X",
realMobile: true
},
{
os: "ios",
os_version: "15",
device: "iPhone 8",
realMobile: true
},
{
os: "ios",
os_version: "13",
device: "iPhone 8",
realMobile: true
},
{
os: "ios",
os_version: "12",
device: "iPhone 8",
realMobile: true
},
{
os: "ios",
os_version: "11",
device: "iPhone 8",
realMobile: true
},
{
os: "ios",
os_version: "12",
device: "iPhone 8 Plus",
realMobile: true
},
{
os: "ios",
os_version: "11",
device: "iPhone 8 Plus",
realMobile: true
},
{
os: "ios",
os_version: "12",
device: "iPhone 7",
realMobile: true
},
{
os: "ios",
os_version: "10",
device: "iPhone 7",
realMobile: true
},
{
os: "ios",
os_version: "12",
device: "iPhone 6S",
realMobile: true
},
{
os: "ios",
os_version: "11",
device: "iPhone 6S",
realMobile: true
},
{
os: "ios",
os_version: "11",
device: "iPhone 6S Plus",
realMobile: true
},
{
os: "ios",
os_version: "11",
device: "iPhone 6",
realMobile: true
},
{
os: "ios",
os_version: "15",
device: "iPhone SE 2022",
realMobile: true
},
{
os: "ios",
os_version: "13",
device: "iPhone SE 2020",
realMobile: true
},
{
os: "ios",
os_version: "11",
device: "iPhone SE",
realMobile: true
},
{
os: "ios",
os_version: "14",
device: "iPad Air 4",
realMobile: true
},
{
os: "ios",
os_version: "15",
device: "iPad 9th",
realMobile: true
},
{
os: "ios",
os_version: "16",
device: "iPad Pro 12.9 2022",
realMobile: true
},
{
os: "ios",
os_version: "16",
device: "iPad Pro 12.9 2020",
realMobile: true
},
{
os: "ios",
os_version: "16",
device: "iPad Pro 11 2022",
realMobile: true
},
{
os: "ios",
os_version: "16",
device: "iPad 10th",
realMobile: true
},
{
os: "ios",
os_version: "15",
device: "iPad Air 5",
realMobile: true
},
{
os: "ios",
os_version: "14",
device: "iPad Pro 12.9 2021",
realMobile: true
},
{
os: "ios",
os_version: "14",
device: "iPad Pro 12.9 2020",
realMobile: true
},
{
os: "ios",
os_version: "14",
device: "iPad Pro 11 2021",
realMobile: true
},
{
os: "ios",
os_version: "13",
device: "iPad Pro 12.9 2020",
realMobile: true
},
{
os: "ios",
os_version: "16",
device: "iPad 8th",
realMobile: true
},
{
os: "ios",
os_version: "15",
device: "iPad Pro 12.9 2018",
realMobile: true
},
{
os: "ios",
os_version: "15",
device: "iPad Mini 2021",
realMobile: true
},
{
os: "ios",
os_version: "14",
device: "iPad 8th",
realMobile: true
},
{
os: "ios",
os_version: "13",
device: "iPad Pro 12.9 2018",
realMobile: true
},
{
os: "ios",
os_version: "13",
device: "iPad Pro 11 2020",
realMobile: true
},
{
os: "ios",
os_version: "13",
device: "iPad Mini 2019",
realMobile: true
},
{
os: "ios",
os_version: "13",
device: "iPad Air 2019",
realMobile: true
},
{
os: "ios",
os_version: "13",
device: "iPad 7th",
realMobile: true
},
{
os: "ios",
os_version: "12",
device: "iPad Pro 12.9 2018",
realMobile: true
},
{
os: "ios",
os_version: "12",
device: "iPad Pro 11 2018",
realMobile: true
},
{
os: "ios",
os_version: "12",
device: "iPad Mini 2019",
realMobile: true
},
{
os: "ios",
os_version: "12",
device: "iPad Air 2019",
realMobile: true
},
{
os: "ios",
os_version: "11",
device: "iPad Pro 9.7 2016",
realMobile: true
},
{
os: "ios",
os_version: "11",
device: "iPad Pro 12.9 2017",
realMobile: true
},
{
os: "ios",
os_version: "11",
device: "iPad Mini 4",
realMobile: true
},
{
os: "ios",
os_version: "11",
device: "iPad 6th",
realMobile: true
},
{
os: "ios",
os_version: "11",
device: "iPad 5th",
realMobile: true
},
{
os: "android",
os_version: "12.0",
device: "Samsung Galaxy S22 Ultra",
realMobile: true
},
{
os: "android",
os_version: "12.0",
device: "Samsung Galaxy S22 Plus",
realMobile: true
},
{
os: "android",
os_version: "12.0",
device: "Samsung Galaxy S22",
realMobile: true
},
{
os: "android",
os_version: "12.0",
device: "Samsung Galaxy S21",
realMobile: true
},
{
os: "android",
os_version: "11.0",
device: "Samsung Galaxy S21 Ultra",
realMobile: true
},
{
os: "android",
os_version: "11.0",
device: "Samsung Galaxy S21",
realMobile: true
},
{
os: "android",
os_version: "11.0",
device: "Samsung Galaxy S21 Plus",
realMobile: true
},
{
os: "android",
os_version: "10.0",
device: "Samsung Galaxy S20",
realMobile: true
},
{
os: "android",
os_version: "10.0",
device: "Samsung Galaxy S20 Plus",
realMobile: true
},
{
os: "android",
os_version: "10.0",
device: "Samsung Galaxy S20 Ultra",
realMobile: true
},
{
os: "android",
os_version: "11.0",
device: "Samsung Galaxy M52",
realMobile: true
},
{
os: "android",
os_version: "11.0",
device: "Samsung Galaxy M32",
realMobile: true
},
{
os: "android",
os_version: "11.0",
device: "Samsung Galaxy A52",
realMobile: true
},
{
os: "android",
os_version: "10.0",
device: "Samsung Galaxy Note 20 Ultra",
realMobile: true
},
{
os: "android",
os_version: "10.0",
device: "Samsung Galaxy Note 20",
realMobile: true
},
{
os: "android",
os_version: "10.0",
device: "Samsung Galaxy A51",
realMobile: true
},
{
os: "android",
os_version: "10.0",
device: "Samsung Galaxy A11",
realMobile: true
},
{
os: "android",
os_version: "9.0",
device: "Samsung Galaxy S9 Plus",
realMobile: true
},
{
os: "android",
os_version: "9.0",
device: "Samsung Galaxy S10e",
realMobile: true
},
{
os: "android",
os_version: "9.0",
device: "Samsung Galaxy S10 Plus",
realMobile: true
},
{
os: "android",
os_version: "9.0",
device: "Samsung Galaxy S10",
realMobile: true
},
{
os: "android",
os_version: "9.0",
device: "Samsung Galaxy Note 10 Plus",
realMobile: true
},
{
os: "android",
os_version: "9.0",
device: "Samsung Galaxy Note 10",
realMobile: true
},
{
os: "android",
os_version: "9.0",
device: "Samsung Galaxy A10",
realMobile: true
},
{
os: "android",
os_version: "8.1",
device: "Samsung Galaxy Note 9",
realMobile: true
},
{
os: "android",
os_version: "8.1",
device: "Samsung Galaxy J7 Prime",
realMobile: true
},
{
os: "android",
os_version: "8.0",
device: "Samsung Galaxy S9 Plus",
realMobile: true
},
{
os: "android",
os_version: "8.0",
device: "Samsung Galaxy S9",
realMobile: true
},
{
os: "android",
os_version: "7.1",
device: "Samsung Galaxy Note 8",
realMobile: true
},
{
os: "android",
os_version: "7.1",
device: "Samsung Galaxy A8",
realMobile: true
},
{
os: "android",
os_version: "7.0",
device: "Samsung Galaxy S8 Plus",
realMobile: true
},
{
os: "android",
os_version: "7.0",
device: "Samsung Galaxy S8",
realMobile: true
},
{
os: "android",
os_version: "6.0",
device: "Samsung Galaxy S7",
realMobile: true
},
{
os: "android",
os_version: "5.0",
device: "Samsung Galaxy S6",
realMobile: true
},
{
os: "android",
os_version: "13.0",
device: "Google Pixel 7 Pro",
realMobile: true
},
{
os: "android",
os_version: "13.0",
device: "Google Pixel 7",
realMobile: true
},
{
os: "android",
os_version: "13.0",
device: "Google Pixel 6 Pro",
realMobile: true
},
{
os: "android",
os_version: "12.0",
device: "Google Pixel 6 Pro",
realMobile: true
},
{
os: "android",
os_version: "12.0",
device: "Google Pixel 6",
realMobile: true
},
{
os: "android",
os_version: "12.0",
device: "Google Pixel 5",
realMobile: true
},
{
os: "android",
os_version: "11.0",
device: "Google Pixel 5",
realMobile: true
},
{
os: "android",
os_version: "11.0",
device: "Google Pixel 4",
realMobile: true
},
{
os: "android",
os_version: "10.0",
device: "Google Pixel 4 XL",
realMobile: true
},
{
os: "android",
os_version: "10.0",
device: "Google Pixel 4",
realMobile: true
},
{
os: "android",
os_version: "10.0",
device: "Google Pixel 3",
realMobile: true
},
{
os: "android",
os_version: "9.0",
device: "Google Pixel 3a XL",
realMobile: true
},
{
os: "android",
os_version: "9.0",
device: "Google Pixel 3a",
realMobile: true
},
{
os: "android",
os_version: "9.0",
device: "Google Pixel 3 XL",
realMobile: true
},
{
os: "android",
os_version: "9.0",
device: "Google Pixel 3",
realMobile: true
},
{
os: "android",
os_version: "9.0",
device: "Google Pixel 2",
realMobile: true
},
{
os: "android",
os_version: "8.0",
device: "Google Pixel 2",
realMobile: true
},
{
os: "android",
os_version: "7.1",
device: "Google Pixel",
realMobile: true
},
{
os: "android",
os_version: "6.0",
device: "Google Nexus 6",
realMobile: true
},
{
os: "android",
os_version: "4.4",
device: "Google Nexus 5",
realMobile: true
},
{
os: "android",
os_version: "11.0",
device: "OnePlus 9",
realMobile: true
},
{
os: "android",
os_version: "10.0",
device: "OnePlus 8",
realMobile: true
},
{
os: "android",
os_version: "10.0",
device: "OnePlus 7T",
realMobile: true
},
{
os: "android",
os_version: "9.0",
device: "OnePlus 7",
realMobile: true
},
{
os: "android",
os_version: "9.0",
device: "OnePlus 6T",
realMobile: true
},
{
os: "android",
os_version: "11.0",
device: "Xiaomi Redmi Note 11",
realMobile: true
},
{
os: "android",
os_version: "10.0",
device: "Xiaomi Redmi Note 9",
realMobile: true
},
{
os: "android",
os_version: "9.0",
device: "Xiaomi Redmi Note 8",
realMobile: true
},
{
os: "android",
os_version: "9.0",
device: "Xiaomi Redmi Note 7",
realMobile: true
},
{
os: "android",
os_version: "11.0",
device: "Vivo Y21",
realMobile: true
},
{
os: "android",
os_version: "11.0",
device: "Vivo V21",
realMobile: true
},
{
os: "android",
os_version: "10.0",
device: "Vivo Y50",
realMobile: true
},
{
os: "android",
os_version: "11.0",
device: "Oppo Reno 6",
realMobile: true
},
{
os: "android",
os_version: "11.0",
device: "Oppo A96",
realMobile: true
},
{
os: "android",
os_version: "10.0",
device: "Oppo Reno 3 Pro",
realMobile: true
},
{
os: "android",
os_version: "11.0",
device: "Motorola Moto G71 5G",
realMobile: true
},
{
os: "android",
os_version: "10.0",
device: "Motorola Moto G9 Play",
realMobile: true
},
{
os: "android",
os_version: "9.0",
device: "Motorola Moto G7 Play",
realMobile: true
},
{
os: "android",
os_version: "9.0",
device: "Huawei P30",
realMobile: true
},
{
os: "android",
os_version: "12.0",
device: "Samsung Galaxy Tab S8",
realMobile: true
},
{
os: "android",
os_version: "11.0",
device: "Samsung Galaxy Tab S7",
realMobile: true
},
{
os: "android",
os_version: "10.0",
device: "Samsung Galaxy Tab S7",
realMobile: true
},
{
os: "android",
os_version: "9.0",
device: "Samsung Galaxy Tab S6",
realMobile: true
},
{
os: "android",
os_version: "9.0",
device: "Samsung Galaxy Tab S5e",
realMobile: true
},
{
os: "android",
os_version: "8.1",
device: "Samsung Galaxy Tab S4",
realMobile: true
}
];
}
});
// src/server/config.ts
import fs from "fs";
import dotenv2 from "dotenv";
import crypto2 from "crypto";
import { execSync } from "child_process";
var getCommitHash, customDevicesPath, logsFolder, CURRENT_VERSION, major, minor, minSupportedMinor, MIN_SUPPORTED_SDK_VERSION, config;
var init_config = __esm({
"src/server/config.ts"() {
"use strict";
init_package();
init_envConfig();
init_devices();
getCommitHash = () => {
if (gitHead) {
return gitHead.substring(0, 7);
}
try {
return execSync("git rev-parse --short HEAD", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
} catch {
return "";
}
};
customDevicesPath = "./server/data/custom_devices.json";
logsFolder = "./logs";
dotenv2.config();
CURRENT_VERSION = version;
[major, minor] = CURRENT_VERSION.split(".").map(Number);
minSupportedMinor = Math.max(0, minor - 2);
MIN_SUPPORTED_SDK_VERSION = `${major}.${minSupportedMinor}.0`;
config = {
version,
commitHash: getCommitHash(),
minSupportedSdkVersion: MIN_SUPPORTED_SDK_VERSION,
apiVersion: "1",
// this isn't used
getDevices: async () => {
if (fs.existsSync(customDevicesPath)) {
return [...devices_default, ...(await import(customDevicesPath)).default];
}
return devices_default;
},
defaultImagesPath: env.SYNGRISI_IMAGES_PATH,
domSnapshotsPath: env.SYNGRISI_DOM_SNAPSHOTS_PATH || env.SYNGRISI_IMAGES_PATH,
connectionString: env.SYNGRISI_DB_URI || "mongodb://127.0.0.1:27017/SyngrisiDb",
host: env.SYNGRISI_HOSTNAME,
port: env.SYNGRISI_APP_PORT || 3e3,
backupsFolder: "./backups",
enableHttpLogger: env.SYNGRISI_HTTP_LOG,
httpLoggerFilePath: `${logsFolder}/http.log`,
storeSessionKey: env.SYNGRISI_SESSION_STORE_KEY || crypto2.randomBytes(64).toString("hex"),
codeCoverage: env.SYNGRISI_COVERAGE,
disableCors: env.SYNGRISI_DISABLE_DEV_CORS,
fileUploadMaxSize: 50 * 1024 * 1024,
adminDataJobsPath: env.SYNGRISI_ADMIN_DATA_JOBS_PATH,
adminDataJobsTtlMs: env.SYNGRISI_ADMIN_DATA_JOBS_TTL_MS,
adminDataMaxConcurrentJobs: env.SYNGRISI_ADMIN_DATA_MAX_CONCURRENT_JOBS,
adminDataUploadMaxSize: env.SYNGRISI_ADMIN_DATA_UPLOAD_MAX_SIZE_MB * 1024 * 1024,
testMode: env.SYNGRISI_TEST_MODE,
jsonLimit: "50mb",
tmpDir: env.SYNGRISI_TMP_DIR,
helmet: {
crossOriginEmbedderPolicy: false,
crossOriginResourcePolicy: false,
crossOriginOpenerPolicy: false,
contentSecurityPolicy: {
useDefaults: false,
directives: {
defaultSrc: ["'self'", "*", "'unsafe-inline'", "'unsafe-eval'", "data:", "blob:"],
frameAncestors: ["'self'", "*"],
frameSrc: ["'self'", "*"],
scriptSrc: ["'self'", "*", "'unsafe-inline'", "'unsafe-eval'"],
styleSrc: ["'self'", "*", "'unsafe-inline'"],
imgSrc: ["'self'", "*", "data:", "blob:"],
fontSrc: ["'self'", "*", "data:"],
connectSrc: ["'self'", "*"],
baseUri: ["'self'"],
formAction: ["'self'"],
objectSrc: ["'none'"],
scriptSrcAttr: ["'none'"]
}
},
hsts: false
},
rateLimit: {
windowMs: env.SYNGRISI_RATE_LIMIT_WINDOW_MS,
max: env.SYNGRISI_RATE_LIMIT_MAX,
standardHeaders: true,
legacyHeaders: false
},
authRateLimit: {
windowMs: env.SYNGRISI_AUTH_RATE_LIMIT_WINDOW_MS,
max: env.SYNGRISI_AUTH_RATE_LIMIT_MAX,
standardHeaders: true,
legacyHeaders: false
}
};
if (!fs.existsSync(config.defaultImagesPath)) {
fs.mkdirSync(config.defaultImagesPath, { recursive: true });
}
if (config.domSnapshotsPath !== config.defaultImagesPath && !fs.existsSync(config.domSnapshotsPath)) {
fs.mkdirSync(config.domSnapshotsPath, { recursive: true });
}
if (!fs.existsSync(config.adminDataJobsPath)) {
fs.mkdirSync(config.adminDataJobsPath, { recursive: true });
}
if (!fs.existsSync(logsFolder)) {
fs.mkdirSync(logsFolder, { recursive: true });
}
}
});
// src/server/lib/logger.ts
import winston from "winston";
import "winston-mongodb";
function createWinstonLogger(opts) {
if (env.SYNGRISI_DISABLE_LOGS) {
return winston.createLogger({ transports: [], silent: true });
}
const transports = [
new winston.transports.Console({
level: logLevel || "silly",
format: winston.format.combine(
winston.format.colorize(),
winston.format.timestamp(),
winston.format.ms(),
winston.format.metadata(),
winston.format.printf((info) => {
const user = info.metadata.user ? colors.blue(` <${info.metadata.user}>`) : "";
const ref = info.metadata.ref ? colors.gray(` ${info.metadata.ref}`) : "";
const msgType = info.metadata.msgType ? ` ${info.metadata.msgType}` : "";
const itemType = info.metadata.itemType ? colors.magenta(` ${info.metadata.itemType}`) : "";
const scope = info.metadata.scope ? colors.magenta(` [${info.metadata.scope}]`) : "";
const msg = typeof info.message === "object" ? `
${JSON.stringify(info.message, null, 2)}` : info.message;
return `${info.level} ${scope}${formatISOToDateTime_default(info.metadata.timestamp)} ${info.metadata.ms}${user}${ref}${msgType}${itemType} '${msg}'`;
}),
winston.format.padLevels()
)
})
];
if (!env.SYNGRISI_TEST_MODE) {
transports.push(
new winston.transports.MongoDB({
level: logLevel || "debug",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
winston.format.metadata()
),
options: {},
db: opts.dbConnectionString,
collection: "vrslogs"
})
);
}
return winston.createLogger({ transports });
}
var logLevel, Logger, logger_default;
var init_logger = __esm({
"src/server/lib/logger.ts"() {
"use strict";
init_colors();
init_formatISOToDateTime();
init_config();
init_utils();
init_envConfig();
logLevel = env.SYNGRISI_LOG_LEVEL;
Logger = class _Logger {
winstonLogger;
constructor(opts = { dbConnectionString: config.connectionString }) {
this.winstonLogger = createWinstonLogger(opts);
}
static mergeMeta(objects) {
return objects.reduce((acc, obj) => {
return { ...acc, ...obj };
}, {});
}
static sanitizeForLog(obj) {
try {
return JSON.parse(JSON.stringify(obj));
} catch (e) {
return String(obj);
}
}
log(severity, msg, meta) {
const mergedMeta = _Logger.sanitizeForLog(_Logger.mergeMeta(meta));
const formattedMsg = typeof msg === "object" ? JSON.stringify(msg, null, 2) : msg;
this.winstonLogger.log(severity, formattedMsg, mergedMeta);
}
error(msg, ...meta) {
let message = String(msg);
let code = 0;
if (msg instanceof Object) {
try {
message = JSON.stringify(_Logger.sanitizeForLog(msg));
} catch (e) {
message = String(msg);
}
}
if (msg instanceof Error) {
message = msg.stack || msg.message;
}
if (msg instanceof ApiError_default) {
code = msg.statusCode;
}
this.log("error", `${code !== 0 ? "[" + code + "]" : ""}${message}
stacktrace: ${new Error().stack}`, meta);
}
warn(msg, ...meta) {
this.log("warn", `${msg}
stacktrace: ${new Error().stack}`, meta);
}
info(msg, ...meta) {
this.log("info", msg, meta);
}
verbose(msg, ...meta) {
this.log("verbose", msg, meta);
}
debug(msg, ...meta) {
this.log("debug", msg, meta);
}
silly(msg, ...meta) {
this.log("silly", msg, meta);
}
};
logger_default = new Logger();
}
});
// src/server/plugins/core/HookRegistry.ts
import { match } from "path-to-regexp";
var logOpts3, HookRegistry, hookRegistry;
var init_HookRegistry = __esm({
"src/server/plugins/core/HookRegistry.ts"() {
"use strict";
init_logger();
logOpts3 = {
scope: "HookRegistry",
msgType: "PLUGIN"
};
HookRegistry = class {
hooks = /* @__PURE__ */ new Map();
/**
* Register a hook from a plugin
*/
register(pluginName, hookName, handler, priority = 100, routes = ["*"]) {
const existing = this.hooks.get(hookName) || [];
const registeredHook = {
pluginName,
hookName,
handler,
priority,
routes
};
existing.push(registeredHook);
existing.sort((a, b) => a.priority - b.priority);
this.hooks.set(hookName, existing);
logger_default.info(`Registered hook '${hookName}' from plugin '${pluginName}' (priority: ${priority})`, logOpts3);
}
/**
* Unregister all hooks from a plugin
*/
unregister(pluginName) {
for (const [hookName, hooks] of this.hooks.entries()) {
const filtered = hooks.filter((h) => h.pluginName !== pluginName);
if (filtered.length !== hooks.length) {
this.hooks.set(hookName, filtered);
logger_default.info(`Unregistered hooks from plugin '${pluginName}' for '${hookName}'`, logOpts3);
}
}
}
/**
* Get all hooks for a given hook name, optionally filtered by route
*/
getHooks(hookName, route) {
const hooks = this.hooks.get(hookName) || [];
if (!route) {
return hooks;
}
return hooks.filter((hook) => this.matchesRoute(hook.routes, route));
}
/**
* Check if a route matches any of the patterns
*/
matchesRoute(patterns, route) {
for (const pattern of patterns) {
if (pattern === "*") {
return true;
}
const regexPattern = pattern.replace(/\*/g, "(.*)");
try {
const matcher = match(regexPattern, { decode: decodeURIComponent });
if (matcher(route)) {
return true;
}
} catch {
if (route.startsWith(pattern.replace("*", ""))) {
return true;
}
}
}
return false;
}
/**
* Execute auth:validate hooks (first-match mode)
*/
async executeAuthValidate(req, res, context) {
const hooks = this.getHooks("auth:validate", req.path);
for (const hook of hooks) {
try {
logger_default.debug(`Executing auth:validate hook from '${hook.pluginName}'`, logOpts3);
const result = await hook.handler(
req,
res,
context
);
if (result !== null) {
logger_default.debug(`Auth hook '${hook.pluginName}' returned result: ${result.authenticated}`, logOpts3);
return result;
}
} catch (error) {
logger_default.error(`Error in auth:validate hook from '${hook.pluginName}': ${error}`, logOpts3);
}
}
return null;
}
/**
* Execute check:beforeCompare hooks (waterfall mode)
*/
async executeCheckBeforeCompare(context, pluginContext) {
const hooks = this.getHooks("check:beforeCompare");
let currentContext = context;
for (const hook of hooks) {
try {
logger_default.debug(`Executing check:beforeCompare hook from '${hook.pluginName}'`, logOpts3);
const result = await hook.handler(
currentContext,
pluginContext
);
if ("skip" in result && result.skip) {
logger_default.info(`Check comparison skipped by plugin '${hook.pluginName}'`, logOpts3);
return result;
}
currentContext = result;
} catch (error) {
logger_default.error(`Error in check:beforeCompare hook from '${hook.pluginName}': ${error}`, logOpts3);
}
}
return currentContext;
}
/**
* Execute check:afterCompare hooks (waterfall mode)
*/
async executeCheckAfterCompare(context, compareResult, pluginContext) {
const hooks = this.getHooks("check:afterCompare");
let currentResult = compareResult;
for (const hook of hooks) {
try {
logger_default.debug(`Executing check:afterCompare hook from '${hook.pluginName}'`, logOpts3);
currentResult = await hook.handler(
context,
currentResult,
pluginContext
);
} catch (error) {
logger_default.error(`Error in check:afterCompare hook from '${hook.pluginName}': ${error}`, logOpts3);
}
}
return currentResult;
}
/**
* Check if any hooks are registered for a given hook name
*/
hasHooks(hookName) {
const hooks = this.hooks.get(hookName);
return hooks !== void 0 && hooks.length > 0;
}
/**
* Get count of registered hooks
*/
getHookCount(hookName) {
if (hookName) {
return (this.hooks.get(hookName) || []).length;
}
let total = 0;
for (const hooks of this.hooks.values()) {
total += hooks.length;
}
return total;
}
/**
* Clear all hooks
*/
clear() {
this.hooks.clear();
logger_default.info("Cleared all hooks", logOpts3);
}
};
hookRegistry = new HookRegistry();
}
});
// src/server/plugins/sdk/context.ts
function buildPluginContext(pluginConfig = {}) {
const appConfig = {
connectionString: config.connectionString,
defaultImagesPath: config.defaultImagesPath
};
return {
config: appConfig,
logger: logger_default,
models: {
User: User_model_default,
Check: Check_model_default,
Baseline: Baseline_model_default
},
pluginConfig
};
}
var init_context = __esm({
"src/server/plugins/sdk/context.ts"() {
"use strict";
init_models();
init_config();
init_logger();
}
});
// src/server/plugins/core/PluginManager.ts
var logOpts4, PluginManager, pluginManager;
var init_PluginManager = __esm({
"src/server/plugins/core/PluginManager.ts"() {
"use strict";
init_logger();
init_context();
init_HookRegistry();
logOpts4 = {
scope: "PluginManager",
msgType: "PLUGIN"
};
PluginManager = class {
plugins = /* @__PURE__ */ new Map();
context = null;
registry;
pluginConfigs = /* @__PURE__ */ new Map();
constructor(registry2 = hookRegistry) {
this.registry = registry2;
}
/**
* Initialize the plugin manager with application context
*/
async initialize(pluginConfigs = {}) {
logger_default.info("Initializing Plugin Manager", logOpts4);
for (const [name, config2] of Object.entries(pluginConfigs)) {
this.pluginConfigs.set(name, config2);
}
this.context = null;
}
/**
* Register and load a plugin
*/
async loadPlugin(pluginExport, config2) {
let plugin;
if (typeof pluginExport === "function") {
const factory = pluginExport;
plugin = factory(config2 || {});
} else {
plugin = pluginExport;
}
const { manifest } = plugin;
const pluginName = manifest.name;
if (this.plugins.has(pluginName)) {
logger_default.warn(`Plugin '${pluginName}' is already loaded, skipping`, logOpts4);
return;
}
if (manifest.enabled === false) {
logger_default.info(`Plugin '${pluginName}' is disabled, skipping`, logOpts4);
return;
}
logger_default.info(`Loading plugin: ${pluginName} v${manifest.version}`, logOpts4);
const loadedPlugin = {
plugin,
loaded: false
};
try {
const pluginConfig = config2 || this.pluginConfigs.get(pluginName) || {};
const context = buildPluginContext(pluginConfig);
if (plugin.onLoad) {
await plugin.onLoad(context);
}
this.registerPluginHooks(plugin);
loadedPlugin.loaded = true;
this.plugins.set(pluginName, loadedPlugin);
logger_default.info(`Plugin '${pluginName}' loaded successfully`, logOpts4);
} catch (error) {
loadedPlugin.error = error instanceof Error ? error : new Error(String(error));
this.plugins.set(pluginName, loadedPlugin);
logger_default.error(`Failed to load plugin '${pluginName}': ${error}`, logOpts4);
throw error;
}
}
/**
* Register all hooks from a plugin
*/
registerPluginHooks(plugin) {
const { manifest, hooks } = plugin;
if (!hooks) {
return;
}
const priority = manifest.priority ?? 100;
const routes = manifest.routes ?? ["*"];
for (const [hookName, handler] of Object.entries(hooks)) {
if (handler) {
this.registry.register(
manifest.name,
hookName,
handler,
priority,
routes
);
}
}
}
/**
* Unload a plugin
*/
async unloadPlugin(pluginName) {
const loadedPlugin = this.plugins.get(pluginName);
if (!loadedPlugin) {
logger_default.warn(`Plugin '${pluginName}' is not loaded`, logOpts4);
return;
}
logger_default.info(`Unloading plugin: ${pluginName}`, logOpts4);
try {
if (loadedPlugin.plugin.onUnload) {
await loadedPlugin.plugin.onUnload();
}
this.registry.unregister(pluginName);
this.plugins.delete(pluginName);
logger_default.info(`Plugin '${pluginName}' unloaded successfully`, logOpts4);
} catch (error) {
logger_default.error(`Error unloading plugin '${pluginName}': ${error}`, logOpts4);
throw error;
}
}
/**
* Unload all plugins
*/
async unloadAll() {
logger_default.info("Unloading all plugins", logOpts4);
const pluginNames = Array.from(this.plugins.keys());
for (const pluginName of pluginNames) {
try {
await this.unloadPlugin(pluginName);
} catch (error) {
logger_default.error(`Error unloading plugin '${pluginName}': ${error}`, logOpts4);
}
}
this.registry.clear();
}
/**
* Get a loaded plugin by name
*/
getPlugin(pluginName) {
return this.plugins.get(pluginName);
}
/**
* Get all loaded plugins
*/
getLoadedPlugins() {
return new Map(this.plugins);
}
/**
* Get plugin names
*/
getPluginNames() {
return Array.from(this.plugins.keys());
}
/**
* Check if a plugin is loaded
*/
isPluginLoaded(pluginName) {
const plugin = this.plugins.get(pluginName);
return plugin?.loaded ?? false;
}
/**
* Get plugin count
*/
getPluginCount() {
return this.plugins.size;
}
/**
* Get the hook registry
*/
getHookRegistry() {
return this.registry;
}
};
pluginManager = new PluginManager();
}
});
// src/server/plugins/core/PluginLoader.ts
var init_PluginLoader = __esm({
"src/server/plugins/core/PluginLoader.ts"() {
"use strict";
init_logger();
init_PluginManager();
init_models();
}
});
// src/server/routes/v1/runs.route.ts
import express from "express";
import { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi";
// src/server/controllers/runs.controller.ts
init_utils();
// src/server/services/run.service.ts
var run_service_exports = {};
__export(run_service_exports, {
remove: () => remove2
});
init_models();
init_logger();
// src/server/services/test.service.ts
import { Types as Types2 } from "mongoose";
init_models();
init_logger();
var remove = async (id2, user) => {
const logOpts6 = {
scope: "removeTest",
itemType: "test",
ref: id2,
user: user?.username,
msgType: "REMOVE"
};
logger_default.info(`remove test with, id: '${id2}', user: '${user.username}'`, logOpts6);
try {
logger_default.debug(`try to delete all checks associated to test with ID: '${id2}'`, logOpts6);
const checks = await Check_model_default.find({ test: id2 });
for (const check of checks) {
await check_service_exports.remove(check._id, user);
}
return Test_model_default.findByIdAndDelete(id2);
} catch (e) {
logger_default.error(`cannot remove test with id: ${id2} error: ${e instanceof Error ? e.stack : String(e)}`, logOpts6);
throw new Error();
}
};
// src/server/services/run.service.ts
init_utils();
init_utils();
var remove2 = async (id2, user) => {
const logOpts6 = {
scope: "removeRun",
itemType: "run",
ref: id2,
user: user?.username,
msgType: "REMOVE"
};
logger_default.info(`remove run with, id: '${id2}', user: '${user.username}'`, logOpts6);
const tests = await Test_model_default.find({ run: id2 }).exec();
for (const test of tests) {
await remove(test._id, user);
}
const run = await Run_model_default.findByIdAndDelete(id2).exec();
if (!run) {
throw new ApiError_default(httpStatus_default.NOT_FOUND, `cannot remove run with id: '${id2}', not found`);
}
return run;
};
// src/server/services/suite.service.ts
init_models();
init_logger();
init_utils();
init_utils();
// src/server/services/logs.service.ts
init_models();
init_logger();
init_envConfig();
// src/server/services/generic.service.ts
var generic_service_exports = {};
__export(generic_service_exports, {
get: () => get,
put: () => put
});
init_utils();
init_logger();
import mongoose16 from "mongoose";
var get = async (modelName, filter, options) => {
const itemModel = mongoose16.model(modelName);
return itemModel.paginate(filter, options);
};
var put = async (modelName, id2, options, user) => {
const itemModel = mongoose16.model(modelName);
const logOpts6 = {
scope: "generic.service.put",
ref: id2,
itemType: modelName,
msgType: "UPDATE",
user: user?.username
};
const opts = removeEmptyProperties(options);
logger_default.debug(`start update '${modelName}' with id: '${id2}', body: '${JSON.stringify(opts)}'`, logOpts6);
const item = await itemModel.findByIdAndUpdate(id2, options).exec();
if (!item) throw new Error(`cannot find the item: ${modelName}, id: ${id2}, options: ${JSON.stringify(options)}`);
await item.save();
logger_default.debug(`baseline with id: '${id2}' and opts: '${JSON.stringify(opts)}' was updated`, logOpts6);
return item;
};
// src/server/services/app.service.ts
init_models();
// src/server/services/tasks.service.ts
init_envConfig();
init_config();
init_logger();
// src/tasks/lib/output-writer.ts
init_logger();
// src/tasks/core/handle-database-consistency.task.ts
init_config();
init_stringTable();
import mongoose17 from "mongoose";
// src/tasks/lib/index.ts
init_Check_model();
init_Log_model();
init_App_model();
init_Snapshot_model();
init_Suite_model();
init_Run_model();
init_User_model();
init_Baseline_model();
init_Test_model();
// src/tasks/core/handle-old-checks.task.ts
init_config();
init_utils();
init_stringTable();
import mongoose18 from "mongoose";
// src/tasks/core/handle-orphan-files.task.ts
init_config();
init_stringTable();
// src/tasks/core/handle-orphan-baselines.task.ts
init_models();
// src/tasks/core/remove-old-logs.task.ts
init_utils();
// src/server/services/tasks.service.ts
init_models();
// src/server/services/client.service.ts
init_utils();
init_logger();
// src/server/services/dom-snapshot.service.ts
init_models();
init_config();
import fs2 from "fs/promises";
import path2 from "path";
import { Types as Types3 } from "mongoose";
// src/server/utils/domDumpUtils.ts
import { gunzipSync, gzipSync } from "zlib";
import { createHash as createHash2 } from "crypto";
function isCompressedDomDump(data) {
return data !== null && typeof data === "object" && "compressed" in data && data.compressed === true && "data" in data && typeof data.data === "string";
}
function decompressDomDump(data) {
if (!data) return null;
try {
if (typeof data === "object" && !isCompressedDomDump(data)) {
return data;
}
if (isCompressedDomDump(data)) {
const buffer = Buffer.from(data.data, "base64");
const decompressed = gunzipSync(buffer).toString("utf8");
return JSON.parse(decompressed);
}
if (typeof data === "string") {
const parsed = JSON.parse(data);
if (isCompressedDomDump(parsed)) {
return decompressDomDump(parsed);
}
return parsed;
}
return null;
} catch (e) {
console.error("Failed to decompress domDump:", e);
return null;
}
}
function calculateHash(data) {
const input = typeof data === "string" ? Buffer.from(data, "utf8") : data;
return createHash2("sha256").update(input).digest("hex");
}
function prepareDomDumpForStorage(data, compressionHeader) {
if (!data) {
return { content: null, wasCompressed: false, originalSize: 0 };
}
let wasCompressed = false;
let content = null;
let originalSize = 0;
if (compressionHeader === "gzip") {
wasCompressed = true;
}
if (typeof data === "string") {
originalSize = Buffer.byteLength(data, "utf8");
try {
const parsed = JSON.parse(data);
if (isCompressedDomDump(parsed)) {
wasCompressed = true;
originalSize = parsed.originalSize;
content = decompressDomDump(parsed);
} else {
content = parsed;
}
} catch {
return { content: null, wasCompressed: false, originalSize: 0 };
}
} else if (isCompressedDomDump(data)) {
wasCompressed = true;
originalSize = data.originalSize;
content = decompressDomDump(data);
} else {
content = data;
originalSize = Buffer.byteLength(JSON.stringify(data), "utf8");
}
return { content, wasCompressed, originalSize };
}
function serializeForStorage(data) {
const jsonString = JSON.stringify(data);
const originalSize = Buffer.byteLength(jsonString, "utf8");
const compressed = gzipSync(Buffer.from(jsonString, "utf8"));
return {
buffer: compressed,
compressed: true,
originalSize,
compressedSize: compressed.length
};
}
function deserializeFromStorage(buffer, isCompressed) {
try {
if (isCompressed) {
const decompressed = gunzipSync(buffer).toString("utf8");
return JSON.parse(decompressed);
}
return JSON.parse(buffer.toString("utf8"));
} catch (e) {
console.error("Failed to deserialize domDump from storage:", e);
return null;
}
}
// src/server/services/dom-snapshot.service.ts
init_logger();
async function createDomSnapshot(params) {
const { checkId, baselineId, type, content, compressionHeader } = params;
const { content: parsedContent, wasCompressed, originalSize } = prepareDomDumpForStorage(
content,
compressionHeader
);
if (!parsedContent) {
logger_default.debug("No DOM content to store");
return null;
}
const contentString = JSON.stringify(parsedContent);
const hash = calculateHash(contentString);
const existingSnapshot = await DomSnapshot_model_default.findOne({ hash });
if (existingSnapshot) {
logger_default.debug(`Found existing DOM snapshot with hash ${hash}, creating reference`);
const snapshot2 = await DomSnapshot_model_default.create({
checkId: new Types3.ObjectId(checkId),
baselineId: baselineId ? new Types3.ObjectId(baselineId) : void 0,
type,
filename: existingSnapshot.filename,
hash,
compressed: existingSnapshot.compressed,
originalSize,
compressedSize: existingSnapshot.compressedSize
});
return snapshot2;
}
const { buffer, compressed, compressedSize } = serializeForStorage(parsedContent);
const filename = `${checkId}_${type}_${Date.now()}.dom.gz`;
const filePath = path2.join(config.domSnapshotsPath, filename);
await fs2.writeFile(filePath, buffer);
logger_default.debug(`Saved DOM snapshot to ${filePath} (${compressedSize} bytes compressed, ${originalSize} bytes original)`);
const snapshot = await DomSnapshot_model_default.create({
checkId: new Types3.ObjectId(checkId),
baselineId: baselineId ? new Types3.ObjectId(baselineId) : void 0,
type,
filename,
hash,
compressed,
originalSize,
compressedSize
});
return snapshot;
}
async function getDomContent(snapshotId) {
const snapshot = await DomSnapshot_model_default.findById(snapshotId);
if (!snapshot) {
logger_default.warn(`DOM snapshot not found: ${snapshotId}`);
return null;
}
return getDomContentBySnapshot(snapshot);
}
async function getDomContentBySnapshot(snapshot) {
const filePath = path2.join(config.domSnapshotsPath, snapshot.filename);
try {
const buffer = await fs2.readFile(filePath);
return deserializeFromStorage(buffer, snapshot.compressed);
} catch (e) {
logger_default.error(`Failed to read DOM snapshot file: ${filePath}`, e);
return null;
}
}
async function getDomSnapshotByCheckId(checkId, type = "actual") {
return DomSnapshot_model_default.findOne({
checkId: new Types3.ObjectId(checkId),
type
});
}
async function getDomSnapshotByBaselineId(baselineId) {
return DomSnapshot_model_default.findOne({
baselineId: new Types3.ObjectId(baselineId),
type: "baseline"
});
}
async function linkDomSnapshotToBaseline(checkId, baselineId) {
const actualSnapshot = await getDomSnapshotByCheckId(checkId, "actual");
if (!actualSnapshot) {
logger_default.debug(`No DOM snapshot found for check ${checkId}`);
return null;
}
const baselineSnapshot = await DomSnapshot_model_default.create({
checkId: new Types3.ObjectId(checkId),
baselineId: new Types3.ObjectId(baselineId),
type: "baseline",
filename: actualSnapshot.filename,
hash: actualSnapshot.hash,
compressed: actualSnapshot.compressed,
originalSize: actualSnapshot.originalSize,
compressedSize: actualSnapshot.compressedSize
});
logger_default.debug(`Linked DOM snapshot to baseline ${baselineId}`);
return baselineSnapshot;
}
async function removeDomSnapshotsByCheckId(checkId) {
const snapshots = await DomSnapshot_model_default.find({ checkId: new Types3.ObjectId(checkId) });
for (const snapshot of snapshots) {
const otherSnapshots = await DomSnapshot_model_default.countDocuments({
filename: snapshot.filename,
_id: { $ne: snapshot._id }
});
if (otherSnapshots === 0) {
const filePath = path2.join(config.domSnapshotsPath, snapshot.filename);
try {
await fs2.unlink(filePath);
logger_default.debug(`Deleted DOM snapshot file: ${filePath}`);
} catch (e) {
logger_default.warn(`Failed to delete DOM snapshot file: ${filePath}`, e);
}
}
await snapshot.deleteOne();
}
}
async function removeDomSnapshotsByBaselineId(baselineId) {
const snapshots = await DomSnapshot_model_default.find({ baselineId: new Types3.ObjectId(baselineId) });
for (const snapshot of snapshots) {
const otherSnapshots = await DomSnapshot_model_default.countDocuments({
filename: snapshot.filename,
_id: { $ne: snapshot._id }
});
if (otherSnapshots === 0) {
const filePath = path2.join(config.domSnapshotsPath, snapshot.filename);
try {
await fs2.unlink(filePath);
logger_default.debug(`Deleted DOM snapshot file: ${filePath}`);
} catch (e) {
logger_default.warn(`Failed to delete DOM snapshot file: ${filePath}`, e);
}
}
await snapshot.deleteOne();
}
}
var domSnapshotService = {
createDomSnapshot,
getDomContent,
getDomContentBySnapshot,
getDomSnapshotByCheckId,
getDomSnapshotByBaselineId,
linkDomSnapshotToBaseline,
removeDomSnapshotsByCheckId,
removeDomSnapshotsByBaselineId
};
// src/server/services/client.service.ts
init_utils();
// src/server/services/snapshot-file.service.ts
init_utils();
init_models();
init_utils();
init_config();
init_logger();
import fs3, { promises as fsp } from "fs";
import path3 from "path";
async function createSnapshot(parameters, session) {
const logOpts6 = {
scope: "createSnapshot",
itemType: "snapshot",
msgType: "CREATE"
};
const { name, fileData, hashCode } = parameters;
const opts = { name };
if (fileData === null) {
throw new ApiError_default(httpStatus_default.BAD_REQUEST, `cannot create the snapshot, the 'fileData' is not set, name: '${name}'`);
}
opts.imghash = hashCode || hashSync(fileData);
const snapshot = new Snapshot_model_default(opts);
const filename = `${snapshot.id}.png`;
const imagePath = path3.join(config.defaultImagesPath, filename);
logger_default.debug(`save screenshot for: '${name}' snapshot to: '${imagePath}'`, logOpts6);
await fsp.writeFile(imagePath, fileData);
snapshot.filename = filename;
await snapshot.save({ session });
logger_default.debug(`snapshot was saved: '${JSON.stringify(snapshot)}'`, { ...logOpts6, ...{ ref: String(snapshot._id) } });
return snapshot;
}
// src/server/services/test-run.service.ts
init_utils();
init_models();
// src/server/lib/dbItems/updateItem.ts
init_logger();
import mongoose19 from "mongoose";
// src/server/lib/dbItems/updateItemDate.ts
init_logger();
import mongoose20 from "mongoose";
var logOpts = {
scope: "dbitems",
msgType: "DB"
};
async function updateItemDate(mdClass, id2, session) {
logger_default.debug(`update date for the item: '${mdClass}' with id: '${id2}'`, logOpts);
const itemModel = await mongoose20.model(mdClass).findById(id2).session(session || null);
const updatedItem = await itemModel?.updateOne({ updatedDate: Date.now() }, { session });
logger_default.debug(`'${mdClass}' date updated: '${JSON.stringify(itemModel)}'`, logOpts);
return updatedItem;
}
// src/server/lib/dbItems/createItemIfNotExist.ts
init_logger();
import mongoose21 from "mongoose";
// src/server/lib/dbItems/createItemProm.ts
init_logger();
import mongoose22 from "mongoose";
// src/server/lib/dbItems/createAppIfNotExist.ts
init_models();
init_logger();
// src/server/lib/dbItems/createSuiteIfNotExist.ts
init_models();
init_logger();
// src/server/lib/dbItems/createRunIfNotExist.ts
init_models();
init_utils();
init_logger();
// src/server/services/test-run.service.ts
init_logger();
// src/server/services/baseline.service.ts
init_models();
init_utils();
init_logger();
init_utils();
import { Types as Types4 } from "mongoose";
// src/server/services/check.service.ts
var check_service_exports = {};
__export(check_service_exports, {
accept: () => accept,
createCheckDocument: () => createCheckDocument,
enrichChecksWithCurrentAcceptance: () => enrichChecksWithCurrentAcceptance,
recompare: () => recompare,
remove: () => remove4,
update: () => update
});
init_models();
init_utils();
import { Types as Types5 } from "mongoose";
// src/server/services/snapshot.service.ts
init_config();
init_models();
init_logger();
import fs4 from "fs";
import path4 from "path";
var logOpts2 = {
scope: "snapshot_helper",
msgType: "API"
};
var removeSnapshotFile = async (snapshot) => {
let relatedSnapshots;
if (snapshot.filename) {
relatedSnapshots = await Snapshot_model_default.find({ filename: snapshot.filename });
logger_default.debug(`there are '${relatedSnapshots.length}' snapshots with filename: '${snapshot.filename}'`, logOpts2);
}
const isLastSnapshotFile = () => {
if (!snapshot.filename) {
return true;
}
return relatedSnapshots.length === 0;
};
logger_default.debug({ isLastSnapshotFile: isLastSnapshotFile() });
if (isLastSnapshotFile()) {
const imagePath = path4.join(config.defaultImagesPath, snapshot.filename);
logger_default.silly(`path: ${imagePath}`, logOpts2);
if (fs4.existsSync(imagePath)) {
logger_default.debug(`removing file: '${imagePath}'`, logOpts2, {
msgType: "REMOVE",
itemType: "file"
});
fs4.unlinkSync(imagePath);
}
}
};
var remove3 = async (id2) => {
const logOpts6 = {
scope: "removeSnapshot",
msgType: "REMOVE",
itemType: "snapshot",
ref: id2
};
if (!id2) {
return;
}
const baseline = await Baseline_model_default.findOne({ snapshootId: id2 });
if (baseline) {
logger_default.debug(`snapshot: '${id2}' is related to a baseline, skipping deletion`, logOpts6);
return;
}
const snapshot = await Snapshot_model_default.findById(id2);
if (snapshot) {
await removeSnapshotFile(snapshot);
await Snapshot_model_default.findByIdAndDelete(id2);
} else {
}
};
// src/server/services/check.service.ts
init_logger();
// src/server/services/webhook.service.ts
init_models();
init_logger();
var triggerWebhooks = async (event, payload) => {
const webhooks = await Webhook_model_default.find({ events: event });
for (const webhook of webhooks) {
try {
logger_default.info(`Triggering webhook ${webhook.url} for event ${event}`);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5e3);
const response = await fetch(webhook.url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Syngrisi-Event": event,
"X-Syngrisi-Secret": webhook.secret || ""
},
body: JSON.stringify({
event,
payload,
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}),
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error) {
logger_default.error(`Failed to trigger webhook ${webhook.url}: ${error}`);
}
}
};
var webhookService = {
triggerWebhooks
};
// src/server/services/comparison.service.ts
init_models();
init_config();
import path6 from "path";
import { promises as fsp2 } from "fs";
// src/server/lib/comparison/comparator.ts
init_utils();
init_logger();
import { Worker } from "worker_threads";
import fs5 from "fs";
import path5 from "path";
import { fileURLToPath } from "url";
import { createRequire } from "module";
var DEFAULT_OPTIONS = {
output: {
largeImageThreshold: 0,
outputDiff: true,
errorType: "flat",
errorColor: { red: 255, green: 0, blue: 255 },
transparency: 0
},
ignore: "nothing"
};
var __filename = fileURLToPath(import.meta.url);
var __dirname = path5.dirname(__filename);
var resolveWorkerScript = () => {
const candidates = [
// 1. Local development (relative to original source file)
path5.join(__dirname, "imageDiffWorker.js"),
// 2. Bundled mode (after tsup bundles into server.js, __dirname points to dist/server/)
path5.join(__dirname, "lib", "comparison", "imageDiffWorker.js"),
// 3. Legacy fallback (cwd-based)
path5.join(process.cwd(), "dist", "server", "lib", "comparison", "imageDiffWorker.js")
];
try {
const require2 = createRequire(import.meta.url);
const pkgPath = require2.resolve("@syngrisi/syngrisi/package.json");
const pkgDir = path5.dirname(pkgPath);
candidates.push(path5.join(pkgDir, "dist", "server", "lib", "comparison", "imageDiffWorker.js"));
} catch {
}
for (const candidate of candidates) {
if (fs5.existsSync(candidate)) return candidate;
}
return null;
};
var normalizeOptions = (options = {}) => {
const mergedOutput = { ...DEFAULT_OPTIONS.output, ...options.output || {} };
return {
...DEFAULT_OPTIONS,
...options,
output: mergedOutput,
ignoreRectangles: options.ignoredBoxes
};
};
var runDiffInWorker = (baselineOrigin, actualOrigin, options) => new Promise((resolve, reject) => {
const script = resolveWorkerScript();
if (!script) throw new Error("Image diff worker script is missing");
const worker = new Worker(script, {
workerData: {
baselineOrigin,
actualOrigin,
options: normalizeOptions(options)
}
});
worker.on("message", (message) => {
if (!message.ok) {
reject(new Error(message.error || "Image diff worker failed"));
return;
}
const diff = message.result || {};
if (message.diffBuffer) {
const bufferCopy = Buffer.from(message.diffBuffer);
diff.getBuffer = () => bufferCopy;
}
resolve(diff);
});
worker.on("error", (err) => {
reject(err);
});
worker.on("exit", (code) => {
if (code !== 0) {
reject(new Error(`Image diff worker stopped with exit code ${code}`));
}
});
});
async function getDiff(baselineOrigin, actualOrigin, opts = {}) {
const logOpts6 = {
scope: "getDiff",
itemType: "image",
msgType: "GET_DIFF"
};
try {
const executionTimer = process.hrtime();
logger_default.debug(`SAMPLE #1: ${process.hrtime(executionTimer).toString()}`, logOpts6);
const directDiff = await runDiffInWorker(baselineOrigin, actualOrigin, opts);
logger_default.debug(`SAMPLE #2: ${process.hrtime(executionTimer).toString()}`, logOpts6);
directDiff.executionTotalTime = process.hrtime(executionTimer).toString();
logger_default.debug(`SAMPLE #3: ${process.hrtime(executionTimer).toString()}`, logOpts6);
logger_default.debug(`the diff is: ${JSON.stringify(directDiff, null, 4)}`, logOpts6);
return directDiff;
} catch (e) {
logger_default.error(errMsg(e), logOpts6);
throw new Error(errMsg(e));
}
}
// src/server/services/comparison.service.ts
init_logger();
init_utils();
init_utils();
// src/server/plugins/index.ts
init_envConfig();
init_logger();
// src/server/plugins/core/index.ts
init_HookRegistry();
init_PluginManager();
init_PluginLoader();
// src/server/plugins/index.ts
init_models();
init_context();
// src/server/plugins/sdk/hooks.ts
var HOOK_EXECUTION_MODE = {
/** First successful result wins (auth hooks) */
FIRST_MATCH: "first-match",
/** Result passes through all handlers (waterfall) */
WATERFALL: "waterfall",
/** All handlers run, results collected */
PARALLEL: "parallel"
};
var HOOK_MODES = {
"auth:validate": HOOK_EXECUTION_MODE.FIRST_MATCH,
"check:beforeCompare": HOOK_EXECUTION_MODE.WATERFALL,
"check:afterCompare": HOOK_EXECUTION_MODE.WATERFALL,
"request:before": HOOK_EXECUTION_MODE.WATERFALL,
"request:after": HOOK_EXECUTION_MODE.WATERFALL
};
// src/server/plugins/sdk/index.ts
init_context();
// src/server/plugins/index.ts
async function executeBeforeCompareHook(checkContext) {
if (!hookRegistry.hasHooks("check:beforeCompare")) {
return checkContext;
}
const pluginContext = buildPluginContext();
return hookRegistry.executeCheckBeforeCompare(checkContext, pluginContext);
}
async function executeAfterCompareHook(checkContext, compareResult) {
if (!hookRegistry.hasHooks("check:afterCompare")) {
return compareResult;
}
const pluginContext = buildPluginContext();
return hookRegistry.executeCheckAfterCompare(checkContext, compareResult, pluginContext);
}
// src/server/services/comparison.service.ts
var isWithinToleranceThreshold = (rawMismatch, threshold) => {
if (!Number.isFinite(rawMismatch) || !Number.isFinite(threshold)) return false;
const normalizedThreshold = Math.max(0, Math.min(100, threshold));
return rawMismatch > 0 && rawMismatch <= normalizedThreshold;
};
var compareSnapshots = async (baselineSnapshot, actual, opts = {}) => {
const logOpts6 = {
scope: "compareSnapshots",
ref: baselineSnapshot.id,
itemType: "snapshot",
msgType: "COMPARE"
};
try {
logger_default.debug(`compare baseline and actual snapshots with ids: [${baselineSnapshot.id}, ${actual.id}]`, logOpts6);
logger_default.debug(`current baseline snapshot: ${JSON.stringify(baselineSnapshot)}`, logOpts6);
let diff;
if (baselineSnapshot.imghash === actual.imghash) {
logger_default.debug(`baseline and actual snapshot have the identical image hashes: '${baselineSnapshot.imghash}'`, logOpts6);
diff = {
isSameDimensions: true,
dimensionDifference: { width: 0, height: 0 },
rawMisMatchPercentage: 0,
misMatchPercentage: "0.00",
analysisTime: 0,
executionTotalTime: "0",
getBuffer: null
};
} else {
if (!baselineSnapshot.filename || !actual.filename) {
throw new Error("Snapshot filename is missing");
}
const baselinePath = path6.join(config.defaultImagesPath, baselineSnapshot.filename);
const actualPath = path6.join(config.defaultImagesPath, actual.filename);
const baselineData = await fsp2.readFile(baselinePath);
const actualData = await fsp2.readFile(actualPath);
logger_default.debug(`baseline path: ${baselinePath}`, logOpts6);
logger_default.debug(`actual path: ${actualPath}`, logOpts6);
diff = await getDiff(baselineData, actualData, opts);
}
logger_default.silly(`the diff is: '${JSON.stringify(diff, null, 2)}'`);
if (diff.rawMisMatchPercentage.toString() !== "0") {
logger_default.debug(`images are different, ids: [${baselineSnapshot.id}, ${actual.id}], rawMisMatchPercentage: '${diff.rawMisMatchPercentage}'`);
}
if (diff.stabMethod && diff.vOffset) {
if (diff.stabMethod === "downup") {
actual.vOffset = -diff.vOffset;
await actual.save();
}
if (diff.stabMethod === "updown") {
baselineSnapshot.vOffset = -diff.vOffset;
await baselineSnapshot.save();
}
}
return diff;
} catch (e) {
const errMsg2 = `cannot compare snapshots: ${e}
${e instanceof Error ? e.stack : e}`;
logger_default.error(errMsg2, logOpts6);
throw new Error(String(e));
}
};
var ignoreDifferentResolutions = ({ height, width }) => {
if (width === 0 && height === -1) return true;
if (width === 0 && height === 1) return true;
return false;
};
var compareCheck = async (expectedSnapshot, actualSnapshot, newCheckParams, skipSaveOnCompareError, currentUser, session) => {
const logOpts6 = {
scope: "createCheck.compare",
user: currentUser.username,
itemType: "check",
msgType: "COMPARE"
};
const executionTimer = process.hrtime();
const compareResult = {};
compareResult.failReasons = [...newCheckParams.failReasons];
let checkCompareResult;
let diffSnapshot = null;
const areSnapshotsDifferent = (result) => result.rawMisMatchPercentage.toString() !== "0";
const areSnapshotsWrongDimensions = (result) => !result.isSameDimensions && !ignoreDifferentResolutions(result.dimensionDifference);
if (newCheckParams.status !== "new" && !compareResult.failReasons.includes("not_accepted")) {
try {
logger_default.debug(`'the check with name: '${newCheckParams.name}' isn't new, make comparing'`, logOpts6);
const baseline = await Baseline_model_default.findOne({ snapshootId: expectedSnapshot._id }).exec();
const compareOptions = { vShifting: newCheckParams.vShifting };
if (baseline) {
if (baseline.ignoreRegions) {
logger_default.debug(`ignore regions: '${baseline.ignoreRegions}', type: '${typeof baseline.ignoreRegions}'`);
compareOptions.ignoredBoxes = JSON.parse(baseline.ignoreRegions);
}
compareOptions.ignore = baseline.matchType || "nothing";
}
const apiThreshold = typeof newCheckParams.toleranceThreshold === "number" ? newCheckParams.toleranceThreshold : null;
const baselineThreshold = Number(baseline?.toleranceThreshold || 0);
const toleranceThreshold = Math.max(0, Math.min(100, apiThreshold ?? baselineThreshold));
const toleranceSource = apiThreshold !== null ? "api" : "baseline";
const checkContext = {
expectedSnapshot,
actualSnapshot,
checkParams: newCheckParams,
baseline: baseline || void 0,
compareOptions
};
const beforeHookResult = await executeBeforeCompareHook(checkContext);
if ("skip" in beforeHookResult && beforeHookResult.skip) {
logger_default.info(`Comparison skipped by plugin, using override result`, logOpts6);
const overrideResult = beforeHookResult.result;
return {
failReasons: overrideResult.failReasons || [],
status: overrideResult.status,
result: overrideResult.result || JSON.stringify({ pluginOverride: true })
};
}
checkCompareResult = await compareSnapshots(expectedSnapshot, actualSnapshot, compareOptions);
logger_default.silly(`ignoreDifferentResolutions: '${ignoreDifferentResolutions(checkCompareResult.dimensionDifference)}'`);
logger_default.silly(`dimensionDifference: '${JSON.stringify(checkCompareResult.dimensionDifference)}`);
if (areSnapshotsDifferent(checkCompareResult) && ignoreDifferentResolutions(checkCompareResult.dimensionDifference)) {
const baselineDims = checkCompareResult.baselineDimensions;
const actualDims = checkCompareResult.actualDimensions;
if (baselineDims && actualDims) {
const heightDiff = actualDims.height - baselineDims.height;
let ignoredBox;
if (heightDiff === 1) {
ignoredBox = { left: 0, top: actualDims.height - 1, right: actualDims.width, bottom: actualDims.height };
} else if (heightDiff === -1) {
ignoredBox = { left: 0, top: baselineDims.height - 1, right: baselineDims.width, bottom: baselineDims.height };
}
if (ignoredBox) {
logger_default.debug(`Retrying comparison with ignored box for 1px diff: ${JSON.stringify(ignoredBox)}`, logOpts6);
const retryOptions = { ...compareOptions };
retryOptions.ignoredBoxes = retryOptions.ignoredBoxes ? [...retryOptions.ignoredBoxes, ignoredBox] : [ignoredBox];
const retryResult = await compareSnapshots(expectedSnapshot, actualSnapshot, retryOptions);
if (!areSnapshotsDifferent(retryResult)) {
logger_default.debug(`Retry passed with ignored box`, logOpts6);
checkCompareResult = retryResult;
}
}
}
}
const rawMismatch = Number(checkCompareResult.rawMisMatchPercentage || 0);
const mismatchWithinTolerance = isWithinToleranceThreshold(rawMismatch, toleranceThreshold);
if (mismatchWithinTolerance) {
logger_default.debug(`mismatch '${rawMismatch}' is within tolerance '${toleranceThreshold}', mark check as passed`, logOpts6);
}
if (areSnapshotsDifferent(checkCompareResult) && !mismatchWithinTolerance || areSnapshotsWrongDimensions(checkCompareResult)) {
let logMsg;
if (areSnapshotsWrongDimensions(checkCompareResult)) {
logMsg = "snapshots have different dimensions";
compareResult.failReasons.push("wrong_dimensions");
}
if (areSnapshotsDifferent(checkCompareResult) && !mismatchWithinTolerance) {
logMsg = "snapshots have differences";
compareResult.failReasons.push("different_images");
}
if (logMsg) logger_default.debug(logMsg, logOpts6);
logger_default.debug(`saving diff snapshot for check with name: '${newCheckParams.name}'`, logOpts6);
if (!skipSaveOnCompareError) {
diffSnapshot = await createSnapshot({
name: newCheckParams.name,
fileData: checkCompareResult.getBuffer()
}, session);
compareResult.diffId = diffSnapshot.id;
compareResult.diffSnapshot = diffSnapshot;
}
compareResult.status = "failed";
} else {
compareResult.status = "passed";
}
checkCompareResult.appliedToleranceThreshold = toleranceThreshold;
checkCompareResult.passedByTolerance = mismatchWithinTolerance;
checkCompareResult.toleranceSource = toleranceSource;
checkCompareResult.totalCheckHandleTime = process.hrtime(executionTimer).toString();
compareResult.result = JSON.stringify(checkCompareResult, null, " ");
} catch (e) {
compareResult.status = "failed";
compareResult.result = JSON.stringify({ server_error: `error during comparing - ${errMsg(e)}` });
compareResult.failReasons.push("internal_server_error");
throw new ApiError_default(httpStatus_default.INTERNAL_SERVER_ERROR, `error during comparing: ${errMsg(e)}`);
}
}
if (compareResult.failReasons.length > 0) {
compareResult.status = "failed";
}
const checkContextForAfterHook = {
expectedSnapshot,
actualSnapshot,
checkParams: newCheckParams
};
const finalResult = await executeAfterCompareHook(
checkContextForAfterHook,
compareResult
);
return finalResult;
};
// src/server/services/check.service.ts
async function calculateTestStatus(testId) {
const checksInTest = await Check_model_default.find({ test: testId });
const statuses = checksInTest.map((x) => x.status[0]);
let testCalculatedStatus = "Failed";
if (statuses.every((x) => x === "new" || x === "passed")) {
testCalculatedStatus = "Passed";
}
if (statuses.every((x) => x === "new")) {
testCalculatedStatus = "New";
}
return testCalculatedStatus;
}
var validateBaselineParam = (params) => {
const mandatoryParams = ["markedAs", "markedById", "markedByUsername", "markedDate"];
for (const param of mandatoryParams) {
if (!params[param]) {
const errMsg2 = `invalid baseline parameters, '${param}' is empty, params: ${JSON.stringify(params)}`;
logger_default.error(errMsg2);
throw new Error(errMsg2);
}
}
};
async function createNewBaseline(params) {
const logOpts6 = {
scope: "createNewBaseline",
msgType: "CREATE"
};
validateBaselineParam(params);
const identFields = buildIdentObject(params);
const lastBaseline = await Baseline_model_default.findOne(identFields).sort({ createdDate: -1 }).exec();
const filter = { ...identFields, snapshootId: params.actualSnapshotId };
const baselineParams = { ...identFields };
if (lastBaseline?.ignoreRegions) {
baselineParams.ignoreRegions = lastBaseline.ignoreRegions;
}
if (typeof lastBaseline?.toleranceThreshold === "number") {
baselineParams.toleranceThreshold = lastBaseline.toleranceThreshold;
}
const update2 = {
$setOnInsert: {
...baselineParams,
snapshootId: params.actualSnapshotId,
createdDate: /* @__PURE__ */ new Date()
},
$set: {
markedAs: params.markedAs,
markedById: params.markedById,
markedByUsername: params.markedByUsername,
lastMarkedDate: params.markedDate
}
};
try {
const baseline = await Baseline_model_default.findOneAndUpdate(
filter,
update2,
{ new: true, upsert: true }
).exec();
logger_default.debug(`baseline upserted for snapshot id: ${params.actualSnapshotId}`, logOpts6);
logger_default.silly({ baseline });
return baseline;
} catch (err) {
if (err?.code === 11e3) {
logger_default.warn(`baseline duplicate key detected for filter ${JSON.stringify(filter)}, retrying fetch`, logOpts6);
const existing = await Baseline_model_default.findOne(filter).exec();
if (existing) {
existing.markedAs = params.markedAs;
existing.markedById = params.markedById;
existing.markedByUsername = params.markedByUsername;
existing.lastMarkedDate = params.markedDate;
existing.createdDate = /* @__PURE__ */ new Date();
existing.snapshootId = params.actualSnapshotId;
return existing.save();
}
}
logger_default.error(`cannot upsert baseline: ${err instanceof Error ? err.message : String(err)}`, logOpts6);
throw err;
}
}
var extractSnapshotId = (snapshot) => {
if (!snapshot) return void 0;
if (typeof snapshot === "string") return snapshot;
if (typeof snapshot === "object") {
const snapshotObj = snapshot;
if (snapshotObj._id) return String(snapshotObj._id);
if (snapshotObj.id) return String(snapshotObj.id);
if (typeof snapshotObj.toString === "function") return snapshotObj.toString();
}
return void 0;
};
var unwrapIdentValue = (value, visited = /* @__PURE__ */ new WeakSet()) => {
if (!value) return void 0;
if (typeof value !== "object") return value;
if (value instanceof Types5.ObjectId || value?._bsontype === "ObjectID") {
return value;
}
const obj = value;
if (visited.has(obj)) return void 0;
visited.add(obj);
if (obj._id && obj._id !== value) {
return unwrapIdentValue(obj._id, visited);
}
if (obj.id && obj.id !== value) {
return unwrapIdentValue(obj.id, visited);
}
return value;
};
var extractIdentValueAsString = (value) => {
const unwrapped = unwrapIdentValue(value);
if (!unwrapped) return "";
if (typeof unwrapped === "string") return unwrapped;
if (unwrapped instanceof Types5.ObjectId || unwrapped?._bsontype === "ObjectID") {
return unwrapped.toString();
}
return String(unwrapped);
};
var normalizeIdentValueForQuery = (field, value) => {
if (value === void 0 || value === null) return void 0;
const unwrapped = unwrapIdentValue(value);
if (field === "app") {
if (unwrapped instanceof Types5.ObjectId || unwrapped?._bsontype === "ObjectID") {
return unwrapped;
}
const strValue = extractIdentValueAsString(unwrapped);
if (!strValue) return void 0;
return Types5.ObjectId.isValid(strValue) ? new Types5.ObjectId(strValue) : strValue;
}
return extractIdentValueAsString(unwrapped);
};
var enrichChecksWithCurrentAcceptance = async (checks) => {
if (!checks || checks.length === 0) return [];
const plainChecks = checks.map((check) => check && typeof check.toJSON === "function" ? check.toJSON() : { ...check });
const identFields = ["name", "viewport", "browserName", "os", "app", "branch"];
const baselineQueries = [];
const checksByIdentKey = /* @__PURE__ */ new Map();
plainChecks.forEach((check) => {
const identKey = identFields.map((field) => extractIdentValueAsString(check?.[field])).join("|");
if (!checksByIdentKey.has(identKey)) {
checksByIdentKey.set(identKey, []);
const query = {};
identFields.forEach((field) => {
const normalized = normalizeIdentValueForQuery(field, check?.[field]);
if (normalized !== void 0) query[field] = normalized;
});
const hasAllFields = identFields.every((field) => query[field] !== void 0);
if (hasAllFields) {
baselineQueries.push(query);
} else {
logger_default.warn(`Check ${check._id} missing required ident fields. Has: ${Object.keys(query).join(", ")}`, {
scope: "enrichChecksWithCurrentAcceptance"
});
}
}
checksByIdentKey.get(identKey)?.push(check);
});
const baselinesMap = /* @__PURE__ */ new Map();
if (baselineQueries.length > 0) {
try {
const baselines = await Baseline_model_default.aggregate([
{
$match: { $or: baselineQueries }
},
{
$sort: { createdDate: -1 }
},
{
$group: {
_id: {
name: "$name",
viewport: "$viewport",
browserName: "$browserName",
os: "$os",
app: "$app",
branch: "$branch"
},
doc: { $first: "$$ROOT" }
}
},
{
$replaceRoot: { newRoot: "$doc" }
}
]).exec();
baselines.forEach((baseline) => {
const baselineObj = baseline;
const identKey = identFields.map((field) => extractIdentValueAsString(baselineObj?.[field])).join("|");
baselinesMap.set(identKey, baselineObj);
logger_default.debug(`[enrichChecks] Found baseline for identKey=${identKey}, snapshootId=${baselineObj.snapshootId}`, {
scope: "enrichChecksWithCurrentAcceptance"
});
});
} catch (err) {
logger_default.error(`[enrichChecks] Error fetching baselines: ${err}`, { scope: "enrichChecksWithCurrentAcceptance" });
throw err;
}
}
return plainChecks.map((check) => {
const identKey = identFields.map((field) => extractIdentValueAsString(check?.[field])).join("|");
const baseline = baselinesMap.get(identKey);
const actualSnapshotId = extractSnapshotId(check?.actualSnapshotId);
const baselineSnapshotId = baseline ? extractSnapshotId(baseline.snapshootId) : void 0;
const checkBaselineSnapshotId = extractSnapshotId(check?.baselineId);
const matchesOwnBaseline = Boolean(
actualSnapshotId && checkBaselineSnapshotId && actualSnapshotId === checkBaselineSnapshotId
);
const matchesLatestBaseline = Boolean(
actualSnapshotId && baselineSnapshotId && actualSnapshotId === baselineSnapshotId
);
const isCurrentlyAccepted = Boolean(
check?.markedAs === "accepted" && (matchesOwnBaseline || matchesLatestBaseline)
);
const hasKnownBaseline = Boolean(checkBaselineSnapshotId || baselineSnapshotId);
const wasAcceptedEarlier = Boolean(
check?.markedAs === "accepted" && hasKnownBaseline && !isCurrentlyAccepted
);
if (check?.markedAs === "accepted") {
logger_default.debug(`[enrichChecks] Check ${check._id}: actualSnapshot=${actualSnapshotId}, baselineSnapshot=${baselineSnapshotId}, checkBaselineSnapshot=${checkBaselineSnapshotId}, isCurrentlyAccepted=${isCurrentlyAccepted}, wasAcceptedEarlier=${wasAcceptedEarlier}, hasBaseline=${Boolean(baseline)}`, {
scope: "enrichChecksWithCurrentAcceptance"
});
}
return {
...check,
isCurrentlyAccepted,
wasAcceptedEarlier
};
});
};
var accept = async (id2, baselineId, user) => {
const logOpts6 = {
msgType: "ACCEPT",
itemType: "check",
ref: id2,
user: user?.username,
scope: "accept"
};
logger_default.debug(`accept check: ${id2}`, logOpts6);
const check = await Check_model_default.findById(id2).exec();
if (!check) throw new Error(`cannot find check with id: ${id2}`);
const test = await Test_model_default.findById(check.test).exec();
if (!test) throw new Error(`cannot find test with id: ${check.test}`);
check.markedById = user._id;
check.markedByUsername = user.username;
check.markedDate = /* @__PURE__ */ new Date();
check.markedAs = "accepted";
check.status = check.status[0] === "new" ? ["new"] : ["passed"];
check.updatedDate = /* @__PURE__ */ new Date();
if (baselineId) {
check.baselineId = new Types5.ObjectId(baselineId);
}
logger_default.debug(`update check with options: '${JSON.stringify(check.toObject())}'`, logOpts6);
const baseline = await createNewBaseline(check.toObject());
try {
await domSnapshotService.linkDomSnapshotToBaseline(id2, baseline._id.toString());
logger_default.debug(`DOM snapshot linked to baseline: '${baseline._id}'`, logOpts6);
} catch (domErr) {
logger_default.warn(`Failed to link DOM snapshot to baseline: ${domErr}`, logOpts6);
}
await check.save();
const testCalculatedStatus = await calculateTestStatus(String(check.test));
const testCalculatedAcceptedStatus = await calculateAcceptedStatus(check.test);
test.status = testCalculatedStatus;
test.markedAs = testCalculatedAcceptedStatus;
test.updatedDate = /* @__PURE__ */ new Date();
await Suite_model_default.findByIdAndUpdate(check.suite, { updatedDate: Date.now() });
logger_default.debug(`update test with status: '${testCalculatedStatus}', marked: '${testCalculatedAcceptedStatus}'`, logOpts6, {
msgType: "UPDATE",
itemType: "test",
ref: test._id
});
await test.save();
await check.save();
logger_default.debug(`check with id: '${id2}' was updated`, logOpts6);
const [enrichedCheck] = await enrichChecksWithCurrentAcceptance([check]);
webhookService.triggerWebhooks("check.updated", enrichedCheck).catch((e) => logger_default.error(`Webhook error: ${e}`));
return enrichedCheck;
};
async function removeCheck(id2, user) {
const logMeta = {
scope: "removeCheck",
itemType: "check",
ref: id2,
msgType: "REMOVE",
user: user?.username
};
try {
const check = await Check_model_default.findByIdAndDelete(id2).exec();
if (!check) throw new Error(`cannot find check with id: ${id2}`);
logger_default.debug(`check with id: '${id2}' was removed, update test: ${check.test}`, logMeta);
const test = await Test_model_default.findById(check.test).exec();
if (!test) throw new Error(`cannot find test with id: ${check.test}`);
const testCalculatedStatus = await calculateTestStatus(String(check.test));
const testCalculatedAcceptedStatus = await calculateAcceptedStatus(check.test);
test.status = testCalculatedStatus;
test.markedAs = testCalculatedAcceptedStatus;
test.updatedDate = /* @__PURE__ */ new Date();
await updateItemDate("VRSSuite", check.suite);
await test.save();
if (check.baselineId && String(check.baselineId) !== "undefined") {
logger_default.debug(`try to remove the snapshot, baseline: ${check.baselineId}`, logMeta);
await remove3(check.baselineId.toString());
}
if (check.actualSnapshotId && String(check.baselineId) !== "undefined") {
logger_default.debug(`try to remove the snapshot, actual: ${check.actualSnapshotId}`, logMeta);
await remove3(check.actualSnapshotId.toString());
}
if (check.diffId && String(check.baselineId) !== "undefined") {
logger_default.debug(`try to remove snapshot, diff: ${check.diffId}`, logMeta);
await remove3(check.diffId.toString());
}
try {
await domSnapshotService.removeDomSnapshotsByCheckId(id2);
logger_default.debug(`DOM snapshots removed for check: ${id2}`, logMeta);
} catch (domErr) {
logger_default.warn(`Failed to remove DOM snapshots for check ${id2}: ${domErr}`, logMeta);
}
return check;
} catch (e) {
const errMsg2 = `cannot remove a check with id: '${id2}', error: '${e instanceof Error ? e.stack : String(e)}'`;
logger_default.error(errMsg2, logMeta);
throw new Error(errMsg2);
}
}
var remove4 = async (id2, user) => {
const logOpts6 = {
scope: "removeCheck",
itemType: "check",
ref: id2,
user: user?.username,
msgType: "REMOVE"
};
logger_default.info(`remove check with, id: '${id2}', user: '${user.username}'`, logOpts6);
return removeCheck(id2, user);
};
var update = async (id2, opts, user) => {
const logMeta = {
msgType: "UPDATE",
itemType: "check",
ref: id2,
user,
scope: "updateCheck"
};
logger_default.debug(`update check with id '${id2}' with params '${JSON.stringify(opts, null, 2)}'`, logMeta);
const check = await Check_model_default.findOneAndUpdate({ _id: id2 }, opts, { new: true }).exec();
if (!check) throw new Error(`cannot find check with id: ${id2}`);
const test = await Test_model_default.findOne({ _id: check.test }).exec();
if (!test) throw new Error(`cannot find test with id: ${check.test}`);
test.status = await calculateTestStatus(String(check.test));
await updateItemDate("VRSCheck", check);
await updateItemDate("VRSTest", test);
await test.save();
await check.save();
webhookService.triggerWebhooks("check.updated", check).catch((e) => logger_default.error(`Webhook error: ${e}`));
return check;
};
var recompare = async (id2, user) => {
const logOpts6 = {
scope: "recompareCheck",
itemType: "check",
ref: id2,
user: user?.username,
msgType: "COMPARE"
};
const check = await Check_model_default.findById(id2).exec();
if (!check) throw new Error(`cannot find check with id: ${id2}`);
if (!check.baselineId) {
throw new Error(`cannot recompare check '${id2}': baselineId is empty`);
}
if (!check.actualSnapshotId) {
throw new Error(`cannot recompare check '${id2}': actualSnapshotId is empty`);
}
const baselineSnapshot = await Snapshot_model_default.findById(check.baselineId).exec();
if (!baselineSnapshot) {
throw new Error(`cannot recompare check '${id2}': baseline snapshot '${check.baselineId}' not found`);
}
const actualSnapshot = await Snapshot_model_default.findById(check.actualSnapshotId).exec();
if (!actualSnapshot) {
throw new Error(`cannot recompare check '${id2}': actual snapshot '${check.actualSnapshotId}' not found`);
}
const oldDiffId = check.diffId ? check.diffId.toString() : null;
const checkParamsForCompare = {
test: check.test.toString(),
name: check.name,
status: "pending",
viewport: check.viewport || "",
browserName: check.browserName || "",
browserVersion: check.browserVersion || "",
browserFullVersion: check.browserFullVersion || "",
os: check.os || "",
updatedDate: Date.now(),
suite: check.suite.toString(),
app: check.app.toString(),
branch: check.branch || "",
run: check.run ? check.run.toString() : "",
creatorId: check.creatorId ? check.creatorId.toString() : user._id.toString(),
creatorUsername: check.creatorUsername || user.username,
failReasons: [],
actualSnapshotId: check.actualSnapshotId.toString(),
hashCode: "",
toleranceThreshold: check.toleranceThreshold
};
const compareResult = await compareCheck(
baselineSnapshot,
actualSnapshot,
checkParamsForCompare,
false,
user
);
check.status = [compareResult.status];
check.result = compareResult.result;
check.failReasons = compareResult.failReasons;
check.updatedDate = /* @__PURE__ */ new Date();
if (compareResult.diffId) {
check.diffId = new Types5.ObjectId(compareResult.diffId);
} else {
check.diffId = void 0;
}
await check.save();
const test = await Test_model_default.findById(check.test).exec();
if (test) {
const testCalculatedStatus = await calculateTestStatus(String(check.test));
const testCalculatedAcceptedStatus = await calculateAcceptedStatus(check.test);
test.status = testCalculatedStatus;
test.markedAs = testCalculatedAcceptedStatus;
test.updatedDate = /* @__PURE__ */ new Date();
await test.save();
await Suite_model_default.findByIdAndUpdate(check.suite, { updatedDate: Date.now() });
}
const newDiffId = check.diffId ? check.diffId.toString() : null;
if (oldDiffId && oldDiffId !== newDiffId) {
remove3(oldDiffId).catch((e) => {
logger_default.warn(`failed to remove old diff snapshot '${oldDiffId}': ${String(e)}`, logOpts6);
});
}
const [enrichedCheck] = await enrichChecksWithCurrentAcceptance([check]);
webhookService.triggerWebhooks("check.updated", enrichedCheck).catch((e) => logger_default.error(`Webhook error: ${e}`));
return enrichedCheck;
};
var createCheckDocument = async (checkParams, session) => {
const [check] = await Check_model_default.create([checkParams], { session });
webhookService.triggerWebhooks("check.created", check).catch((e) => logger_default.error(`Webhook error: ${e}`));
return check;
};
// src/server/services/client.service.ts
import mongoose23 from "mongoose";
init_config();
// src/server/services/user.service.ts
init_utils();
init_models();
init_utils();
init_logger();
// src/server/services/admin-data-job.service.ts
init_config();
import fs6 from "fs";
import { promises as fsp3 } from "fs";
import path7 from "path";
import { randomUUID as randomUUID2 } from "crypto";
import { createGzip, createGunzip } from "zlib";
import { promisify } from "util";
import { pipeline } from "stream";
import tar from "tar-stream";
import mongoose24 from "mongoose";
var pipelineAsync = promisify(pipeline);
var { BSON } = mongoose24.mongo;
var activeTasks = /* @__PURE__ */ new Map();
var META_FILENAME = "job.json";
var LOG_FILENAME = "job.log";
var DB_EXPORT_DIRNAME = "db-export";
var isActiveStatus = (status) => status === "pending" || status === "running";
var getJobDir = (jobId) => path7.join(config.adminDataJobsPath, jobId);
var getJobMetaPath = (jobId) => path7.join(getJobDir(jobId), META_FILENAME);
var getJobLogPath = (jobId) => path7.join(getJobDir(jobId), LOG_FILENAME);
async function ensureDir(dirPath) {
await fsp3.mkdir(dirPath, { recursive: true });
}
async function removeDirSafe(dirPath) {
if (!dirPath) return;
await fsp3.rm(dirPath, { recursive: true, force: true });
}
async function fileExists(filePath) {
try {
await fsp3.access(filePath);
return true;
} catch {
return false;
}
}
async function writeJob(job) {
await ensureDir(job.workDir);
await fsp3.writeFile(getJobMetaPath(job.id), JSON.stringify(job, null, 2));
}
async function appendLog(jobId, message) {
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
`;
await fsp3.appendFile(getJobLogPath(jobId), line);
}
async function readJob(jobId) {
try {
const raw = await fsp3.readFile(getJobMetaPath(jobId), "utf8");
return JSON.parse(raw);
} catch {
return null;
}
}
async function listJobsInternal() {
await ensureDir(config.adminDataJobsPath);
const entries = await fsp3.readdir(config.adminDataJobsPath, { withFileTypes: true });
const jobs = await Promise.all(entries.filter((entry) => entry.isDirectory()).map((entry) => readJob(entry.name)));
return jobs.filter((job) => Boolean(job)).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}
async function updateJob(jobId, patch) {
const current = await readJob(jobId);
if (!current) {
throw new Error(`Job not found: ${jobId}`);
}
const nextJob = { ...current, ...patch };
await writeJob(nextJob);
return nextJob;
}
async function updateProgress(jobId, progress, message, statsPatch) {
const current = await readJob(jobId);
if (!current) return;
const nextProgress = { ...current.progress, ...progress };
if (typeof nextProgress.current === "number" && typeof nextProgress.total === "number" && nextProgress.total > 0) {
nextProgress.percent = Math.min(100, Math.round(nextProgress.current / nextProgress.total * 100));
}
await writeJob({
...current,
progress: nextProgress,
message: message ?? current.message,
stats: { ...current.stats, ...statsPatch }
});
}
async function finalizeJob(jobId, status, patch = {}) {
const current = await readJob(jobId);
if (!current) return null;
const job = {
...current,
...patch,
status,
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
};
await writeJob(job);
return job;
}
function normalizeArchiveName(type) {
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
switch (type) {
case "db_backup":
return `syngrisi-db-backup-${stamp}.tar.gz`;
case "screenshots_backup":
return `syngrisi-screenshots-backup-${stamp}.tar.gz`;
default:
return `${type}-${stamp}.tar.gz`;
}
}
async function getRunningJobs() {
const jobs = await listJobsInternal();
return jobs.filter((job) => isActiveStatus(job.status));
}
async function hasActiveDatabaseRestoreJob() {
const jobs = await getRunningJobs();
return jobs.some((job) => job.type === "db_restore");
}
async function assertCanStartJob() {
const runningJobs = await getRunningJobs();
if (runningJobs.length >= config.adminDataMaxConcurrentJobs) {
throw new Error(`Another data job is already active: ${runningJobs[0].id}`);
}
}
async function countFilesRecursive(rootDir) {
let count = 0;
const stack = [rootDir];
while (stack.length > 0) {
const currentDir = stack.pop();
if (!currentDir) continue;
const dir = await fsp3.opendir(currentDir);
for await (const entry of dir) {
const entryPath = path7.join(currentDir, entry.name);
if (entry.isDirectory()) {
stack.push(entryPath);
} else if (entry.isFile()) {
count += 1;
}
}
}
return count;
}
async function streamToFile(sourcePath, targetPath) {
await ensureDir(path7.dirname(targetPath));
await pipelineAsync(fs6.createReadStream(sourcePath), fs6.createWriteStream(targetPath));
}
function getActiveState(jobId) {
return activeTasks.get(jobId);
}
function assertNotCancelled(jobId) {
if (getActiveState(jobId)?.cancelRequested) {
throw new Error("Job cancelled");
}
}
async function createJob(type, params) {
await assertCanStartJob();
const id2 = randomUUID2();
const workDir = getJobDir(id2);
await ensureDir(workDir);
const job = {
id: id2,
type,
status: "pending",
params,
progress: { stage: "queued", percent: 0 },
message: "Queued",
stats: {},
downloadAvailable: false,
workDir,
logFilePath: getJobLogPath(id2),
createdAt: (/* @__PURE__ */ new Date()).toISOString()
};
await writeJob(job);
await appendLog(id2, `Job created: ${type}`);
return job;
}
async function addFileToTar(pack, filePath, entryName) {
const stat = await fsp3.stat(filePath);
await new Promise((resolve, reject) => {
const entry = pack.entry({ name: entryName, size: stat.size, mode: stat.mode }, (error) => {
if (error) {
reject(error);
return;
}
resolve();
});
fs6.createReadStream(filePath).on("error", reject).pipe(entry).on("error", reject);
});
}
async function createTarGzArchive(outputPath, items) {
await ensureDir(path7.dirname(outputPath));
const pack = tar.pack();
const gzip = createGzip();
const output = fs6.createWriteStream(outputPath);
const pipelinePromise = pipelineAsync(pack, gzip, output);
for (const item of items) {
await addFileToTar(pack, item.path, item.name);
}
pack.finalize();
await pipelinePromise;
}
async function extractTarGzArchive(archivePath, destinationDir) {
await ensureDir(destinationDir);
const extract = tar.extract();
await new Promise((resolve, reject) => {
extract.on("entry", (header, stream, next) => {
const outputPath = path7.join(destinationDir, header.name);
const finishEntry = (error) => {
if (error) {
reject(error);
return;
}
next();
};
if (header.type === "directory") {
void ensureDir(outputPath).then(() => {
stream.resume();
finishEntry();
}).catch((error) => finishEntry(error));
return;
}
void ensureDir(path7.dirname(outputPath)).then(() => pipelineAsync(stream, fs6.createWriteStream(outputPath))).then(() => finishEntry()).catch((error) => finishEntry(error));
});
extract.on("finish", () => resolve());
extract.on("error", reject);
fs6.createReadStream(archivePath).on("error", reject).pipe(createGunzip()).on("error", reject).pipe(extract).on("error", reject);
});
}
async function countFilesInTarGzArchive(archivePath) {
const extract = tar.extract();
let totalFiles = 0;
await new Promise((resolve, reject) => {
extract.on("entry", (_header, stream, next) => {
if (_header.type === "file") {
totalFiles += 1;
}
stream.resume();
next();
});
extract.on("finish", () => resolve());
extract.on("error", reject);
fs6.createReadStream(archivePath).on("error", reject).pipe(createGunzip()).on("error", reject).pipe(extract).on("error", reject);
});
return totalFiles;
}
async function writeCollectionDump(jobId, collectionName, outputPath) {
const db = mongoose24.connection.db;
if (!db) {
throw new Error("MongoDB connection is not available");
}
let documentCount = 0;
await ensureDir(path7.dirname(outputPath));
const gzip = createGzip();
const output = fs6.createWriteStream(outputPath);
gzip.pipe(output);
const cursor = db.collection(collectionName).find({}, { timeout: false });
for await (const doc of cursor) {
assertNotCancelled(jobId);
gzip.write(BSON.serialize(doc));
documentCount += 1;
if (documentCount % 1e3 === 0) {
await appendLog(jobId, `Exported ${documentCount} documents from ${collectionName}`);
}
}
gzip.end();
await new Promise((resolve, reject) => {
output.on("finish", () => resolve());
output.on("error", reject);
gzip.on("error", reject);
});
return documentCount;
}
async function walkFiles(rootDir, onFile) {
const stack = [rootDir];
while (stack.length > 0) {
const currentDir = stack.pop();
if (!currentDir) continue;
const dir = await fsp3.opendir(currentDir);
for await (const entry of dir) {
const fullPath = path7.join(currentDir, entry.name);
const relativePath = path7.relative(rootDir, fullPath);
if (entry.isDirectory()) {
stack.push(fullPath);
} else if (entry.isFile()) {
await onFile(fullPath, relativePath);
}
}
}
}
async function runDbBackup(job) {
const db = mongoose24.connection.db;
if (!db) {
throw new Error("MongoDB connection is not available");
}
const archiveName = normalizeArchiveName(job.type);
const archivePath = path7.join(job.workDir, archiveName);
const exportDir = path7.join(job.workDir, DB_EXPORT_DIRNAME);
await ensureDir(exportDir);
await updateProgress(job.id, { stage: "indexing", percent: 5 }, "Inspecting database");
const collectionInfos = await db.listCollections({}, { nameOnly: true }).toArray();
const collections = collectionInfos.map((item) => item.name).filter((name) => !name.startsWith("system."));
const manifest = {
format: "syngrisi-db-backup-v1",
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
databaseName: db.databaseName,
collections: []
};
let processedCollections = 0;
for (const collectionName of collections) {
assertNotCancelled(job.id);
const dumpFileName = `${collectionName}.bson.gz`;
const dumpPath = path7.join(exportDir, dumpFileName);
await updateProgress(job.id, { stage: "dumping", current: processedCollections, total: collections.length }, `Exporting ${collectionName}`);
const documentCount = await writeCollectionDump(job.id, collectionName, dumpPath);
const indexes = await db.collection(collectionName).indexes();
manifest.collections.push({
name: collectionName,
dumpFile: dumpFileName,
documentCount,
indexes: indexes.map((index) => JSON.parse(JSON.stringify(index)))
});
processedCollections += 1;
await updateProgress(job.id, { stage: "dumping", current: processedCollections, total: collections.length }, `Exported ${collectionName}`);
}
const manifestPath = path7.join(exportDir, "manifest.json");
await fsp3.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
await updateProgress(job.id, { stage: "archiving", percent: 80 }, "Creating archive");
const tarItems = [
{ path: manifestPath, name: "manifest.json" },
...manifest.collections.map((collection) => ({
path: path7.join(exportDir, collection.dumpFile),
name: `collections/${collection.dumpFile}`
}))
];
await createTarGzArchive(archivePath, tarItems);
const stat = await fsp3.stat(archivePath);
await finalizeJob(job.id, "completed", {
message: "Database backup completed",
archivePath,
archiveName,
downloadAvailable: true,
stats: {
archiveSizeBytes: stat.size,
processedFiles: manifest.collections.reduce((acc, item) => acc + item.documentCount, 0),
totalFiles: manifest.collections.length
},
progress: { stage: "completed", percent: 100, current: manifest.collections.length, total: manifest.collections.length }
});
}
async function recreateIndexes(collectionName, indexes) {
const db = mongoose24.connection.db;
if (!db) {
throw new Error("MongoDB connection is not available");
}
const filtered = indexes.filter((index) => index.name !== "_id_");
if (filtered.length === 0) {
return;
}
const definitions = filtered.map((index) => {
const { key, ...options } = index;
return { key, ...options };
});
await db.collection(collectionName).createIndexes(definitions);
}
async function importCollectionDump(jobId, collectionName, dumpPath) {
const db = mongoose24.connection.db;
if (!db) {
throw new Error("MongoDB connection is not available");
}
const collection = db.collection(collectionName);
const batch = [];
let inserted = 0;
const flush = async () => {
if (batch.length === 0) return;
await collection.insertMany(batch, { ordered: false });
inserted += batch.length;
batch.length = 0;
};
const input = fs6.createReadStream(dumpPath).pipe(createGunzip());
let pending = Buffer.alloc(0);
for await (const chunk of input) {
assertNotCancelled(jobId);
pending = Buffer.concat([pending, chunk]);
while (pending.length >= 4) {
const documentSize = pending.readInt32LE(0);
if (documentSize <= 0) {
throw new Error(`Invalid BSON document size ${documentSize} in ${collectionName}`);
}
if (pending.length < documentSize) {
break;
}
const documentBuffer = pending.subarray(0, documentSize);
pending = pending.subarray(documentSize);
batch.push(BSON.deserialize(documentBuffer));
if (batch.length >= 1e3) {
await flush();
await appendLog(jobId, `Imported ${inserted} documents into ${collectionName}`);
}
}
}
if (pending.length > 0) {
throw new Error(`Unexpected trailing BSON bytes in ${collectionName}`);
}
await flush();
return inserted;
}
async function runDbRestore(job) {
const db = mongoose24.connection.db;
if (!db) {
throw new Error("MongoDB connection is not available");
}
const uploadPath = String(job.uploadPath || "");
if (!uploadPath || !await fileExists(uploadPath)) {
throw new Error("Uploaded archive is missing");
}
const extractDir = path7.join(job.workDir, "extracted-db");
await updateProgress(job.id, { stage: "extracting", percent: 10 }, "Extracting database archive");
await extractTarGzArchive(uploadPath, extractDir);
const manifestPath = path7.join(extractDir, "manifest.json");
if (!await fileExists(manifestPath)) {
throw new Error("manifest.json is missing from database archive");
}
const manifest = JSON.parse(await fsp3.readFile(manifestPath, "utf8"));
if (manifest.format !== "syngrisi-db-backup-v1") {
throw new Error("Unsupported database backup format");
}
await updateProgress(job.id, { stage: "dropping_db", percent: 30 }, "Dropping current database");
await db.dropDatabase();
let importedCollections = 0;
for (const collectionInfo of manifest.collections) {
assertNotCancelled(job.id);
const dumpPath = path7.join(extractDir, "collections", collectionInfo.dumpFile);
await updateProgress(
job.id,
{ stage: "restoring", current: importedCollections, total: manifest.collections.length },
`Restoring ${collectionInfo.name}`
);
await importCollectionDump(job.id, collectionInfo.name, dumpPath);
await recreateIndexes(collectionInfo.name, collectionInfo.indexes);
importedCollections += 1;
await updateProgress(
job.id,
{ stage: "restoring", current: importedCollections, total: manifest.collections.length },
`Restored ${collectionInfo.name}`
);
}
await finalizeJob(job.id, "completed", {
message: "Database restore completed",
progress: { stage: "completed", percent: 100, current: importedCollections, total: manifest.collections.length },
stats: {
processedFiles: manifest.collections.reduce((acc, item) => acc + item.documentCount, 0),
totalFiles: manifest.collections.length
}
});
}
async function runScreenshotsBackup(job) {
const archiveName = normalizeArchiveName(job.type);
const archivePath = path7.join(job.workDir, archiveName);
const totalFiles = await countFilesRecursive(config.defaultImagesPath);
let processedFiles = 0;
await ensureDir(path7.dirname(archivePath));
const pack = tar.pack();
const gzip = createGzip();
const output = fs6.createWriteStream(archivePath);
const archivePipeline = pipelineAsync(pack, gzip, output);
await updateProgress(job.id, { stage: "archiving", current: 0, total: totalFiles }, "Archiving screenshots", { totalFiles });
await walkFiles(config.defaultImagesPath, async (fullPath, relativePath) => {
assertNotCancelled(job.id);
await addFileToTar(pack, fullPath, relativePath);
processedFiles += 1;
if (processedFiles % 200 === 0 || processedFiles === totalFiles) {
await updateProgress(job.id, { stage: "archiving", current: processedFiles, total: totalFiles }, "Archiving screenshots", { processedFiles, totalFiles });
}
});
pack.finalize();
await archivePipeline;
const stat = await fsp3.stat(archivePath);
await finalizeJob(job.id, "completed", {
message: "Screenshots backup completed",
archivePath,
archiveName,
downloadAvailable: true,
stats: { archiveSizeBytes: stat.size, processedFiles, totalFiles },
progress: { stage: "completed", percent: 100, current: processedFiles, total: totalFiles }
});
}
async function runScreenshotsRestore(job) {
const uploadPath = String(job.uploadPath || "");
const skipExisting = Boolean(job.params.skipExisting);
if (!uploadPath || !await fileExists(uploadPath)) {
throw new Error("Uploaded archive is missing");
}
const totalFiles = await countFilesInTarGzArchive(uploadPath);
let processedFiles = 0;
let importedFiles = 0;
let skippedFiles = 0;
let errorFiles = 0;
await updateProgress(job.id, { stage: "importing", current: 0, total: totalFiles }, "Importing screenshots", { totalFiles });
const extract = tar.extract();
await new Promise((resolve, reject) => {
extract.on("entry", (header, stream, next) => {
const finish = (error) => {
if (error) {
reject(error);
return;
}
next();
};
if (header.type !== "file") {
stream.resume();
finish();
return;
}
const relativePath = header.name;
const targetPath = path7.join(config.defaultImagesPath, relativePath);
void (async () => {
assertNotCancelled(job.id);
const exists = await fileExists(targetPath);
if (exists && skipExisting) {
skippedFiles += 1;
stream.resume();
} else {
await ensureDir(path7.dirname(targetPath));
if (exists) {
await fsp3.rm(targetPath, { force: true });
}
await pipelineAsync(stream, fs6.createWriteStream(targetPath));
importedFiles += 1;
}
processedFiles += 1;
if (processedFiles % 200 === 0 || processedFiles === totalFiles) {
await updateProgress(
job.id,
{ stage: "importing", current: processedFiles, total: totalFiles },
"Importing screenshots",
{ processedFiles, importedFiles, skippedFiles, errorFiles, totalFiles }
);
}
})().then(() => finish()).catch(async (error) => {
errorFiles += 1;
await appendLog(job.id, `Failed to import ${relativePath}: ${error.message}`);
stream.resume();
processedFiles += 1;
finish();
});
});
extract.on("finish", () => resolve());
extract.on("error", reject);
fs6.createReadStream(uploadPath).on("error", reject).pipe(createGunzip()).on("error", reject).pipe(extract).on("error", reject);
});
await finalizeJob(job.id, "completed", {
message: "Screenshots restore completed",
progress: { stage: "completed", percent: 100, current: processedFiles, total: totalFiles },
stats: { totalFiles, processedFiles, importedFiles, skippedFiles, errorFiles }
});
}
async function cleanupExpiredJobs() {
const jobs = await listJobsInternal();
const now = Date.now();
await Promise.all(jobs.map(async (job) => {
if (isActiveStatus(job.status)) return;
const finishedAt = job.finishedAt ? new Date(job.finishedAt).getTime() : new Date(job.createdAt).getTime();
if (now - finishedAt > config.adminDataJobsTtlMs) {
await removeDirSafe(job.workDir);
}
}));
}
async function cleanupJobWorkdirs(jobId) {
const job = await readJob(jobId);
if (!job) return;
await removeDirSafe(path7.join(job.workDir, "staging"));
await removeDirSafe(path7.join(job.workDir, "extracted"));
await removeDirSafe(path7.join(job.workDir, "extracted-db"));
await removeDirSafe(path7.join(job.workDir, DB_EXPORT_DIRNAME));
if (job.status === "completed" && job.downloadAvailable) {
if (job.uploadPath) {
await fsp3.rm(job.uploadPath, { force: true });
}
return;
}
if (job.uploadPath) {
await fsp3.rm(job.uploadPath, { force: true });
}
}
async function markStaleJobsFailed() {
const jobs = await listJobsInternal();
await Promise.all(jobs.map(async (job) => {
if (isActiveStatus(job.status)) {
await finalizeJob(job.id, "failed", {
message: "Marked as failed after server restart",
error: "Server restarted while job was running",
downloadAvailable: false
});
}
}));
}
async function initialize() {
await ensureDir(config.adminDataJobsPath);
await markStaleJobsFailed();
await cleanupExpiredJobs();
}
async function startJob(job) {
setImmediate(() => {
void runJob(job.id);
});
}
async function runJob(jobId) {
const currentJob = await readJob(jobId);
if (!currentJob || currentJob.status === "cancelled") {
return;
}
activeTasks.set(jobId, { cancelRequested: false });
const job = await updateJob(jobId, {
status: "running",
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
progress: { stage: "preparing", percent: 0 },
message: "Preparing"
});
try {
switch (job.type) {
case "db_backup":
await runDbBackup(job);
break;
case "db_restore":
await runDbRestore(job);
break;
case "screenshots_backup":
await runScreenshotsBackup(job);
break;
case "screenshots_restore":
await runScreenshotsRestore(job);
break;
default:
throw new Error(`Unsupported job type: ${job.type}`);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const cancelled = message === "Job cancelled";
await appendLog(jobId, `Job ${cancelled ? "cancelled" : "failed"}: ${message}`);
await finalizeJob(jobId, cancelled ? "cancelled" : "failed", {
error: cancelled ? void 0 : message,
message: cancelled ? "Cancelled" : message,
downloadAvailable: false
});
} finally {
activeTasks.delete(jobId);
await cleanupJobWorkdirs(jobId);
}
}
async function persistUploadToJobDir(file, jobDir, fileName) {
const targetPath = path7.join(jobDir, fileName);
await ensureDir(jobDir);
if (file.tempFilePath) {
await fsp3.rename(file.tempFilePath, targetPath).catch(async () => {
await streamToFile(file.tempFilePath, targetPath);
await fsp3.rm(file.tempFilePath, { force: true });
});
} else {
await fsp3.writeFile(targetPath, file.data);
}
return targetPath;
}
async function createDbBackupJob() {
const job = await createJob("db_backup", {});
await startJob(job);
return readJob(job.id);
}
async function createScreenshotsBackupJob() {
const job = await createJob("screenshots_backup", {});
await startJob(job);
return readJob(job.id);
}
async function createDbRestoreJob(file) {
const job = await createJob("db_restore", {});
const uploadPath = await persistUploadToJobDir(file, path7.join(job.workDir, "staging"), file.name || "db-restore.tar.gz");
await updateJob(job.id, {
uploadPath,
message: "Upload stored, waiting for restore"
});
const updated = await readJob(job.id);
if (!updated) throw new Error("Failed to read created job");
await startJob(updated);
return readJob(updated.id);
}
async function createScreenshotsRestoreJob(file, skipExisting) {
const job = await createJob("screenshots_restore", { skipExisting });
const uploadPath = await persistUploadToJobDir(file, path7.join(job.workDir, "staging"), file.name || "screenshots-restore.tar.gz");
await updateJob(job.id, {
uploadPath,
message: "Upload stored, waiting for restore"
});
const updated = await readJob(job.id);
if (!updated) throw new Error("Failed to read created job");
await startJob(updated);
return readJob(updated.id);
}
async function getJob(jobId) {
return readJob(jobId);
}
async function getJobLog(jobId) {
try {
return await fsp3.readFile(getJobLogPath(jobId), "utf8");
} catch {
return "";
}
}
async function cancelJob(jobId) {
const job = await readJob(jobId);
if (!job) {
throw new Error(`Job not found: ${jobId}`);
}
const active = activeTasks.get(jobId);
if (!active) {
return finalizeJob(jobId, job.status === "pending" ? "cancelled" : job.status, {
message: job.status === "pending" ? "Cancelled" : job.message
});
}
active.cancelRequested = true;
await appendLog(jobId, "Cancellation requested");
return updateJob(jobId, { message: "Cancellation requested" });
}
async function createDownloadStream(jobId) {
const job = await readJob(jobId);
if (!job || job.status !== "completed" || !job.downloadAvailable || !job.archivePath) {
throw new Error("Download is not available for this job");
}
if (!await fileExists(job.archivePath)) {
throw new Error("Archive file is missing");
}
return {
fileName: job.archiveName || path7.basename(job.archivePath),
stream: fs6.createReadStream(job.archivePath)
};
}
async function deleteJob(jobId) {
const job = await readJob(jobId);
if (!job) {
throw new Error(`Job not found: ${jobId}`);
}
if (isActiveStatus(job.status)) {
throw new Error("Cannot delete an active job. Cancel it first.");
}
await removeDirSafe(job.workDir);
return { deleted: true, id: jobId };
}
var adminDataJobService = {
initialize,
listJobs: listJobsInternal,
getJob,
getJobLog,
createDbBackupJob,
createDbRestoreJob,
createScreenshotsBackupJob,
createScreenshotsRestoreJob,
cancelJob,
deleteJob,
createDownloadStream,
hasActiveDatabaseRestoreJob
};
// src/server/controllers/runs.controller.ts
var get2 = catchAsync_default(async (req, res) => {
const filter = typeof req.query.filter === "string" ? deserializeIfJSON_default(req.query.filter) || {} : {};
const options = pick_default(req.query, ["sortBy", "limit", "page"]);
const result = await generic_service_exports.get("VRSRun", filter, options);
res.send(result);
});
var remove5 = catchAsync_default(async (req, res) => {
const { id: id2 } = req.params;
if (!req.user) throw new Error("req.user is empty");
const result = await run_service_exports.remove(id2, req?.user);
res.send(result);
});
// src/server/utils/validateRequest.ts
init_httpStatus();
import { ZodError } from "zod";
// src/server/utils/ServiceResponse.ts
var ServiceResponse = class {
success;
message;
responseObject;
statusCode;
constructor(status, message, responseObject, statusCode) {
this.success = status === 0 /* Success */;
this.message = message;
this.responseObject = responseObject;
this.statusCode = statusCode;
}
};
// src/server/utils/validateRequest.ts
init_logger();
init_errMsg();
var logOpts5 = {
scope: "validateRequests",
itemType: "type",
msgType: "VALIDATION"
};
var sensitiveKeys = /* @__PURE__ */ new Set([
"password",
"currentpassword",
"newpassword",
"apikey",
"sso_client_secret",
"sso_cert",
"clientsecret",
"secret",
"token"
]);
var sanitizeValueForLogging = (value) => {
if (Array.isArray(value)) {
return value.map((item) => sanitizeValueForLogging(item));
}
if (value && typeof value === "object") {
return Object.entries(value).reduce((acc, [key, val]) => {
if (sensitiveKeys.has(key.toLowerCase())) {
acc[key] = "[REDACTED]";
} else {
acc[key] = sanitizeValueForLogging(val);
}
return acc;
}, Array.isArray(value) ? [] : {});
}
return value;
};
function getReceivedValueFromRequest(request, path8) {
let currentValue = request;
path8.forEach((segment) => {
currentValue = currentValue[segment];
});
return currentValue;
}
var validateRequest = (schema, endpoint = "") => (req, res, next) => {
try {
const parsed = schema.parse({
body: req.body,
query: req.query,
params: req.params
});
if (parsed.body) req.body = parsed.body;
next();
} catch (err) {
if (err instanceof ZodError) {
const zodErrors = Array.isArray(err.errors) ? err.errors : [];
if (zodErrors.length === 0) {
logger_default.error(`ZodError with empty errors array! Raw error: ${JSON.stringify(err)}`, logOpts5);
}
const sanitizedBody = sanitizeValueForLogging(req.body);
const sanitizedQuery = sanitizeValueForLogging(req.query);
const sanitizedParams = sanitizeValueForLogging(req.params);
const errors = zodErrors.map((e) => {
const receivedValue = getReceivedValueFromRequest(
{ body: sanitizedBody, query: sanitizedQuery, params: sanitizedParams },
e.path
);
return `
Error path: '${e.path.join(".")}':
Error ${e.message}, but received ${JSON.stringify(receivedValue)}`;
}).join(", ");
const errorMessage = ` ${endpoint ? '\nValidation error in the endpoint: "' + endpoint + '"' : ""}${errors},
HTTP PROPERTIES:
body: ${JSON.stringify(sanitizedBody, null, " ")},
query: ${JSON.stringify(sanitizedQuery, null, " ")},
params: ${JSON.stringify(sanitizedParams, null, " ")}`;
const statusCode = HttpStatus.BAD_REQUEST;
logger_default.error(errorMessage, logOpts5);
res.status(statusCode).send(new ServiceResponse(1 /* Failed */, errorMessage, null, statusCode));
} else {
logger_default.error(`Unexpected error: ${errMsg(err)}`, logOpts5);
next(err);
}
}
};
// src/server/schemas/Runs.schema.ts
import { z as z3 } from "zod";
// src/server/schemas/utils/commonValidations.ts
import { z as z2 } from "zod";
import { extendZodWithOpenApi as extendZodWithOpenApi2 } from "@asteasolutions/zod-to-openapi";
// src/server/schemas/common/Version.schema.ts
import { z } from "zod";
import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi";
extendZodWithOpenApi(z);
var VersionBaseSchema = z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be in the format "x.y.z"');
var VersionSchema = VersionBaseSchema.transform((value) => {
const parts = value.split(".");
return {
major: parseInt(parts[0]),
minor: parseInt(parts[1]),
patch: parseInt(parts[2])
};
});
// src/server/schemas/utils/commonValidations.ts
extendZodWithOpenApi2(z2);
var mongooseIdRegex = /^[0-9a-fA-F]{24}$/;
var id = z2.string().regex(mongooseIdRegex, {
message: "Invalid Mongoose ObjectId format: /^[0-9a-fA-F]{24}$/"
}).openapi({
description: "baseline ID",
example: "6bbF35cAB3C59dA969edAe79"
});
var commonValidations = {
id,
version: VersionBaseSchema.openapi({ example: "1.1.2" }),
positiveNumberString: z2.string().refine((value) => {
const num2 = Number(value);
return Number.isInteger(num2) && num2 >= 0;
}, {
message: "String must be a positive number or 0"
}),
password: z2.string().min(6).regex(/(?=.*[0-9])/, "Password must include a number").regex(/(?=.*[a-z])/, "Password must include a lowercase letter").regex(/(?=.*[A-Z])/, "Password must include an uppercase letter").refine((value) => {
return /(?=.*[!@#$%^&*(),.?":{}|<>-])/.test(value);
}, {
message: "Password must include a special symbol"
}).openapi({ example: "Aa1!IJASSNOJ" }),
username: z2.string().min(1).openapi({ example: "john.doe@example.com" }),
// TODO: workaround TBD
date: z2.string().refine((val) => {
const date = new Date(val);
return !isNaN(date.getTime());
}, {
message: "Invalid date format"
}),
paramsId: { params: z2.object({ id }) },
paramsTestId: { params: z2.object({ testid: id }) },
success: z2.object({
message: z2.literal("success")
})
};
// src/server/schemas/Runs.schema.ts
var RunResponseSchema = z3.object({
_id: commonValidations.id,
name: z3.string().min(1).openapi({
description: "Name of the run",
example: "DEBUG (VIKTAR)"
}),
app: commonValidations.id.openapi({
description: "App identifier",
example: "6651dd45b9c3e1e0b8c1ce26"
}),
ident: z3.string().uuid().openapi({
description: "Identifier for the run",
example: "7a930247-e422-4833-8ab6-b136c23d07e9"
}),
createdDate: z3.string().datetime().openapi({
description: "Creation date of the run",
example: "2024-05-25T13:15:26.592Z"
}),
parameters: z3.array(z3.unknown()).openapi({
description: "Parameters of the run",
example: []
}),
updatedDate: z3.string().datetime().openapi({
description: "Last update date of the run",
example: "2024-05-25T13:15:30.969Z"
}),
id: commonValidations.id.openapi({
description: "Run identifier",
example: "6651e46e85f83573a821d1f4"
})
});
var RunGetSchema = z3.object({
_id: commonValidations.id,
name: z3.string().min(1).openapi({
description: "Name of the run",
example: "Sample Run"
})
// additional fields here...
});
// src/server/api-docs/openAPIResponseBuilders.ts
init_utils();
// src/server/api-docs/serviceResponse.ts
import { z as z4 } from "zod";
var ServiceResponsePaginationSchema = (dataSchema) => z4.object({
results: z4.array(dataSchema.optional()),
page: z4.number().openapi({ example: 1 }),
limit: z4.number().openapi({ example: 10 }),
totalPages: z4.number().openapi({ example: 2 }),
totalResults: z4.number().openapi({ example: 12 }),
timestamp: z4.number().openapi({ example: 1718035239731968 })
});
// src/server/api-docs/openAPIResponseBuilders.ts
function createApiResponse(schema, description, statusCode = httpStatus_default.OK) {
return {
[statusCode]: {
description,
content: {
"application/json": {
schema
}
}
}
};
}
function createPaginatedApiResponse(schema, description, statusCode = httpStatus_default.OK) {
return {
[statusCode]: {
description,
content: {
"application/json": {
schema: ServiceResponsePaginationSchema(schema)
}
}
}
};
}
// src/server/middlewares/ensureLogin/ensureLoggedIn.ts
init_models();
init_logger();
// src/server/lib/AppSettings/AppSettings.ts
init_models();
// src/seeds/initialAppSettings.json
var initialAppSettings_default = [
{
name: "first_run",
label: "First Run",
description: "Indicates if the application is running the first time",
type: "Boolean",
value: "true",
enabled: true
},
{
name: "authentication",
label: "Authentication",
description: "Enable application authentication",
type: "Boolean",
value: "false",
enabled: true
},
{
name: "auto_remove_old_checks",
label: "Auto remove old checks",
description: "Automatically delete checks older than the configured number of days once per day",
type: "AutoOldChecks",
value: {
days: 365,
lastRunAt: null
},
enabled: false
},
{
name: "auto_remove_old_logs",
label: "Auto remove old logs",
description: "Automatically delete logs older than the configured number of days once per day",
type: "AutoOldLogs",
value: {
days: 120,
lastRunAt: null
},
enabled: true
},
{
name: "share_enabled",
label: "Enable Sharing",
description: "Allow users to share checks via public links",
type: "Boolean",
value: "true",
enabled: true
},
{
name: "sso_enabled",
label: "Enable SSO",
description: "Enable Single Sign-On (SSO) authentication",
type: "Boolean",
value: "false",
enabled: true
},
{
name: "sso_protocol",
label: "SSO Protocol",
description: "Protocol used for SSO (oauth2 or saml)",
type: "String",
value: "oauth2",
enabled: true
},
{
name: "sso_entry_point",
label: "SSO Entry Point",
description: "URL of the Identity Provider (IdP) entry point",
type: "String",
value: "",
enabled: true
},
{
name: "sso_issuer",
label: "SSO Issuer",
description: "Entity ID of the Identity Provider",
type: "String",
value: "",
enabled: true
},
{
name: "sso_client_id",
label: "SSO Client ID",
description: "Client ID for OAuth2",
type: "String",
value: "",
enabled: true
}
];
// src/server/lib/AppSettings/AppSettings.ts
var AppSettings2 = class {
model;
cache;
lastFetch = 0;
TTL = 30 * 1e3;
// 30 seconds
constructor() {
this.model = AppSettings_model_default;
this.cache = null;
}
async init() {
await this.refreshCache();
return this;
}
async refreshCache() {
this.cache = await this.model.find().lean().exec();
this.lastFetch = Date.now();
}
async ensureInitialized() {
if (!this.cache) {
await this.refreshCache();
}
if (Date.now() - this.lastFetch > this.TTL) {
await this.refreshCache();
}
}
async count() {
await this.ensureInitialized();
return this.model.countDocuments().exec();
}
async loadInitialFromFile() {
await this.ensureInitialized();
for (const setting of initialAppSettings_default) {
await this.model.updateOne(
{ name: setting.name },
{ $setOnInsert: setting },
{ upsert: true }
);
}
await this.refreshCache();
}
async get(name) {
await this.ensureInitialized();
return this.cache.find((x) => x.name === name) || this.model.findOne({ name }).exec();
}
async set(name, value) {
await this.ensureInitialized();
const item = await this.model.findOneAndUpdate(
{ name },
{ value },
{ new: true }
).lean().exec();
if (!item) {
throw new Error(`Setting '${name}' not found`);
}
const cachedItem = this.cache.find((x) => x.name === name);
if (cachedItem) {
cachedItem["value"] = item.value;
}
}
async enable(name) {
await this.ensureInitialized();
const item = await this.model.findOneAndUpdate(
{ name },
{ enabled: true },
{ new: true }
).lean().exec();
if (!item) {
throw new Error(`Setting '${name}' not found`);
}
const cachedItem = this.cache.find((x) => x.name === name);
if (cachedItem) {
cachedItem["enabled"] = true;
}
}
async disable(name) {
await this.ensureInitialized();
const item = await this.model.findOneAndUpdate(
{ name },
{ enabled: false },
{ new: true }
).lean().exec();
if (!item) {
throw new Error(`Setting '${name}' not found`);
}
const cachedItem = this.cache.find((x) => x.name === name);
if (cachedItem) {
cachedItem["enabled"] = false;
}
}
async isAuthEnabled() {
await this.ensureInitialized();
const envOverride = process.env.SYNGRISI_AUTH_OVERRIDE;
if (typeof envOverride !== "undefined") {
if (envOverride === "true") {
return true;
}
}
const envAuth = process.env.SYNGRISI_AUTH;
if (envAuth === "true") {
return true;
}
if (envAuth === "false") {
return false;
}
return (await this.get("authentication"))?.value === "true";
}
async isFirstRun() {
await this.ensureInitialized();
return (await this.get("first_run"))?.value === "true";
}
};
var appSettings = new AppSettings2();
// src/server/middlewares/ensureLogin/ensureLoggedIn.ts
init_envConfig();
init_hash();
// src/server/lib/startup/createBasicUsers.ts
init_models();
// src/seeds/guest.json
var guest_default = {
username: "Guest",
role: "user",
firstName: "Syngrisi",
lastName: "Guest",
salt: "861d0193d9a4cd27597d672dd73100b1218cc7629c59bf18f9faef4480d1f58a",
password: "cdd8541c318008fe0eee35d311b428c666d7ddf78722735e40b21b0b9e734eabe1b60f6db6deb8f3f101ed559a7365a497bfa73156ba3fadcbb14c5cae441edfe301568a47320abcda308920fecc482b1082468fd2197893561f5d174ecafdfb930bdc8e070fbed07353231d255616d3072e899a4ae1e6ff71f171edc34ba871d091b657a3207b571d2db0f3fce1fb2bc48942657d35aac7fa1fc2ebcbacdc00e46b184e1e5e4988171bcd843f7bb4adb2920df06c4f6b830154234350f9d65cc6e10d445a242c2149a3ce24458bd741035629bb73bdf29e0db4dcd3e37d85332d58d58674ca56b632294135a3354705b3281ea7564b962d544cfcee7069f3065c394098e11999385b077755f9c7e3d42f55b23ecb3366187d3a8ce2475615fbd06d707020274d7f50fe6c824e589955a667210fa031828f832e3cc050d1c0c5360647bfd88d411d3f76e8c6d9c610e2bc5a22da498888d2c94ad25b6448ca2eb9606210b2018136116fda6fdd87b24bcdfe86daefcdaa97d1f9e3a102188a4314a5a4018599cc70dab82157f5d88f7ada008a1f8e0d6e8abf51ed87ebdab982c01645f4f907414d1bb13b1fac410648cc2e94554ecbe0f472aee26623888878139bd44e109d5ce673ecfbe71269d22bea55a03ad4409fc8578de6001f5601fab4cf800be1d420f76586ed9c31214f30c632de99260256c9d2ce45476c135d32",
apiKey: "4ba2659e94f48ce771a78ee479dc3ab12c2cc6d2484c967b4c564f0a1718a8a254f6aaec3169eb7b29e7d2542d73d378339e311cf912e46fe67eac4eef44636c"
};
// src/server/lib/startup/createBasicUsers.ts
init_logger();
var buildUserInsertPayload = (userData) => ({
username: userData.username,
role: userData.role,
firstName: userData.firstName,
lastName: userData.lastName,
provider: "local",
authSource: "local",
salt: userData.salt,
password: userData.password,
apiKey: userData.apiKey || userData.apikey
});
async function ensureGuestUserExists() {
const guestInsert = buildUserInsertPayload(guest_default);
return User_model_default.updateOne(
{ username: "Guest" },
{ $setOnInsert: guestInsert },
{ upsert: true }
);
}
// src/server/services/share.service.ts
init_hash();
init_models();
init_logger();
init_utils();
// src/server/middlewares/ensureLogin/ensureLoggedIn.ts
var transientGuestUser = {
username: "Guest",
role: "user",
firstName: "Syngrisi",
lastName: "Guest"
};
var handleBasicAuth = async (req, retryCount = 0) => {
const logOpts6 = {
scope: "handleBasicAuth",
msgType: "AUTH_API"
};
const MAX_RETRIES = 10;
const RETRY_DELAY_MS = 500;
const AppSettings3 = appSettings;
const authEnabled = await AppSettings3.isAuthEnabled();
logger_default.debug(`handleBasicAuth: checking auth`, {
...logOpts6,
authEnabled,
isAuthenticated: req.isAuthenticated(),
hasUser: !!req.user,
username: req.user?.username
});
if (req.isAuthenticated()) {
logger_default.debug(`handleBasicAuth: user already authenticated, returning success`, logOpts6);
return { type: "success", status: 200 };
}
if (!authEnabled) {
const guest = await User_model_default.findOne({ username: "Guest" });
if (!guest) {
if (await adminDataJobService.hasActiveDatabaseRestoreJob()) {
logger_default.warn("Guest user is temporarily unavailable during active database restore, using transient guest user", logOpts6);
return {
type: "success",
status: 200,
value: "",
user: transientGuestUser
};
}
await ensureGuestUserExists().catch(() => void 0);
if (retryCount < MAX_RETRIES) {
logger_default.warn(`Guest user not found in handleBasicAuth (attempt ${retryCount + 1}/${MAX_RETRIES}), retrying in ${RETRY_DELAY_MS}ms...`, logOpts6);
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
return handleBasicAuth(req, retryCount + 1);
}
logger_default.error(`cannot find Guest user after ${MAX_RETRIES} retries`, logOpts6);
return {
type: "redirect",
status: 301,
value: `/auth?=Error: cannot find Guest user after ${MAX_RETRIES} retries`,
user: null
};
}
logger_default.debug(`Auth disabled - setting Guest user directly (bypassing session login)`, logOpts6);
return {
type: "success",
status: 200,
value: "",
user: guest
};
}
const result = {
type: "error",
status: 400,
value: "",
user: null
};
if (authEnabled && await AppSettings3.isFirstRun() && !env.SYNGRISI_DISABLE_FIRST_RUN) {
logger_default.info("first run, set admin password", logOpts6);
result.type = "redirect";
result.status = 301;
result.value = "/auth/change?first_run=true";
return result;
}
if (authEnabled) {
logger_default.info(`user is not authenticated, will redirected - ${req.originalUrl}`, logOpts6);
result.type = "redirect";
result.status = 301;
if (req?.originalUrl !== "/") {
result.value = `/auth?origin=${encodeURIComponent(req.originalUrl)}`;
return result;
}
result.value = "/auth";
return result;
}
};
function ensureLoggedIn(options) {
return async (req, res, next) => {
const result = await handleBasicAuth(req);
req.user = result.user || req.user;
if (result.type === "success") {
return next();
}
res.status(result.status).redirect(result.value);
};
}
// src/server/schemas/utils/createRequestQuerySchema.ts
import { z as z5 } from "zod";
var createRequestQuerySchema = (schema) => z5.object({ query: schema });
// src/server/schemas/common/RequestPagination.schema.ts
import { extendZodWithOpenApi as extendZodWithOpenApi4 } from "@asteasolutions/zod-to-openapi";
import { z as z7 } from "zod";
// src/server/schemas/common/requestQueryFilterSchema.schema.ts
import { extendZodWithOpenApi as extendZodWithOpenApi3 } from "@asteasolutions/zod-to-openapi";
import { z as z6 } from "zod";
extendZodWithOpenApi3(z6);
var requestQueryFilterSchema = z6.string().optional().refine((data) => {
if (!data) return false;
try {
const parsed = JSON.parse(data);
const valueSchema = z6.lazy(() => z6.union([
z6.string(),
z6.number(),
z6.boolean(),
z6.array(z6.any()),
z6.record(z6.string(), z6.any())
]));
const schema = z6.record(z6.string(), valueSchema);
schema.parse(parsed);
return true;
} catch (e) {
return false;
}
}, {
message: "Invalid JSON string or does not match the required schema"
}).openapi({ example: '{"key1": "value1", "key2": 123, "key3": true, "$and":[{"name":"CheckName"}]}' });
// src/server/schemas/common/RequestPagination.schema.ts
extendZodWithOpenApi4(z7);
var RequestPaginationSchema = z7.object({
filter: requestQueryFilterSchema.optional(),
limit: commonValidations.positiveNumberString.optional().openapi({ example: "10" }),
page: commonValidations.positiveNumberString.optional().openapi({ example: "1" }),
sortBy: z7.string().optional().openapi({ example: "name:desc" }),
populate: z7.string().optional().openapi({ example: "test" }),
includeUsage: z7.string().optional().openapi({ example: "true" }),
baselineSnapshotId: z7.string().optional().openapi({ example: "656dd9e1a9f9dcd4a0c1beef" })
});
// src/server/schemas/utils/createRequestParamsSchema.ts
import { z as z8 } from "zod";
var createRequestParamsSchema = (schema) => z8.object({ params: schema });
var getByIdParamsSchema = (id2 = "id") => createRequestParamsSchema(
z8.object({ [id2]: commonValidations.id })
);
// src/server/schemas/common/ApiError.schema.ts
import { extendZodWithOpenApi as extendZodWithOpenApi5 } from "@asteasolutions/zod-to-openapi";
import { z as z9 } from "zod";
extendZodWithOpenApi5(z9);
var ApiErrorSchema = z9.object({
name: z9.string().openapi({
description: "Name of the error type",
example: "Error"
}),
message: z9.string().openapi({
description: "Detailed message describing the error",
example: "cannot remove run with id: '6651e46e85f83573a821d1f4', not found"
}),
status: z9.number().openapi({
description: "HTTP status code that corresponds to the error",
example: 404
}),
stacktrace: z9.string().openapi({
description: "Stack trace of the error for debugging purposes",
example: "Error: cannot remove run with id: '6651e46e85f83573a821d1f4', not found\\n at Object.remove2 (/Users/exadel/Projects/SYNGRISI/packages/syngrisi/src/server/services/run.service.ts:27:15)\\n at processTicksAndRejections (node:internal/process/task_queues:95:5)\\n at /Users/exadel/Projects/SYNGRISI/packages/syngrisi/src/server/controllers/runs.controller.ts:25:20"
})
});
// src/server/routes/v1/runs.route.ts
init_utils();
var registry = new OpenAPIRegistry();
var router = express.Router();
registry.registerPath({
method: "get",
path: "/v1/runs",
summary: "List of runs with pagination, and optional filtering and sorting.",
tags: ["Runs"],
request: { query: RequestPaginationSchema },
responses: createPaginatedApiResponse(RunResponseSchema, "Success")
});
router.get(
"/",
ensureLoggedIn(),
validateRequest(createRequestQuerySchema(RequestPaginationSchema), "get, /v1/runs"),
get2
);
registry.registerPath({
method: "delete",
path: "/v1/runs/{id}",
summary: "Remove a run by ID",
description: "Remove a run by ID",
tags: ["Runs"],
request: commonValidations.paramsId,
responses: {
...createApiResponse(RunResponseSchema, "Success"),
...createApiResponse(ApiErrorSchema, "ApiError", httpStatus_default.NOT_FOUND)
}
});
router.delete(
"/:id",
ensureLoggedIn(),
validateRequest(getByIdParamsSchema(), "delete, /v1/runs/{id}"),
remove5
);
var runs_route_default = router;
export {
runs_route_default as default,
registry
};
//# sourceMappingURL=runs.route.js.map