@oneuptime/common 10.0.11 → 10.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/Models/AnalyticsModels/ExceptionInstance.ts +98 -0
  2. package/Models/DatabaseModels/TelemetryException.ts +105 -0
  3. package/Models/DatabaseModels/User.ts +27 -0
  4. package/Server/API/UserWebAuthnAPI.ts +0 -2
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1772111896988-MigrationName.ts +41 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1772280000000-MigrationName.ts +23 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  8. package/Server/Services/MonitorTestService.ts +102 -1
  9. package/Server/Services/UserWebAuthnService.ts +116 -8
  10. package/Server/Utils/Browser.ts +0 -1
  11. package/Server/Utils/Memory.ts +81 -0
  12. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +4 -1
  13. package/Server/Utils/Monitor/Criteria/IncomingEmailCriteria.ts +5 -1
  14. package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +3 -15
  15. package/Server/Utils/Telemetry.ts +7 -2
  16. package/Server/Utils/VM/VMAPI.ts +204 -0
  17. package/Tests/Server/Utils/VM/VMAPI.test.ts +314 -0
  18. package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +2 -2
  19. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +136 -11
  20. package/UI/Components/TextArea/TextArea.tsx +2 -1
  21. package/Utils/Number.ts +27 -0
  22. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +87 -0
  23. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
  24. package/build/dist/Models/DatabaseModels/TelemetryException.js +108 -0
  25. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  26. package/build/dist/Models/DatabaseModels/User.js +31 -0
  27. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  28. package/build/dist/Server/API/UserWebAuthnAPI.js +0 -2
  29. package/build/dist/Server/API/UserWebAuthnAPI.js.map +1 -1
  30. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1772111896988-MigrationName.js +20 -0
  31. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1772111896988-MigrationName.js.map +1 -0
  32. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1772280000000-MigrationName.js +14 -0
  33. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1772280000000-MigrationName.js.map +1 -0
  34. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  35. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  36. package/build/dist/Server/Services/MonitorTestService.js +74 -0
  37. package/build/dist/Server/Services/MonitorTestService.js.map +1 -1
  38. package/build/dist/Server/Services/UserWebAuthnService.js +83 -5
  39. package/build/dist/Server/Services/UserWebAuthnService.js.map +1 -1
  40. package/build/dist/Server/Utils/Browser.js +0 -1
  41. package/build/dist/Server/Utils/Browser.js.map +1 -1
  42. package/build/dist/Server/Utils/Memory.js +55 -0
  43. package/build/dist/Server/Utils/Memory.js.map +1 -0
  44. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +3 -1
  45. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  46. package/build/dist/Server/Utils/Monitor/Criteria/IncomingEmailCriteria.js +4 -1
  47. package/build/dist/Server/Utils/Monitor/Criteria/IncomingEmailCriteria.js.map +1 -1
  48. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +3 -9
  49. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
  50. package/build/dist/Server/Utils/Telemetry.js +4 -2
  51. package/build/dist/Server/Utils/Telemetry.js.map +1 -1
  52. package/build/dist/Server/Utils/VM/VMAPI.js +153 -0
  53. package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
  54. package/build/dist/Tests/Server/Utils/VM/VMAPI.test.js +218 -0
  55. package/build/dist/Tests/Server/Utils/VM/VMAPI.test.js.map +1 -0
  56. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +2 -2
  57. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js.map +1 -1
  58. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +72 -5
  59. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  60. package/build/dist/UI/Components/TextArea/TextArea.js +2 -2
  61. package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
  62. package/build/dist/Utils/Number.js +16 -0
  63. package/build/dist/Utils/Number.js.map +1 -1
  64. package/package.json +1 -1
@@ -311,6 +311,77 @@ export default class ExceptionInstance extends AnalyticsBaseModel {
311
311
  },
312
312
  });
313
313
 
314
+ const releaseColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
315
+ key: "release",
316
+ title: "Release",
317
+ description:
318
+ "Service version / release from service.version resource attribute",
319
+ required: false,
320
+ type: TableColumnType.Text,
321
+ accessControl: {
322
+ read: [
323
+ Permission.ProjectOwner,
324
+ Permission.ProjectAdmin,
325
+ Permission.ProjectMember,
326
+ Permission.ReadTelemetryException,
327
+ ],
328
+ create: [
329
+ Permission.ProjectOwner,
330
+ Permission.ProjectAdmin,
331
+ Permission.ProjectMember,
332
+ Permission.CreateTelemetryException,
333
+ ],
334
+ update: [],
335
+ },
336
+ });
337
+
338
+ const environmentColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
339
+ key: "environment",
340
+ title: "Environment",
341
+ description:
342
+ "Deployment environment from deployment.environment resource attribute",
343
+ required: false,
344
+ type: TableColumnType.Text,
345
+ accessControl: {
346
+ read: [
347
+ Permission.ProjectOwner,
348
+ Permission.ProjectAdmin,
349
+ Permission.ProjectMember,
350
+ Permission.ReadTelemetryException,
351
+ ],
352
+ create: [
353
+ Permission.ProjectOwner,
354
+ Permission.ProjectAdmin,
355
+ Permission.ProjectMember,
356
+ Permission.CreateTelemetryException,
357
+ ],
358
+ update: [],
359
+ },
360
+ });
361
+
362
+ const parsedFramesColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
363
+ key: "parsedFrames",
364
+ title: "Parsed Stack Frames",
365
+ description: "Stack trace parsed into structured frames (JSON array)",
366
+ required: false,
367
+ type: TableColumnType.Text,
368
+ accessControl: {
369
+ read: [
370
+ Permission.ProjectOwner,
371
+ Permission.ProjectAdmin,
372
+ Permission.ProjectMember,
373
+ Permission.ReadTelemetryException,
374
+ ],
375
+ create: [
376
+ Permission.ProjectOwner,
377
+ Permission.ProjectAdmin,
378
+ Permission.ProjectMember,
379
+ Permission.CreateTelemetryException,
380
+ ],
381
+ update: [],
382
+ },
383
+ });
384
+
314
385
  const attributesColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
315
386
  key: "attributes",
316
387
  title: "Attributes",
@@ -384,6 +455,9 @@ export default class ExceptionInstance extends AnalyticsBaseModel {
384
455
  spanIdColumn,
385
456
  fingerprintColumn,
386
457
  spanNameColumn,
458
+ releaseColumn,
459
+ environmentColumn,
460
+ parsedFramesColumn,
387
461
  attributesColumn,
388
462
  ],
389
463
  projections: [],
@@ -504,4 +578,28 @@ export default class ExceptionInstance extends AnalyticsBaseModel {
504
578
  public set spanName(v: string | undefined) {
505
579
  this.setColumnValue("spanName", v);
506
580
  }
581
+
582
+ public get release(): string | undefined {
583
+ return this.getColumnValue("release") as string | undefined;
584
+ }
585
+
586
+ public set release(v: string | undefined) {
587
+ this.setColumnValue("release", v);
588
+ }
589
+
590
+ public get environment(): string | undefined {
591
+ return this.getColumnValue("environment") as string | undefined;
592
+ }
593
+
594
+ public set environment(v: string | undefined) {
595
+ this.setColumnValue("environment", v);
596
+ }
597
+
598
+ public get parsedFrames(): string | undefined {
599
+ return this.getColumnValue("parsedFrames") as string | undefined;
600
+ }
601
+
602
+ public set parsedFrames(v: string | undefined) {
603
+ this.setColumnValue("parsedFrames", v);
604
+ }
507
605
  }
@@ -1026,4 +1026,109 @@ export default class TelemetryException extends DatabaseBaseModel {
1026
1026
  default: 1,
1027
1027
  })
1028
1028
  public occuranceCount?: number = undefined;
1029
+
1030
+ @ColumnAccessControl({
1031
+ create: [
1032
+ Permission.ProjectOwner,
1033
+ Permission.ProjectAdmin,
1034
+ Permission.CreateTelemetryException,
1035
+ ],
1036
+ read: [
1037
+ Permission.ProjectOwner,
1038
+ Permission.ProjectAdmin,
1039
+ Permission.ProjectMember,
1040
+ Permission.ReadTelemetryException,
1041
+ Permission.ReadAllProjectResources,
1042
+ ],
1043
+ update: [
1044
+ Permission.ProjectOwner,
1045
+ Permission.ProjectAdmin,
1046
+ Permission.EditTelemetryException,
1047
+ ],
1048
+ })
1049
+ @TableColumn({
1050
+ required: false,
1051
+ type: TableColumnType.LongText,
1052
+ canReadOnRelationQuery: true,
1053
+ title: "First Seen In Release",
1054
+ description:
1055
+ "The service version / release in which this exception was first observed",
1056
+ example: "v1.2.3",
1057
+ })
1058
+ @Column({
1059
+ nullable: true,
1060
+ type: ColumnType.LongText,
1061
+ length: ColumnLength.LongText,
1062
+ })
1063
+ public firstSeenInRelease?: string = undefined;
1064
+
1065
+ @ColumnAccessControl({
1066
+ create: [
1067
+ Permission.ProjectOwner,
1068
+ Permission.ProjectAdmin,
1069
+ Permission.CreateTelemetryException,
1070
+ ],
1071
+ read: [
1072
+ Permission.ProjectOwner,
1073
+ Permission.ProjectAdmin,
1074
+ Permission.ProjectMember,
1075
+ Permission.ReadTelemetryException,
1076
+ Permission.ReadAllProjectResources,
1077
+ ],
1078
+ update: [
1079
+ Permission.ProjectOwner,
1080
+ Permission.ProjectAdmin,
1081
+ Permission.EditTelemetryException,
1082
+ ],
1083
+ })
1084
+ @TableColumn({
1085
+ required: false,
1086
+ type: TableColumnType.LongText,
1087
+ canReadOnRelationQuery: true,
1088
+ title: "Last Seen In Release",
1089
+ description:
1090
+ "The most recent service version / release in which this exception was observed",
1091
+ example: "v1.4.0",
1092
+ })
1093
+ @Column({
1094
+ nullable: true,
1095
+ type: ColumnType.LongText,
1096
+ length: ColumnLength.LongText,
1097
+ })
1098
+ public lastSeenInRelease?: string = undefined;
1099
+
1100
+ @ColumnAccessControl({
1101
+ create: [
1102
+ Permission.ProjectOwner,
1103
+ Permission.ProjectAdmin,
1104
+ Permission.CreateTelemetryException,
1105
+ ],
1106
+ read: [
1107
+ Permission.ProjectOwner,
1108
+ Permission.ProjectAdmin,
1109
+ Permission.ProjectMember,
1110
+ Permission.ReadTelemetryException,
1111
+ Permission.ReadAllProjectResources,
1112
+ ],
1113
+ update: [
1114
+ Permission.ProjectOwner,
1115
+ Permission.ProjectAdmin,
1116
+ Permission.EditTelemetryException,
1117
+ ],
1118
+ })
1119
+ @TableColumn({
1120
+ required: false,
1121
+ type: TableColumnType.LongText,
1122
+ canReadOnRelationQuery: true,
1123
+ title: "Environment",
1124
+ description:
1125
+ "Deployment environment from deployment.environment resource attribute",
1126
+ example: "production",
1127
+ })
1128
+ @Column({
1129
+ nullable: true,
1130
+ type: ColumnType.LongText,
1131
+ length: ColumnLength.LongText,
1132
+ })
1133
+ public environment?: string = undefined;
1029
1134
  }
@@ -351,6 +351,33 @@ class User extends UserModel {
351
351
  })
352
352
  public resetPasswordExpires?: Date = undefined;
353
353
 
354
+ @ColumnAccessControl({
355
+ create: [],
356
+ read: [],
357
+ update: [],
358
+ })
359
+ @TableColumn({ type: TableColumnType.ShortText })
360
+ @Column({
361
+ type: ColumnType.ShortText,
362
+ length: ColumnLength.ShortText,
363
+ nullable: true,
364
+ unique: false,
365
+ })
366
+ public webauthnChallenge?: string = undefined;
367
+
368
+ @ColumnAccessControl({
369
+ create: [],
370
+ read: [],
371
+ update: [],
372
+ })
373
+ @TableColumn({ type: TableColumnType.Date })
374
+ @Column({
375
+ type: ColumnType.Date,
376
+ nullable: true,
377
+ unique: false,
378
+ })
379
+ public webauthnChallengeExpiresAt?: Date = undefined;
380
+
354
381
  @ColumnAccessControl({
355
382
  create: [],
356
383
  read: [Permission.CurrentUser],
@@ -54,12 +54,10 @@ export default class UserWebAuthnAPI extends BaseAPI<
54
54
  const databaseProps: DatabaseCommonInteractionProps =
55
55
  await CommonAPI.getDatabaseCommonInteractionProps(req);
56
56
 
57
- const expectedChallenge: string = data["challenge"] as string;
58
57
  const credential: any = data["credential"];
59
58
  const name: string = data["name"] as string;
60
59
 
61
60
  await UserWebAuthnService.verifyRegistration({
62
- challenge: expectedChallenge,
63
61
  credential: credential,
64
62
  name: name,
65
63
  props: databaseProps,
@@ -0,0 +1,41 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class MigrationName1772111896988 implements MigrationInterface {
4
+ public name = "MigrationName1772111896988";
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(
8
+ `ALTER TABLE "TelemetryException" ADD "firstSeenInRelease" character varying(500)`,
9
+ );
10
+ await queryRunner.query(
11
+ `ALTER TABLE "TelemetryException" ADD "lastSeenInRelease" character varying(500)`,
12
+ );
13
+ await queryRunner.query(
14
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
15
+ );
16
+ await queryRunner.query(
17
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
18
+ );
19
+ await queryRunner.query(
20
+ `ALTER TABLE "TelemetryException" ADD "environment" character varying(500)`,
21
+ );
22
+ }
23
+
24
+ public async down(queryRunner: QueryRunner): Promise<void> {
25
+ await queryRunner.query(
26
+ `ALTER TABLE "TelemetryException" DROP COLUMN "environment"`,
27
+ );
28
+ await queryRunner.query(
29
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
30
+ );
31
+ await queryRunner.query(
32
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
33
+ );
34
+ await queryRunner.query(
35
+ `ALTER TABLE "TelemetryException" DROP COLUMN "lastSeenInRelease"`,
36
+ );
37
+ await queryRunner.query(
38
+ `ALTER TABLE "TelemetryException" DROP COLUMN "firstSeenInRelease"`,
39
+ );
40
+ }
41
+ }
@@ -0,0 +1,23 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class MigrationName1772280000000 implements MigrationInterface {
4
+ public name = "MigrationName1772280000000";
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(
8
+ `ALTER TABLE "User" ADD "webauthnChallenge" character varying(100)`,
9
+ );
10
+ await queryRunner.query(
11
+ `ALTER TABLE "User" ADD "webauthnChallengeExpiresAt" TIMESTAMP WITH TIME ZONE`,
12
+ );
13
+ }
14
+
15
+ public async down(queryRunner: QueryRunner): Promise<void> {
16
+ await queryRunner.query(
17
+ `ALTER TABLE "User" DROP COLUMN "webauthnChallengeExpiresAt"`,
18
+ );
19
+ await queryRunner.query(
20
+ `ALTER TABLE "User" DROP COLUMN "webauthnChallenge"`,
21
+ );
22
+ }
23
+ }
@@ -259,6 +259,8 @@ import { MigrationName1770732721195 } from "./1770732721195-MigrationName";
259
259
  import { MigrationName1770833704656 } from "./1770833704656-MigrationName";
260
260
  import { MigrationName1770834237090 } from "./1770834237090-MigrationName";
261
261
  import { MigrationName1770834237091 } from "./1770834237091-MigrationName";
262
+ import { MigrationName1772111896988 } from "./1772111896988-MigrationName";
263
+ import { MigrationName1772280000000 } from "./1772280000000-MigrationName";
262
264
 
263
265
  export default [
264
266
  InitialMigration,
@@ -522,4 +524,6 @@ export default [
522
524
  MigrationName1770833704656,
523
525
  MigrationName1770834237090,
524
526
  MigrationName1770834237091,
527
+ MigrationName1772111896988,
528
+ MigrationName1772280000000,
525
529
  ];
@@ -1,11 +1,112 @@
1
- import DatabaseService from "./DatabaseService";
1
+ import DatabaseService, { EntityManager } from "./DatabaseService";
2
2
  import MonitorTest from "../../Models/DatabaseModels/MonitorTest";
3
+ import ObjectID from "../../Types/ObjectID";
4
+ import OneUptimeDate from "../../Types/Date";
5
+ import { MonitorStepProbeResponse } from "../../Models/DatabaseModels/MonitorProbe";
3
6
 
4
7
  export class Service extends DatabaseService<MonitorTest> {
8
+ private static readonly STALE_TEST_CLAIM_TIMEOUT_IN_MINUTES: number = 10;
9
+
5
10
  public constructor() {
6
11
  super(MonitorTest);
7
12
  this.hardDeleteItemsOlderThanInDays("createdAt", 2); // this is temporary data. Clear it after 2 days.
8
13
  }
14
+
15
+ /**
16
+ * Atomically claims monitor tests for a specific probe instance using
17
+ * FOR UPDATE SKIP LOCKED so concurrent probe replicas do not execute the
18
+ * same monitor test.
19
+ */
20
+ public async claimMonitorTestsForProbing(data: {
21
+ probeId: ObjectID;
22
+ limit: number;
23
+ }): Promise<Array<ObjectID>> {
24
+ const staleClaimThreshold: Date = OneUptimeDate.addRemoveMinutes(
25
+ OneUptimeDate.getCurrentDate(),
26
+ -Service.STALE_TEST_CLAIM_TIMEOUT_IN_MINUTES,
27
+ );
28
+
29
+ return await this.executeTransaction(
30
+ async (transactionalEntityManager: EntityManager) => {
31
+ const selectQuery: string = `
32
+ SELECT mt."_id"
33
+ FROM "MonitorTest" mt
34
+ WHERE mt."probeId" = $1
35
+ AND (
36
+ mt."isInQueue" = true
37
+ OR (
38
+ mt."isInQueue" = false
39
+ AND mt."updatedAt" <= $3
40
+ )
41
+ )
42
+ AND mt."monitorStepProbeResponse" IS NULL
43
+ AND mt."deletedAt" IS NULL
44
+ ORDER BY mt."createdAt" ASC
45
+ LIMIT $2
46
+ FOR UPDATE OF mt SKIP LOCKED
47
+ `;
48
+
49
+ const selectedRows: Array<{ _id: string }> =
50
+ await transactionalEntityManager.query(selectQuery, [
51
+ data.probeId.toString(),
52
+ data.limit,
53
+ staleClaimThreshold,
54
+ ]);
55
+
56
+ if (selectedRows.length === 0) {
57
+ return [];
58
+ }
59
+
60
+ const ids: Array<string> = selectedRows.map((row: { _id: string }) => {
61
+ return row._id;
62
+ });
63
+
64
+ const updateQuery: string = `
65
+ UPDATE "MonitorTest"
66
+ SET "isInQueue" = false,
67
+ "updatedAt" = now()
68
+ WHERE "_id" = ANY($1::uuid[])
69
+ `;
70
+
71
+ await transactionalEntityManager.query(updateQuery, [ids]);
72
+
73
+ return ids.map((id: string) => {
74
+ return new ObjectID(id);
75
+ });
76
+ },
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Merge a single step response into monitorStepProbeResponse atomically.
82
+ * This avoids step-response overwrite races when multiple step jobs are
83
+ * processed concurrently.
84
+ */
85
+ public async mergeStepProbeResponse(data: {
86
+ testId: ObjectID;
87
+ monitorStepProbeResponse: MonitorStepProbeResponse;
88
+ }): Promise<void> {
89
+ const testedAt: Date = OneUptimeDate.getCurrentDate();
90
+
91
+ await this.executeTransaction(
92
+ async (transactionalEntityManager: EntityManager) => {
93
+ const updateQuery: string = `
94
+ UPDATE "MonitorTest"
95
+ SET "monitorStepProbeResponse" = COALESCE("monitorStepProbeResponse", '{}'::jsonb) || $2::jsonb,
96
+ "testedAt" = $3,
97
+ "updatedAt" = now()
98
+ WHERE "_id" = $1
99
+ AND "deletedAt" IS NULL
100
+ `;
101
+
102
+ await transactionalEntityManager.query(updateQuery, [
103
+ data.testId.toString(),
104
+ JSON.stringify(data.monitorStepProbeResponse),
105
+ testedAt,
106
+ ]);
107
+ },
108
+ );
109
+ }
9
110
  }
10
111
 
11
112
  export default new Service();
@@ -19,6 +19,9 @@ import ObjectID from "../../Types/ObjectID";
19
19
  import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
20
20
  import UserTotpAuth from "../../Models/DatabaseModels/UserTotpAuth";
21
21
  import UserTotpAuthService from "./UserTotpAuthService";
22
+ import OneUptimeDate from "../../Types/Date";
23
+
24
+ const WEBAUTHN_CHALLENGE_TTL_MINUTES: number = 5;
22
25
 
23
26
  export class Service extends DatabaseService<Model> {
24
27
  public constructor() {
@@ -102,6 +105,21 @@ export class Service extends DatabaseService<Model> {
102
105
  );
103
106
  }
104
107
 
108
+ // Store the challenge server-side so verification uses a trusted value
109
+ await UserService.updateOneById({
110
+ id: data.userId,
111
+ data: {
112
+ webauthnChallenge: options.challenge,
113
+ webauthnChallengeExpiresAt: OneUptimeDate.addRemoveMinutes(
114
+ OneUptimeDate.getCurrentDate(),
115
+ WEBAUTHN_CHALLENGE_TTL_MINUTES,
116
+ ),
117
+ },
118
+ props: {
119
+ isRoot: true,
120
+ },
121
+ });
122
+
105
123
  return {
106
124
  options: options as any,
107
125
  challenge: options.challenge,
@@ -110,16 +128,24 @@ export class Service extends DatabaseService<Model> {
110
128
 
111
129
  @CaptureSpan()
112
130
  public async verifyRegistration(data: {
113
- challenge: string;
114
131
  credential: any;
115
132
  name: string;
116
133
  props: DatabaseCommonInteractionProps;
117
134
  }): Promise<void> {
135
+ if (!data.props.userId) {
136
+ throw new BadDataException("User ID not found in request");
137
+ }
138
+
139
+ // Retrieve the challenge from the server-side store
140
+ const storedChallenge: string = await this.getAndClearStoredChallenge(
141
+ data.props.userId,
142
+ );
143
+
118
144
  const expectedOrigin: string = `${HttpProtocol}${Host.toString()}`;
119
145
 
120
146
  const verification: any = await verifyRegistrationResponse({
121
147
  response: data.credential,
122
- expectedChallenge: data.challenge,
148
+ expectedChallenge: storedChallenge,
123
149
  expectedOrigin: expectedOrigin,
124
150
  expectedRPID: Host.toString(),
125
151
  });
@@ -134,10 +160,6 @@ export class Service extends DatabaseService<Model> {
134
160
  throw new BadDataException("Registration info not found");
135
161
  }
136
162
 
137
- if (!data.props.userId) {
138
- throw new BadDataException("User ID not found in request");
139
- }
140
-
141
163
  // Save the credential
142
164
  const userWebAuthn: Model = Model.fromJSON(
143
165
  {
@@ -213,6 +235,21 @@ export class Service extends DatabaseService<Model> {
213
235
  options.challenge = Buffer.from(options.challenge).toString("base64url");
214
236
  // allowCredentials id is already base64url string
215
237
 
238
+ // Store the challenge server-side so verification uses a trusted value
239
+ await UserService.updateOneById({
240
+ id: user.id!,
241
+ data: {
242
+ webauthnChallenge: options.challenge,
243
+ webauthnChallengeExpiresAt: OneUptimeDate.addRemoveMinutes(
244
+ OneUptimeDate.getCurrentDate(),
245
+ WEBAUTHN_CHALLENGE_TTL_MINUTES,
246
+ ),
247
+ },
248
+ props: {
249
+ isRoot: true,
250
+ },
251
+ });
252
+
216
253
  return {
217
254
  options: options as any,
218
255
  challenge: options.challenge,
@@ -223,9 +260,13 @@ export class Service extends DatabaseService<Model> {
223
260
  @CaptureSpan()
224
261
  public async verifyAuthentication(data: {
225
262
  userId: string;
226
- challenge: string;
227
263
  credential: any;
228
264
  }): Promise<User> {
265
+ // Retrieve the challenge from the server-side store
266
+ const storedChallenge: string = await this.getAndClearStoredChallenge(
267
+ new ObjectID(data.userId),
268
+ );
269
+
229
270
  const user: User | null = await UserService.findOneById({
230
271
  id: new ObjectID(data.userId),
231
272
  select: {
@@ -267,7 +308,7 @@ export class Service extends DatabaseService<Model> {
267
308
 
268
309
  const verification: any = await verifyAuthenticationResponse({
269
310
  response: data.credential,
270
- expectedChallenge: data.challenge,
311
+ expectedChallenge: storedChallenge,
271
312
  expectedOrigin: expectedOrigin,
272
313
  expectedRPID: Host.toString(),
273
314
  credential: {
@@ -295,6 +336,73 @@ export class Service extends DatabaseService<Model> {
295
336
  return user;
296
337
  }
297
338
 
339
+ /**
340
+ * Retrieves the stored WebAuthn challenge for the given user,
341
+ * validates it has not expired, and clears it so it cannot be reused.
342
+ */
343
+ private async getAndClearStoredChallenge(userId: ObjectID): Promise<string> {
344
+ const user: User | null = await UserService.findOneById({
345
+ id: userId,
346
+ select: {
347
+ webauthnChallenge: true,
348
+ webauthnChallengeExpiresAt: true,
349
+ },
350
+ props: {
351
+ isRoot: true,
352
+ },
353
+ });
354
+
355
+ if (!user) {
356
+ throw new BadDataException("User not found");
357
+ }
358
+
359
+ if (!user.webauthnChallenge || !user.webauthnChallengeExpiresAt) {
360
+ throw new BadDataException(
361
+ "No pending WebAuthn challenge found. Please initiate the WebAuthn flow again.",
362
+ );
363
+ }
364
+
365
+ // Check expiry
366
+ if (
367
+ OneUptimeDate.isBefore(
368
+ user.webauthnChallengeExpiresAt,
369
+ OneUptimeDate.getCurrentDate(),
370
+ )
371
+ ) {
372
+ // Clear the expired challenge
373
+ await UserService.updateOneById({
374
+ id: userId,
375
+ data: {
376
+ webauthnChallenge: null as any,
377
+ webauthnChallengeExpiresAt: null as any,
378
+ },
379
+ props: {
380
+ isRoot: true,
381
+ },
382
+ });
383
+
384
+ throw new BadDataException(
385
+ "WebAuthn challenge has expired. Please initiate the WebAuthn flow again.",
386
+ );
387
+ }
388
+
389
+ const challenge: string = user.webauthnChallenge;
390
+
391
+ // Clear the challenge immediately so it cannot be reused (one-time use)
392
+ await UserService.updateOneById({
393
+ id: userId,
394
+ data: {
395
+ webauthnChallenge: null as any,
396
+ webauthnChallengeExpiresAt: null as any,
397
+ },
398
+ props: {
399
+ isRoot: true,
400
+ },
401
+ });
402
+
403
+ return challenge;
404
+ }
405
+
298
406
  @CaptureSpan()
299
407
  protected override async onBeforeCreate(
300
408
  createBy: CreateBy<Model>,
@@ -27,7 +27,6 @@ export default class BrowserUtil {
27
27
  "--disable-features=dbus", // additional D-Bus feature gate
28
28
  "--no-zygote", // skip zygote process that fails OOM score adjustments in containers
29
29
  // Memory optimization flags
30
- "--single-process", // run browser in single process to reduce memory overhead
31
30
  "--disable-extensions", // no extensions needed for monitoring
32
31
  "--disable-background-networking", // disable background network requests
33
32
  "--disable-default-apps", // don't load default apps