@oneuptime/common 12.0.3 → 12.0.4

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 (173) hide show
  1. package/Models/DatabaseModels/CephCluster.ts +14 -0
  2. package/Models/DatabaseModels/CodeRepository.ts +14 -6
  3. package/Models/DatabaseModels/DockerHost.ts +14 -0
  4. package/Models/DatabaseModels/GlobalOidcProject.ts +18 -0
  5. package/Models/DatabaseModels/GlobalSsoProject.ts +18 -0
  6. package/Models/DatabaseModels/IoTFleet.ts +14 -0
  7. package/Models/DatabaseModels/KubernetesCluster.ts +14 -0
  8. package/Models/DatabaseModels/NetworkDevice.ts +14 -0
  9. package/Models/DatabaseModels/NetworkInterface.ts +18 -0
  10. package/Models/DatabaseModels/Project.ts +16 -2
  11. package/Models/DatabaseModels/ProxmoxCluster.ts +14 -0
  12. package/Models/DatabaseModels/Service.ts +30 -0
  13. package/Models/DatabaseModels/StatusPagePrivateUser.ts +30 -0
  14. package/Models/DatabaseModels/User.ts +30 -0
  15. package/Server/API/AIAgentDataAPI.ts +31 -0
  16. package/Server/API/DashboardAPI.ts +419 -12
  17. package/Server/API/GitHubAPI.ts +119 -283
  18. package/Server/API/UserAPI.ts +263 -1
  19. package/Server/EnvironmentConfig.ts +7 -3
  20. package/Server/Infrastructure/GlobalCache.ts +37 -0
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.ts +21 -0
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.ts +68 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.ts +100 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.ts +214 -0
  25. package/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.ts +69 -0
  26. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
  27. package/Server/Services/AnalyticsDatabaseService.ts +72 -1
  28. package/Server/Services/CodeRepositoryService.ts +105 -2
  29. package/Server/Services/DatabaseService.ts +185 -2
  30. package/Server/Services/OpenTelemetryIngestService.ts +230 -26
  31. package/Server/Services/UserService.ts +184 -0
  32. package/Server/Utils/AI/Toolbox/CodeTools.ts +45 -4
  33. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +13 -0
  34. package/Server/Utils/CodeRepository/GitHub/GitHub.ts +157 -4
  35. package/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.ts +130 -0
  36. package/Server/Utils/PasswordHash.ts +306 -0
  37. package/Tests/App/Dashboard/OverviewCustomFields.test.tsx +333 -0
  38. package/Tests/App/Dashboard/UserCustomFields.test.tsx +392 -0
  39. package/Tests/Server/API/AIAgentDataRepositoryToken.test.ts +381 -0
  40. package/Tests/Server/API/DashboardPublicAttributeValuesAPI.test.ts +943 -0
  41. package/Tests/Server/API/DashboardPublicMetricsAggregateAPI.test.ts +1296 -0
  42. package/Tests/Server/API/DashboardPublicTemplatePayloads.test.ts +598 -0
  43. package/Tests/Server/API/GitHubAppInstallationBindingAPI.test.ts +539 -0
  44. package/Tests/Server/API/Helpers.ts +6 -1
  45. package/Tests/Server/API/UserProjectsAPI.test.ts +852 -0
  46. package/Tests/Server/Infrastructure/GlobalCache.test.ts +128 -0
  47. package/Tests/Server/Infrastructure/SemaphoreMutex.test.ts +215 -0
  48. package/Tests/Server/Services/AddPerUserPasswordSaltMigration.test.ts +178 -0
  49. package/Tests/Server/Services/AnalyticsDatabasePaginationStability.test.ts +582 -0
  50. package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +5 -0
  51. package/Tests/Server/Services/CodeRepositoryInstallationBinding.test.ts +305 -0
  52. package/Tests/Server/Services/CodeRepositoryResolutionBinding.test.ts +147 -0
  53. package/Tests/Server/Services/DatabaseServicePerUserPasswordSalt.test.ts +870 -0
  54. package/Tests/Server/Services/OpenTelemetryServiceResolutionCache.test.ts +568 -0
  55. package/Tests/Server/Services/RestoreDroppedUniqueIndexesMigration.test.ts +386 -0
  56. package/Tests/Server/Services/RestoreServiceLowerNameIndexMigration.test.ts +240 -0
  57. package/Tests/Server/Services/UserServiceFirstMasterAdminElection.test.ts +885 -0
  58. package/Tests/Server/Services/WidenHashedStringColumnsForScryptMigration.test.ts +220 -0
  59. package/Tests/Server/Utils/AI/CodeTools.test.ts +47 -1
  60. package/Tests/Server/Utils/AI/CodeWriteTools.test.ts +10 -0
  61. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +120 -0
  62. package/Tests/Server/Utils/CodeRepository/GitHubInstallationBinding.test.ts +292 -0
  63. package/Tests/Server/Utils/GitHubInstallationOwnershipVerification.test.ts +340 -0
  64. package/Tests/Server/Utils/GitHubWebhookAndTreeCacheIsolation.test.ts +250 -0
  65. package/Tests/Server/Utils/PasswordHash.test.ts +555 -0
  66. package/Tests/Types/Database/ColumnLength.test.ts +7 -1
  67. package/Tests/Types/HashedStringPerUserSalt.test.ts +476 -0
  68. package/Tests/Types/Monitor/DockerAlertTemplates.test.ts +293 -0
  69. package/Tests/Types/Monitor/HostAlertTemplates.test.ts +249 -0
  70. package/Tests/Types/Monitor/IotAlertTemplates.test.ts +320 -0
  71. package/Tests/Types/Monitor/PodmanAlertTemplates.test.ts +271 -0
  72. package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +696 -0
  73. package/Tests/UI/Utils/UserProjectsModelAPI.test.ts +714 -0
  74. package/Tests/Utils/TeamMembersByProject.test.ts +655 -0
  75. package/Types/Database/ColumnLength.ts +9 -1
  76. package/Types/Database/TableColumn.ts +11 -0
  77. package/Types/Database/UnsynchronizedIndex.ts +48 -0
  78. package/Types/HashedString.ts +139 -4
  79. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +85 -11
  80. package/UI/Components/Navbar/NavBar.tsx +12 -2
  81. package/UI/Components/Navbar/NavBarMenuModal.tsx +19 -7
  82. package/UI/Utils/ModelAPI/UserProjectsModelAPI.ts +257 -0
  83. package/Utils/TeamMembersByProject.ts +237 -0
  84. package/build/dist/Models/DatabaseModels/CephCluster.js +16 -1
  85. package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
  86. package/build/dist/Models/DatabaseModels/CodeRepository.js +14 -6
  87. package/build/dist/Models/DatabaseModels/CodeRepository.js.map +1 -1
  88. package/build/dist/Models/DatabaseModels/DockerHost.js +16 -1
  89. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  90. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js +16 -1
  91. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js.map +1 -1
  92. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js +16 -1
  93. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/IoTFleet.js +16 -1
  95. package/build/dist/Models/DatabaseModels/IoTFleet.js.map +1 -1
  96. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +16 -1
  97. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  98. package/build/dist/Models/DatabaseModels/NetworkDevice.js +16 -1
  99. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  100. package/build/dist/Models/DatabaseModels/NetworkInterface.js +16 -1
  101. package/build/dist/Models/DatabaseModels/NetworkInterface.js.map +1 -1
  102. package/build/dist/Models/DatabaseModels/Project.js +16 -2
  103. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  104. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +16 -1
  105. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
  106. package/build/dist/Models/DatabaseModels/Service.js +28 -1
  107. package/build/dist/Models/DatabaseModels/Service.js.map +1 -1
  108. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js +32 -0
  109. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js.map +1 -1
  110. package/build/dist/Models/DatabaseModels/User.js +32 -0
  111. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  112. package/build/dist/Server/API/AIAgentDataAPI.js +21 -2
  113. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  114. package/build/dist/Server/API/DashboardAPI.js +304 -10
  115. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  116. package/build/dist/Server/API/GitHubAPI.js +94 -167
  117. package/build/dist/Server/API/GitHubAPI.js.map +1 -1
  118. package/build/dist/Server/API/UserAPI.js +187 -2
  119. package/build/dist/Server/API/UserAPI.js.map +1 -1
  120. package/build/dist/Server/EnvironmentConfig.js +7 -3
  121. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  122. package/build/dist/Server/Infrastructure/GlobalCache.js +29 -0
  123. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.js +14 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.js.map +1 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.js +59 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.js.map +1 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js +93 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js.map +1 -0
  130. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.js +101 -0
  131. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.js.map +1 -0
  132. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.js +66 -0
  133. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.js.map +1 -0
  134. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
  135. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  136. package/build/dist/Server/Services/AnalyticsDatabaseService.js +57 -1
  137. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  138. package/build/dist/Server/Services/CodeRepositoryService.js +90 -2
  139. package/build/dist/Server/Services/CodeRepositoryService.js.map +1 -1
  140. package/build/dist/Server/Services/DatabaseService.js +160 -2
  141. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  142. package/build/dist/Server/Services/OpenTelemetryIngestService.js +155 -25
  143. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  144. package/build/dist/Server/Services/UserService.js +165 -0
  145. package/build/dist/Server/Services/UserService.js.map +1 -1
  146. package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js +31 -1
  147. package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js.map +1 -1
  148. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +13 -0
  149. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  150. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +126 -4
  151. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
  152. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.js +136 -0
  153. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.js.map +1 -0
  154. package/build/dist/Server/Utils/PasswordHash.js +237 -0
  155. package/build/dist/Server/Utils/PasswordHash.js.map +1 -0
  156. package/build/dist/Types/Database/ColumnLength.js +9 -1
  157. package/build/dist/Types/Database/ColumnLength.js.map +1 -1
  158. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  159. package/build/dist/Types/Database/UnsynchronizedIndex.js +46 -0
  160. package/build/dist/Types/Database/UnsynchronizedIndex.js.map +1 -0
  161. package/build/dist/Types/HashedString.js +95 -5
  162. package/build/dist/Types/HashedString.js.map +1 -1
  163. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +51 -10
  164. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  165. package/build/dist/UI/Components/Navbar/NavBar.js +3 -1
  166. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  167. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +19 -7
  168. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  169. package/build/dist/UI/Utils/ModelAPI/UserProjectsModelAPI.js +156 -0
  170. package/build/dist/UI/Utils/ModelAPI/UserProjectsModelAPI.js.map +1 -0
  171. package/build/dist/Utils/TeamMembersByProject.js +145 -0
  172. package/build/dist/Utils/TeamMembersByProject.js.map +1 -0
  173. package/package.json +1 -1
@@ -80,6 +80,20 @@ import {
80
80
  */
81
81
  @Index(["projectId", "name"], { unique: true })
82
82
  @Index(["projectId", "isArchived"])
83
+ /*
84
+ * Restores a UNIQUE index that an autogenerated migration deleted.
85
+ *
86
+ * Declared natively and NAMED. TypeORM's schema builder matches database
87
+ * indexes to entity metadata by name and drops every one it cannot find, which
88
+ * is how the original was lost: it existed only in a migration, so entity
89
+ * metadata never mentioned it and `migration:generate` emitted a DROP. Naming
90
+ * it here keeps the original index name and lets the builder match, compare
91
+ * uniqueness and columns, and keep it.
92
+ */
93
+ @Index("IDX_ceph_cluster_slug", ["slug"], {
94
+ unique: true,
95
+ where: '"deletedAt" IS NULL',
96
+ })
83
97
  @TableMetadata({
84
98
  tableName: "CephCluster",
85
99
  singularName: "Ceph Cluster",
@@ -649,13 +649,21 @@ export default class CodeRepository extends BaseModel {
649
649
  })
650
650
  public repositoryUrl?: URL = undefined;
651
651
 
652
- // GitHub App specific fields
652
+ /*
653
+ * GitHub App specific fields.
654
+ *
655
+ * `create` is empty on purpose. This column decides which GitHub App
656
+ * installation a repository token is minted from, so a caller who could set
657
+ * it could name ANOTHER tenant's installation, put the row in a project they
658
+ * own, and pass every "is this row in your project?" check on the way to a
659
+ * live `ghs_` token. It is assigned server-side only — by the install
660
+ * callback and the installation webhooks, which write with `isRoot` after
661
+ * GitHub has confirmed the installation belongs to the project.
662
+ * CodeRepositoryService.onBeforeCreate/onBeforeUpdate enforce the same rule
663
+ * a second time, in case a future route writes as root.
664
+ */
653
665
  @ColumnAccessControl({
654
- create: [
655
- Permission.ProjectOwner,
656
- Permission.ProjectAdmin,
657
- Permission.CreateCodeRepository,
658
- ],
666
+ create: [],
659
667
  read: [
660
668
  Permission.ProjectOwner,
661
669
  Permission.ProjectAdmin,
@@ -72,6 +72,20 @@ import {
72
72
  @CrudApiEndpoint(new Route("/docker-host"))
73
73
  @SlugifyColumn("name", "slug")
74
74
  @Index(["projectId", "isArchived"])
75
+ /*
76
+ * Restores a UNIQUE index that an autogenerated migration deleted.
77
+ *
78
+ * Declared natively and NAMED. TypeORM's schema builder matches database
79
+ * indexes to entity metadata by name and drops every one it cannot find, which
80
+ * is how the original was lost: it existed only in a migration, so entity
81
+ * metadata never mentioned it and `migration:generate` emitted a DROP. Naming
82
+ * it here keeps the original index name and lets the builder match, compare
83
+ * uniqueness and columns, and keep it.
84
+ */
85
+ @Index("IDX_docker_host_slug", ["slug"], {
86
+ unique: true,
87
+ where: '"deletedAt" IS NULL',
88
+ })
75
89
  @TableMetadata({
76
90
  tableName: "DockerHost",
77
91
  singularName: "Docker Host",
@@ -39,6 +39,24 @@ import {
39
39
  update: [],
40
40
  })
41
41
  @CrudApiEndpoint(new Route("/global-oidc-project"))
42
+ /*
43
+ * Restores a UNIQUE index that an autogenerated migration deleted.
44
+ *
45
+ * Declared natively and NAMED. TypeORM's schema builder matches database
46
+ * indexes to entity metadata by name and drops every one it cannot find, which
47
+ * is how the original was lost: it existed only in a migration, so entity
48
+ * metadata never mentioned it and `migration:generate` emitted a DROP. Naming
49
+ * it here keeps the original index name and lets the builder match, compare
50
+ * uniqueness and columns, and keep it.
51
+ */
52
+ @Index(
53
+ "IDX_GlobalOIDCProject_globalOidcId_projectId",
54
+ ["globalOidcId", "projectId"],
55
+ {
56
+ unique: true,
57
+ where: '"deletedAt" IS NULL',
58
+ },
59
+ )
42
60
  @TableMetadata({
43
61
  tableName: "GlobalOIDCProject",
44
62
  singularName: "Global OIDC Project",
@@ -42,6 +42,24 @@ import {
42
42
  update: [],
43
43
  })
44
44
  @CrudApiEndpoint(new Route("/global-sso-project"))
45
+ /*
46
+ * Restores a UNIQUE index that an autogenerated migration deleted.
47
+ *
48
+ * Declared natively and NAMED. TypeORM's schema builder matches database
49
+ * indexes to entity metadata by name and drops every one it cannot find, which
50
+ * is how the original was lost: it existed only in a migration, so entity
51
+ * metadata never mentioned it and `migration:generate` emitted a DROP. Naming
52
+ * it here keeps the original index name and lets the builder match, compare
53
+ * uniqueness and columns, and keep it.
54
+ */
55
+ @Index(
56
+ "IDX_GlobalSSOProject_globalSsoId_projectId",
57
+ ["globalSsoId", "projectId"],
58
+ {
59
+ unique: true,
60
+ where: '"deletedAt" IS NULL',
61
+ },
62
+ )
45
63
  @TableMetadata({
46
64
  tableName: "GlobalSSOProject",
47
65
  singularName: "Global SSO Project",
@@ -80,6 +80,20 @@ import {
80
80
  */
81
81
  @Index(["projectId", "name"], { unique: true })
82
82
  @Index(["projectId", "isArchived"])
83
+ /*
84
+ * Restores a UNIQUE index that an autogenerated migration deleted.
85
+ *
86
+ * Declared natively and NAMED. TypeORM's schema builder matches database
87
+ * indexes to entity metadata by name and drops every one it cannot find, which
88
+ * is how the original was lost: it existed only in a migration, so entity
89
+ * metadata never mentioned it and `migration:generate` emitted a DROP. Naming
90
+ * it here keeps the original index name and lets the builder match, compare
91
+ * uniqueness and columns, and keep it.
92
+ */
93
+ @Index("IDX_iot_fleet_slug", ["slug"], {
94
+ unique: true,
95
+ where: '"deletedAt" IS NULL',
96
+ })
83
97
  @TableMetadata({
84
98
  tableName: "IoTFleet",
85
99
  singularName: "IoT Fleet",
@@ -79,6 +79,20 @@ import {
79
79
  */
80
80
  @Index(["projectId", "clusterIdentifier"], { unique: true })
81
81
  @Index(["projectId", "isArchived"])
82
+ /*
83
+ * Restores a UNIQUE index that an autogenerated migration deleted.
84
+ *
85
+ * Declared natively and NAMED. TypeORM's schema builder matches database
86
+ * indexes to entity metadata by name and drops every one it cannot find, which
87
+ * is how the original was lost: it existed only in a migration, so entity
88
+ * metadata never mentioned it and `migration:generate` emitted a DROP. Naming
89
+ * it here keeps the original index name and lets the builder match, compare
90
+ * uniqueness and columns, and keep it.
91
+ */
92
+ @Index("IDX_kubernetes_cluster_slug", ["slug"], {
93
+ unique: true,
94
+ where: '"deletedAt" IS NULL',
95
+ })
82
96
  @TableMetadata({
83
97
  tableName: "KubernetesCluster",
84
98
  singularName: "Kubernetes Cluster",
@@ -78,6 +78,20 @@ import {
78
78
  @CrudApiEndpoint(new Route("/network-device"))
79
79
  @SlugifyColumn("name", "slug")
80
80
  @Index(["projectId", "isArchived"])
81
+ /*
82
+ * Restores a UNIQUE index that an autogenerated migration deleted.
83
+ *
84
+ * Declared natively and NAMED. TypeORM's schema builder matches database
85
+ * indexes to entity metadata by name and drops every one it cannot find, which
86
+ * is how the original was lost: it existed only in a migration, so entity
87
+ * metadata never mentioned it and `migration:generate` emitted a DROP. Naming
88
+ * it here keeps the original index name and lets the builder match, compare
89
+ * uniqueness and columns, and keep it.
90
+ */
91
+ @Index("IDX_network_device_slug", ["slug"], {
92
+ unique: true,
93
+ where: '"deletedAt" IS NULL',
94
+ })
81
95
  @TableMetadata({
82
96
  tableName: "NetworkDevice",
83
97
  singularName: "Network Device",
@@ -72,6 +72,24 @@ const decimalTransformer: ValueTransformer = {
72
72
  })
73
73
  @CrudApiEndpoint(new Route("/network-interface"))
74
74
  @Index(["projectId", "networkDeviceId"])
75
+ /*
76
+ * Restores a UNIQUE index that an autogenerated migration deleted.
77
+ *
78
+ * Declared natively and NAMED. TypeORM's schema builder matches database
79
+ * indexes to entity metadata by name and drops every one it cannot find, which
80
+ * is how the original was lost: it existed only in a migration, so entity
81
+ * metadata never mentioned it and `migration:generate` emitted a DROP. Naming
82
+ * it here keeps the original index name and lets the builder match, compare
83
+ * uniqueness and columns, and keep it.
84
+ */
85
+ @Index(
86
+ "IDX_network_interface_device_ifindex",
87
+ ["networkDeviceId", "interfaceIndex"],
88
+ {
89
+ unique: true,
90
+ where: '"deletedAt" IS NULL',
91
+ },
92
+ )
75
93
  @TableMetadata({
76
94
  tableName: "NetworkInterface",
77
95
  singularName: "Network Interface",
@@ -2583,7 +2583,21 @@ export default class Project extends TenantModel {
2583
2583
  })
2584
2584
  public doNotAddGlobalProbesByDefaultOnNewMonitors?: boolean = undefined;
2585
2585
 
2586
- // GitHub App Installation ID for this project
2586
+ /*
2587
+ * GitHub App Installation ID for this project.
2588
+ *
2589
+ * This column IS the authority on which GitHub App installation a project
2590
+ * owns — every path that mints a repository token re-derives the binding
2591
+ * from it (see GitHubInstallationBinding). That makes it security state, not
2592
+ * project configuration, so `update` is empty: a Project Owner who could PUT
2593
+ * an arbitrary installation ID here would simply be writing their own
2594
+ * permission slip to another tenant's repositories.
2595
+ *
2596
+ * It is written server-side only, with `isRoot`, by the GitHub App install
2597
+ * callback — and only after the OAuth user-to-server round trip has proved
2598
+ * to GitHub's satisfaction that the installer controls that installation —
2599
+ * and cleared again by the `installation.deleted` webhook.
2600
+ */
2587
2601
  @ColumnAccessControl({
2588
2602
  create: [],
2589
2603
  read: [
@@ -2593,7 +2607,7 @@ export default class Project extends TenantModel {
2593
2607
  Permission.Viewer,
2594
2608
  Permission.ReadProject,
2595
2609
  ],
2596
- update: [Permission.ProjectOwner, Permission.ProjectAdmin],
2610
+ update: [],
2597
2611
  })
2598
2612
  @TableColumn({
2599
2613
  required: false,
@@ -81,6 +81,20 @@ import {
81
81
  */
82
82
  @Index(["projectId", "name"], { unique: true })
83
83
  @Index(["projectId", "isArchived"])
84
+ /*
85
+ * Restores a UNIQUE index that an autogenerated migration deleted.
86
+ *
87
+ * Declared natively and NAMED. TypeORM's schema builder matches database
88
+ * indexes to entity metadata by name and drops every one it cannot find, which
89
+ * is how the original was lost: it existed only in a migration, so entity
90
+ * metadata never mentioned it and `migration:generate` emitted a DROP. Naming
91
+ * it here keeps the original index name and lets the builder match, compare
92
+ * uniqueness and columns, and keep it.
93
+ */
94
+ @Index("IDX_proxmox_cluster_slug", ["slug"], {
95
+ unique: true,
96
+ where: '"deletedAt" IS NULL',
97
+ })
84
98
  @TableMetadata({
85
99
  tableName: "ProxmoxCluster",
86
100
  singularName: "Proxmox Cluster",
@@ -26,6 +26,7 @@ import Permission from "../../Types/Permission";
26
26
  import TechStack from "../../Types/Service/TechStack";
27
27
  import MetricDownsamplingRetentionDays from "../../Types/Metrics/MetricDownsamplingRetentionDays";
28
28
  import TelemetryRetentionConfig from "../../Types/Telemetry/TelemetryRetentionConfig";
29
+ import UNSYNCHRONIZED_INDEX from "../../Types/Database/UnsynchronizedIndex";
29
30
  import {
30
31
  Column,
31
32
  Entity,
@@ -101,6 +102,35 @@ import {
101
102
  */
102
103
  @Index(["projectId", "name"], { unique: true })
103
104
  @Index(["projectId", "isArchived"])
105
+ /*
106
+ * Serves the hottest read on the telemetry ingest path: OTLP resource
107
+ * resolution looks a service up by `service.name` case-insensitively, so
108
+ * `QueryHelper.findWithSameText` emits `LOWER("Service"."name") = $2`. The
109
+ * unique index above is over the RAW name column and cannot satisfy that
110
+ * predicate — Postgres can only use its `projectId` prefix as an index qual and
111
+ * must then heap-fetch and filter every service in the project. The real
112
+ * definition is an EXPRESSION index and lives in migration
113
+ * 1786100000000-RestoreServiceLowerNameIndex; `@Index()` cannot express
114
+ * `LOWER(...)`.
115
+ *
116
+ * `synchronize: false` is the load-bearing part of this declaration, and it is
117
+ * why the index is declared here at all rather than only in the migration.
118
+ * TypeORM's schema builder matches database indexes to entity metadata BY NAME
119
+ * and drops every one it cannot find (RdbmsSchemaBuilder.dropOldIndices). An
120
+ * index created only in a migration is therefore invisible to it and gets
121
+ * emitted as a DROP by the next autogenerated migration. That is not
122
+ * hypothetical: this exact index existed once, as
123
+ * IDX_SERVICE_PROJECT_NAME_UNIQUE in migration 1774559064921, and migration
124
+ * 1775735059360 dropped it as collateral damage — its down() even "restores"
125
+ * it without the LOWER(), so the round-trip silently loses the expression.
126
+ * Naming it here with synchronize:false makes the builder skip it in both
127
+ * directions: it will never drop it, and it will never try to create it.
128
+ */
129
+ @Index(
130
+ "IDX_SERVICE_PROJECT_LOWER_NAME",
131
+ ["projectId", "name"],
132
+ UNSYNCHRONIZED_INDEX,
133
+ )
104
134
  @TableMetadata({
105
135
  tableName: "Service",
106
136
  singularName: "Service",
@@ -319,6 +319,7 @@ export default class StatusPagePrivateUser extends BaseModel {
319
319
  @TableColumn({
320
320
  title: "Password",
321
321
  hashed: true,
322
+ hashSaltColumn: "passwordSalt",
322
323
  type: TableColumnType.HashedString,
323
324
  })
324
325
  @Column({
@@ -330,6 +331,35 @@ export default class StatusPagePrivateUser extends BaseModel {
330
331
  })
331
332
  public password?: HashedString = undefined;
332
333
 
334
+ /*
335
+ * Per-user salt mixed into `password`'s hash. Written by the database
336
+ * write path on every password write — never by an API caller, hence the
337
+ * empty access control and `computed: true`.
338
+ *
339
+ * Nullable on purpose: rows that predate per-user salts have none, and are
340
+ * verified against the legacy unsalted scheme until the next successful
341
+ * login re-hashes them with a fresh salt.
342
+ */
343
+ @ColumnAccessControl({
344
+ create: [],
345
+ read: [],
346
+ update: [],
347
+ })
348
+ @TableColumn({
349
+ title: "Password Salt",
350
+ description: "Per-user random salt mixed into this user's password hash.",
351
+ computed: true,
352
+ type: TableColumnType.ShortText,
353
+ hideColumnInDocumentation: true,
354
+ })
355
+ @Column({
356
+ type: ColumnType.ShortText,
357
+ length: ColumnLength.ShortText,
358
+ unique: false,
359
+ nullable: true,
360
+ })
361
+ public passwordSalt?: string = undefined;
362
+
333
363
  @ColumnAccessControl({
334
364
  create: [],
335
365
  read: [],
@@ -140,6 +140,7 @@ class User extends UserModel {
140
140
  @TableColumn({
141
141
  title: "Password",
142
142
  hashed: true,
143
+ hashSaltColumn: "passwordSalt",
143
144
  type: TableColumnType.HashedString,
144
145
  })
145
146
  @Column({
@@ -151,6 +152,35 @@ class User extends UserModel {
151
152
  })
152
153
  public password?: HashedString = undefined;
153
154
 
155
+ /*
156
+ * Per-user salt mixed into `password`'s hash. Written by the database
157
+ * write path on every password write — never by an API caller, hence the
158
+ * empty access control and `computed: true`.
159
+ *
160
+ * Nullable on purpose: rows that predate per-user salts have none, and are
161
+ * verified against the legacy unsalted scheme until the next successful
162
+ * login re-hashes them with a fresh salt.
163
+ */
164
+ @ColumnAccessControl({
165
+ create: [],
166
+ read: [],
167
+ update: [],
168
+ })
169
+ @TableColumn({
170
+ title: "Password Salt",
171
+ description: "Per-user random salt mixed into this user's password hash.",
172
+ computed: true,
173
+ type: TableColumnType.ShortText,
174
+ hideColumnInDocumentation: true,
175
+ })
176
+ @Column({
177
+ type: ColumnType.ShortText,
178
+ length: ColumnLength.ShortText,
179
+ unique: false,
180
+ nullable: true,
181
+ })
182
+ public passwordSalt?: string = undefined;
183
+
154
184
  @ColumnAccessControl({
155
185
  create: [],
156
186
  read: [Permission.CurrentUser],
@@ -69,6 +69,7 @@ import ObjectID from "../../Types/ObjectID";
69
69
  import GitHubUtil, {
70
70
  GitHubInstallationToken,
71
71
  } from "../Utils/CodeRepository/GitHub/GitHub";
72
+ import GitHubInstallationBinding from "../Utils/CodeRepository/GitHub/GitHubInstallationBinding";
72
73
  import CodeRepositoryType from "../../Types/CodeRepository/CodeRepositoryType";
73
74
  import URL from "../../Types/API/URL";
74
75
  import PullRequestState from "../../Types/CodeRepository/PullRequestState";
@@ -1166,6 +1167,36 @@ export default class AIAgentDataAPI {
1166
1167
  );
1167
1168
  }
1168
1169
 
1170
+ /*
1171
+ * The project check above proves the ROW is in the agent's project.
1172
+ * It does not prove the INSTALLATION is — and those are different
1173
+ * questions, because the row's installation ID is just a number
1174
+ * stored on a record. Someone who could get an arbitrary
1175
+ * installation ID onto a row in a project they own would otherwise
1176
+ * pass every check to this point and be handed a live `ghs_` token
1177
+ * for another tenant's repositories. So re-derive the binding from
1178
+ * the project itself, right before minting.
1179
+ */
1180
+ if (
1181
+ !codeRepository.projectId ||
1182
+ !(await GitHubInstallationBinding.isInstallationBoundToProject({
1183
+ projectId: codeRepository.projectId,
1184
+ installationId: codeRepository.gitHubAppInstallationId,
1185
+ }))
1186
+ ) {
1187
+ logger.error(
1188
+ `Refusing to mint a GitHub token for code repository ${codeRepositoryId.toString()}: installation ${codeRepository.gitHubAppInstallationId} is not bound to project ${codeRepository.projectId?.toString()}.`,
1189
+ );
1190
+
1191
+ return Response.sendErrorResponse(
1192
+ req,
1193
+ res,
1194
+ new BadDataException(
1195
+ "This repository's GitHub App installation is not connected to its project. Please reconnect the GitHub App from Project Settings.",
1196
+ ),
1197
+ );
1198
+ }
1199
+
1169
1200
  /*
1170
1201
  * G11 guardrail: per-repo open-PR cap, enforced HERE because the
1171
1202
  * token is the agent's only way to clone and push — a repo at its