@syngrisi/syngrisi
Version:
Syngrisi - Visual Testing Tool
4,692 lines • 135 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 init_ident = __esm({
"src/server/utils/ident.ts"() {
"use strict";
}
});
// src/server/utils/buildIdentObject.ts
var init_buildIdentObject = __esm({
"src/server/utils/buildIdentObject.ts"() {
"use strict";
init_ident();
}
});
// 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, path4, index) => {
if (index === path4.length - 1) {
delete obj[path4[index]];
return;
}
deleteAtPath(obj[path4[index]], path4, 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((path4) => {
if (schema.paths[path4].options && schema.paths[path4].options.private) {
deleteAtPath(ret, path4.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;
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);
}
});
// 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;
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);
}
});
// src/server/models/Run.model.ts
import mongoose8, { Schema as Schema7 } from "mongoose";
var RunSchema, Run;
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);
}
});
// 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;
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);
}
});
// src/server/models/Webhook.model.ts
import mongoose12, { Schema as Schema11 } from "mongoose";
var WebhookSchema, Webhook;
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);
}
});
// 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;
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);
}
});
// 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 init_calculateAcceptedStatus = __esm({
"src/server/utils/calculateAcceptedStatus.ts"() {
"use strict";
init_models();
}
});
// 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";
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 logOpts, HookRegistry, hookRegistry;
var init_HookRegistry = __esm({
"src/server/plugins/core/HookRegistry.ts"() {
"use strict";
init_logger();
logOpts = {
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})`, logOpts);
}
/**
* 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}'`, logOpts);
}
}
}
/**
* 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}'`, logOpts);
const result = await hook.handler(
req,
res,
context
);
if (result !== null) {
logger_default.debug(`Auth hook '${hook.pluginName}' returned result: ${result.authenticated}`, logOpts);
return result;
}
} catch (error) {
logger_default.error(`Error in auth:validate hook from '${hook.pluginName}': ${error}`, logOpts);
}
}
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}'`, logOpts);
const result = await hook.handler(
currentContext,
pluginContext
);
if ("skip" in result && result.skip) {
logger_default.info(`Check comparison skipped by plugin '${hook.pluginName}'`, logOpts);
return result;
}
currentContext = result;
} catch (error) {
logger_default.error(`Error in check:beforeCompare hook from '${hook.pluginName}': ${error}`, logOpts);
}
}
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}'`, logOpts);
currentResult = await hook.handler(
context,
currentResult,
pluginContext
);
} catch (error) {
logger_default.error(`Error in check:afterCompare hook from '${hook.pluginName}': ${error}`, logOpts);
}
}
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", logOpts);
}
};
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 logOpts2, PluginManager, pluginManager;
var init_PluginManager = __esm({
"src/server/plugins/core/PluginManager.ts"() {
"use strict";
init_logger();
init_context();
init_HookRegistry();
logOpts2 = {
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", logOpts2);
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`, logOpts2);
return;
}
if (manifest.enabled === false) {
logger_default.info(`Plugin '${pluginName}' is disabled, skipping`, logOpts2);
return;
}
logger_default.info(`Loading plugin: ${pluginName} v${manifest.version}`, logOpts2);
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`, logOpts2);
} 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}`, logOpts2);
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`, logOpts2);
return;
}
logger_default.info(`Unloading plugin: ${pluginName}`, logOpts2);
try {
if (loadedPlugin.plugin.onUnload) {
await loadedPlugin.plugin.onUnload();
}
this.registry.unregister(pluginName);
this.plugins.delete(pluginName);
logger_default.info(`Plugin '${pluginName}' unloaded successfully`, logOpts2);
} catch (error) {
logger_default.error(`Error unloading plugin '${pluginName}': ${error}`, logOpts2);
throw error;
}
}
/**
* Unload all plugins
*/
async unloadAll() {
logger_default.info("Unloading all plugins", logOpts2);
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}`, logOpts2);
}
}
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/snapshots.route.ts
import express from "express";
import { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi";
// src/server/controllers/snapshots.controller.ts
init_utils();
// src/server/services/run.service.ts
init_models();
init_logger();
// src/server/services/test.service.ts
import { Types as Types2 } from "mongoose";
init_models();
init_logger();
// src/server/services/run.service.ts
init_utils();
init_utils();
// 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 logOpts4 = {
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)}'`, logOpts4);
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`, logOpts4);
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 { Types as Types3 } from "mongoose";
// src/server/utils/domDumpUtils.ts
import { gunzipSync, gzipSync } from "zlib";
import { createHash as createHash2 } from "crypto";
// src/server/services/dom-snapshot.service.ts
init_logger();
// 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();
// 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";
// 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
init_models();
init_utils();
import { Types as Types5 } from "mongoose";
// src/server/services/snapshot.service.ts
init_config();
init_models();
init_logger();
// src/server/services/check.service.ts
init_logger();
// src/server/services/webhook.service.ts
init_models();
init_logger();
// src/server/services/comparison.service.ts
init_models();
init_config();
// src/server/lib/comparison/comparator.ts
init_utils();
init_logger();
import { Worker } from "worker_threads";
import fs2 from "fs";
import path2 from "path";
import { fileURLToPath } from "url";
import { createRequire } from "module";
var __filename = fileURLToPath(import.meta.url);
var __dirname = path2.dirname(__filename);
// 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/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 fs3 from "fs";
import { promises as fsp } from "fs";
import path3 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) => path3.join(config.adminDataJobsPath, jobId);
var getJobMetaPath = (jobId) => path3.join(getJobDir(jobId), META_FILENAME);
var getJobLogPath = (jobId) => path3.join(getJobDir(jobId), LOG_FILENAME);
async function ensureDir(dirPath) {
await fsp.mkdir(dirPath, { recursive: true });
}
async function removeDirSafe(dirPath) {
if (!dirPath) return;
await fsp.rm(dirPath, { recursive: true, force: true });
}
async function fileExists(filePath) {
try {
await fsp.access(filePath);
return true;
} catch {
return false;
}
}
async function writeJob(job) {
await ensureDir(job.workDir);
await fsp.writeFile(getJobMetaPath(job.id), JSON.stringify(job, null, 2));
}
async function appendLog(jobId, message) {
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
`;
await fsp.appendFile(getJobLogPath(jobId), line);
}
async function readJob(jobId) {
try {
const raw = await fsp.readFile(getJobMetaPath(jobId), "utf8");
return JSON.parse(raw);
} catch {
return null;
}
}
async function listJobsInternal() {
await ensureDir(config.adminDataJobsPath);
const entries = await fsp.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 fsp.opendir(currentDir);
for await (const entry of dir) {
const entryPath = path3.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(path3.dirname(targetPath));
await pipelineAsync(fs3.createReadStream(sourcePath), fs3.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 fsp.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();
});
fs3.createReadStream(filePath).on("error", reject).pipe(entry).on("error", reject);
});
}
async function createTarGzArchive(outputPath, items) {
await ensureDir(path3.dirname(outputPath));
const pack = tar.pack();
const gzip = createGzip();
const output = fs3.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 = path3.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(path3.dirname(outputPath)).then(() => pipelineAsync(stream, fs3.createWriteStream(outputPath))).then(() => finishEntry()).catch((error) => finishEntry(error));
});
extract.on("finish", () => resolve());
extract.on("error", reject);
fs3.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);
fs3.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(path3.dirname(outputPath));
const gzip = createGzip();
const output = fs3.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 fsp.opendir(currentDir);
for await (const entry of dir) {
const fullPath = path3.join(currentDir, entry.name);
const relativePath = path3.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 = path3.join(job.workDir, archiveName);
const exportDir = path3.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 = path3.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 = path3.join(exportDir, "manifest.json");
await fsp.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: path3.join(exportDir, collection.dumpFile),
name: `collections/${collection.dumpFile}`
}))
];
await createTarGzArchive(archivePath, tarItems);
const stat = await fsp.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 = fs3.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 = path3.join(job.workDir, "extracted-db");
await updateProgress(job.id, { stage: "extracting", percent: 10 }, "Extracting database archive");
await extractTarGzArchive(uploadPath, extractDir);
const manifestPath = path3.join(extractDir, "manifest.json");
if (!await fileExists(manifestPath)) {
throw new Error("manifest.json is missing from database archive");
}
const manifest = JSON.parse(await fsp.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 = path3.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 = path3.join(job.workDir, archiveName);
const totalFiles = await countFilesRecursive(config.defaultImagesPath);
let processedFiles = 0;
await ensureDir(path3.dirname(archivePath));
const pack = tar.pack();
const gzip = createGzip();
const output = fs3.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 fsp.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 = path3.join(config.defaultImagesPath, relativePath);
void (async () => {
assertNotCancelled(job.id);
const exists = await fileExists(targetPath);
if (exists && skipExisting) {
skippedFiles += 1;
stream.resume();
} else {
await ensureDir(path3.dirname(targetPath));
if (exists) {
await fsp.rm(targetPath, { force: true });
}
await pipelineAsync(stream, fs3.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);
fs3.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(path3.join(job.workDir, "staging"));
await removeDirSafe(path3.join(job.workDir, "extracted"));
await removeDirSafe(path3.join(job.workDir, "extracted-db"));
await removeDirSafe(path3.join(job.workDir, DB_EXPORT_DIRNAME));
if (job.status === "completed" && job.downloadAvailable) {
if (job.uploadPath) {
await fsp.rm(job.uploadPath, { force: true });
}
return;
}
if (job.uploadPath) {
await fsp.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 = path3.join(jobDir, fileName);
await ensureDir(jobDir);
if (file.tempFilePath) {
await fsp.rename(file.tempFilePath, targetPath).catch(async () => {
await streamToFile(file.tempFilePath, targetPath);
await fsp.rm(file.tempFilePath, { force: true });
});
} else {
await fsp.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, path3.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, path3.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 fsp.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 || path3.basename(job.archivePath),
stream: fs3.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/snapshots.controller.ts
init_utils();
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("VRSSnapshot", filter, options);
res.send(result);
});
// 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 logOpts4 = {
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`, {
...logOpts4,
authEnabled,
isAuthenticated: req.isAuthenticated(),
hasUser: !!req.user,
username: req.user?.username
});
if (req.isAuthenticated()) {
logger_default.debug(`handleBasicAuth: user already authenticated, returning success`, logOpts4);
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", logOpts4);
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...`, logOpts4);
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`, logOpts4);
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)`, logOpts4);
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", logOpts4);
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}`, logOpts4);
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/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 logOpts3 = {
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, path4) {
let currentValue = request;
path4.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)}`, logOpts3);
}
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, logOpts3);
res.status(statusCode).send(new ServiceResponse(1 /* Failed */, errorMessage, null, statusCode));
} else {
logger_default.error(`Unexpected error: ${errMsg(err)}`, logOpts3);
next(err);
}
}
};
// src/server/schemas/Snapshots.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/Snapshots.schema.ts
var SnapshotSchema2 = z3.object({
_id: commonValidations.id,
name: z3.string().min(1).openapi({
description: "Name of the snapshot",
example: "Login page"
}),
filename: z3.string().min(1).openapi({
description: "Filename of the snapshot",
example: "6651dd4f7c9186e315910b24.png"
}),
imghash: z3.string().min(1).openapi({
description: "Image hash of the snapshot",
example: "96e8359554f12142bc19e44288295aa67a59cd128e242b6756651bf8e3d9f34caa7f587367ca8e5cdcfbaaf180adfd8825250fc7485784c41de11a9c08c1f9ab"
}),
createdDate: z3.string().datetime().openapi({
description: "Creation date of the snapshot",
example: "2024-05-25T13:15:30.946Z"
}),
id: commonValidations.id.openapi({
description: "Snapshot identifier",
example: "6651e47285f83573a821d20e"
}),
stabMethod: z3.string().optional(),
vOffset: z3.number().optional()
});
var SnapshotsResponseSchema = z3.array(
SnapshotSchema2
);
// 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 createPaginatedApiResponse(schema, description, statusCode = httpStatus_default.OK) {
return {
[statusCode]: {
description,
content: {
"application/json": {
schema: ServiceResponsePaginationSchema(schema)
}
}
}
};
}
// 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/routes/v1/snapshots.route.ts
var registry = new OpenAPIRegistry();
var router = express.Router();
registry.registerPath({
method: "get",
path: "/v1/snapshots",
summary: "List of snapshots with pagination, and optional filtering and sorting.",
tags: ["Snapshots"],
request: { query: RequestPaginationSchema },
responses: createPaginatedApiResponse(SnapshotsResponseSchema, "Success")
});
router.get(
"/",
ensureLoggedIn(),
validateRequest(createRequestQuerySchema(RequestPaginationSchema), "get, /v1/snapshots"),
get2
);
var snapshots_route_default = router;
export {
snapshots_route_default as default,
registry
};
//# sourceMappingURL=snapshots.route.js.map