@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
@@ -0,0 +1,220 @@
1
+ import { WidenHashedStringColumnsForScrypt1786023262402 } from "../../../Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt";
2
+ import SchemaMigrations from "../../../Server/Infrastructure/Postgres/SchemaMigrations/Index";
3
+ import ColumnLength from "../../../Types/Database/ColumnLength";
4
+ import { QueryRunner } from "typeorm";
5
+ import { describe, expect, test } from "@jest/globals";
6
+
7
+ /*
8
+ * The migration that widens every HashedString column so a scrypt hash, which
9
+ * carries its own cost parameters, fits.
10
+ *
11
+ * THE REASON THIS FILE EXISTS.
12
+ *
13
+ * `typeorm migration:generate` does not express a varchar length change as
14
+ * ALTER COLUMN TYPE. It emits DROP COLUMN followed by ADD COLUMN — which for
15
+ * these six columns means deleting every password, every status page and
16
+ * dashboard master password, and every active session in the instance. The
17
+ * generated file was replaced by hand for exactly that reason.
18
+ *
19
+ * Anyone who regenerates this migration (or writes the next widening the same
20
+ * way) gets the destructive version back, and it looks perfectly ordinary in
21
+ * review. The first test below is the guard: no statement in this migration
22
+ * may drop a column.
23
+ */
24
+
25
+ type MakeQueryRunnerResult = {
26
+ runner: QueryRunner;
27
+ statements: Array<string>;
28
+ };
29
+
30
+ type MakeQueryRunnerFunction = () => MakeQueryRunnerResult;
31
+
32
+ const makeQueryRunner: MakeQueryRunnerFunction = (): MakeQueryRunnerResult => {
33
+ const statements: Array<string> = [];
34
+
35
+ const query: (...args: Array<unknown>) => Promise<undefined> = (
36
+ ...args: Array<unknown>
37
+ ): Promise<undefined> => {
38
+ statements.push(String(args[0]));
39
+ return Promise.resolve(undefined);
40
+ };
41
+
42
+ return {
43
+ runner: { query } as unknown as QueryRunner,
44
+ statements,
45
+ };
46
+ };
47
+
48
+ const WIDENED: ReadonlyArray<[string, string]> = [
49
+ ["User", "password"],
50
+ ["StatusPagePrivateUser", "password"],
51
+ ["StatusPage", "masterPassword"],
52
+ ["Dashboard", "masterPassword"],
53
+ ["UserSession", "refreshToken"],
54
+ ["StatusPagePrivateUserSession", "refreshToken"],
55
+ ];
56
+
57
+ describe("WidenHashedStringColumnsForScrypt — must not destroy data", () => {
58
+ test("up() never drops a column", async () => {
59
+ /*
60
+ * The whole point. A DROP COLUMN here silently deletes every credential
61
+ * in the database, and the migration would still 'succeed'.
62
+ */
63
+ const { runner, statements } = makeQueryRunner();
64
+
65
+ await new WidenHashedStringColumnsForScrypt1786023262402().up(runner);
66
+
67
+ for (const statement of statements) {
68
+ expect(statement).not.toMatch(/DROP COLUMN/i);
69
+ }
70
+ });
71
+
72
+ test("down() never drops a column either", async () => {
73
+ const { runner, statements } = makeQueryRunner();
74
+
75
+ await new WidenHashedStringColumnsForScrypt1786023262402().down(runner);
76
+
77
+ for (const statement of statements) {
78
+ expect(statement).not.toMatch(/DROP COLUMN/i);
79
+ }
80
+ });
81
+
82
+ test("up() is nothing but ALTER COLUMN TYPE statements", async () => {
83
+ const { runner, statements } = makeQueryRunner();
84
+
85
+ await new WidenHashedStringColumnsForScrypt1786023262402().up(runner);
86
+
87
+ for (const statement of statements) {
88
+ expect(statement).toMatch(
89
+ /^ALTER TABLE "[A-Za-z]+" ALTER COLUMN "[A-Za-z]+" TYPE character varying\(\d+\)$/,
90
+ );
91
+ }
92
+ });
93
+
94
+ test("it does not touch constraints or indexes", async () => {
95
+ /*
96
+ * The generated version dropped and recreated the refresh-token unique
97
+ * constraints and indexes purely to work around its own DROP COLUMN.
98
+ * Widening a varchar needs no table rewrite, so the indexes survive
99
+ * untouched and none of that churn is necessary.
100
+ */
101
+ const { runner, statements } = makeQueryRunner();
102
+
103
+ await new WidenHashedStringColumnsForScrypt1786023262402().up(runner);
104
+
105
+ for (const statement of statements) {
106
+ expect(statement).not.toMatch(/CONSTRAINT/i);
107
+ expect(statement).not.toMatch(/INDEX/i);
108
+ }
109
+ });
110
+ });
111
+
112
+ describe("WidenHashedStringColumnsForScrypt — SQL contract", () => {
113
+ test("up() widens exactly the six HashedString columns", async () => {
114
+ const { runner, statements } = makeQueryRunner();
115
+
116
+ await new WidenHashedStringColumnsForScrypt1786023262402().up(runner);
117
+
118
+ expect(statements).toHaveLength(WIDENED.length);
119
+
120
+ for (const [table, column] of WIDENED) {
121
+ expect(statements).toContain(
122
+ `ALTER TABLE "${table}" ALTER COLUMN "${column}" TYPE character varying(255)`,
123
+ );
124
+ }
125
+ });
126
+
127
+ test("it widens to whatever ColumnLength.HashedString says", async () => {
128
+ /*
129
+ * The entity metadata and the migration have to agree, or the drift check
130
+ * fails and the next generated migration tries to 'fix' it.
131
+ */
132
+ const { runner, statements } = makeQueryRunner();
133
+
134
+ await new WidenHashedStringColumnsForScrypt1786023262402().up(runner);
135
+
136
+ for (const statement of statements) {
137
+ expect(statement).toContain(
138
+ `character varying(${ColumnLength.HashedString})`,
139
+ );
140
+ }
141
+ });
142
+
143
+ test("down() returns the same six columns to 64", async () => {
144
+ const { runner, statements } = makeQueryRunner();
145
+
146
+ await new WidenHashedStringColumnsForScrypt1786023262402().down(runner);
147
+
148
+ expect(statements).toHaveLength(WIDENED.length);
149
+
150
+ for (const [table, column] of WIDENED) {
151
+ expect(statements).toContain(
152
+ `ALTER TABLE "${table}" ALTER COLUMN "${column}" TYPE character varying(64)`,
153
+ );
154
+ }
155
+ });
156
+
157
+ test("down() reverses up()'s order", async () => {
158
+ const up: MakeQueryRunnerResult = makeQueryRunner();
159
+ const down: MakeQueryRunnerResult = makeQueryRunner();
160
+
161
+ await new WidenHashedStringColumnsForScrypt1786023262402().up(up.runner);
162
+ await new WidenHashedStringColumnsForScrypt1786023262402().down(
163
+ down.runner,
164
+ );
165
+
166
+ type ColumnsFunction = (statements: Array<string>) => Array<string>;
167
+
168
+ const columns: ColumnsFunction = (
169
+ statements: Array<string>,
170
+ ): Array<string> => {
171
+ return statements.map((statement: string) => {
172
+ return statement
173
+ .match(/ALTER TABLE "([^"]+)" ALTER COLUMN "([^"]+)"/)!
174
+ .slice(1, 3)
175
+ .join(".");
176
+ });
177
+ };
178
+
179
+ expect(columns(down.statements)).toEqual(columns(up.statements).reverse());
180
+ });
181
+
182
+ test("the widened column holds a scrypt hash with room to spare", () => {
183
+ // `scrypt$N=16384,r=8,p=1$` + 64 hex characters.
184
+ const worstCaseToday: number = "scrypt$N=1048576,r=32,p=16$".length + 64;
185
+
186
+ expect(ColumnLength.HashedString).toBeGreaterThan(worstCaseToday);
187
+ });
188
+ });
189
+
190
+ describe("WidenHashedStringColumnsForScrypt — registration", () => {
191
+ test("is registered in SchemaMigrations/Index.ts", () => {
192
+ expect(SchemaMigrations).toContain(
193
+ WidenHashedStringColumnsForScrypt1786023262402,
194
+ );
195
+ });
196
+
197
+ test("appears exactly once", () => {
198
+ const occurrences: number = SchemaMigrations.filter(
199
+ (migration: { name: string }) => {
200
+ return (
201
+ migration.name === "WidenHashedStringColumnsForScrypt1786023262402"
202
+ );
203
+ },
204
+ ).length;
205
+
206
+ expect(occurrences).toBe(1);
207
+ });
208
+
209
+ test("runs after the migration that added the salt columns", () => {
210
+ const names: Array<string> = SchemaMigrations.map(
211
+ (migration: { name: string }) => {
212
+ return migration.name;
213
+ },
214
+ );
215
+
216
+ expect(
217
+ names.indexOf("WidenHashedStringColumnsForScrypt1786023262402"),
218
+ ).toBeGreaterThan(names.indexOf("AddPerUserPasswordSalt1786018109307"));
219
+ });
220
+ });
@@ -15,7 +15,8 @@ import CodeRepositoryModel from "../../../../Models/DatabaseModels/CodeRepositor
15
15
  import TelemetryException from "../../../../Models/DatabaseModels/TelemetryException";
16
16
  import CodeRepositoryType from "../../../../Types/CodeRepository/CodeRepositoryType";
17
17
  import ObjectID from "../../../../Types/ObjectID";
18
- import { describe, expect, test, afterEach } from "@jest/globals";
18
+ import GitHubInstallationBinding from "../../../../Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding";
19
+ import { describe, expect, test, afterEach, beforeEach } from "@jest/globals";
19
20
 
20
21
  /*
21
22
  * The code tools are the bridge from a telemetry signal to the source that
@@ -86,6 +87,19 @@ function mockFileContent(content: string): void {
86
87
  });
87
88
  }
88
89
 
90
+ /*
91
+ * findReadableRepositories now also proves each row's GitHub App installation
92
+ * belongs to the row's project (GHSA-xx95-gmcf-7q86), which is a database
93
+ * read. That guard has its own tests; here it would just be a Postgres
94
+ * connection these tool tests do not need, so treat every fixture row as
95
+ * bound.
96
+ */
97
+ beforeEach(() => {
98
+ jest
99
+ .spyOn(GitHubInstallationBinding, "getBoundInstallationId")
100
+ .mockResolvedValue("12345");
101
+ });
102
+
89
103
  afterEach(() => {
90
104
  jest.restoreAllMocks();
91
105
  });
@@ -119,6 +133,38 @@ describe("list_code_repositories", () => {
119
133
  expect(result.rowCount).toBe(1);
120
134
  });
121
135
 
136
+ /*
137
+ * GHSA-xx95-gmcf-7q86. A row can be in this project and still name a GitHub
138
+ * App installation the project does not own — that combination used to be
139
+ * creatable, and these tools would then read, commit and open pull requests
140
+ * in another tenant's private repositories. Being in the project is not the
141
+ * same as owning the installation, so an unbound row must be invisible here
142
+ * exactly like an uninstalled one.
143
+ */
144
+ test("hides a repository whose installation is not bound to the project", async () => {
145
+ jest
146
+ .spyOn(GitHubInstallationBinding, "getBoundInstallationId")
147
+ .mockResolvedValue("12345");
148
+
149
+ mockRepositories([
150
+ buildRepository({ name: "our-repo", installationId: "12345" }),
151
+ buildRepository({
152
+ id: ObjectID.generate(),
153
+ name: "someone-elses-repo",
154
+ installationId: "99999999",
155
+ }),
156
+ ]);
157
+
158
+ const result: ToolExecutionResult = await ListCodeRepositoriesTool.execute(
159
+ {},
160
+ ctx,
161
+ );
162
+
163
+ expect(result.dataForLlm).toContain("our-repo");
164
+ expect(result.dataForLlm).not.toContain("someone-elses-repo");
165
+ expect(result.rowCount).toBe(1);
166
+ });
167
+
122
168
  test("says plainly when no repository is connected", async () => {
123
169
  mockRepositories([]);
124
170
 
@@ -23,6 +23,7 @@ import AIAgentTaskPullRequest from "../../../../Models/DatabaseModels/AIAgentTas
23
23
  import CodeRepositoryType from "../../../../Types/CodeRepository/CodeRepositoryType";
24
24
  import PullRequestState from "../../../../Types/CodeRepository/PullRequestState";
25
25
  import ObjectID from "../../../../Types/ObjectID";
26
+ import GitHubInstallationBinding from "../../../../Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding";
26
27
  import { describe, expect, test, afterEach, beforeEach } from "@jest/globals";
27
28
 
28
29
  /*
@@ -68,6 +69,15 @@ beforeEach(() => {
68
69
  .spyOn(CodeRepositoryService, "findBy")
69
70
  .mockResolvedValue([buildRepository()] as never);
70
71
  jest.spyOn(GitHubUtil, "getDefaultBranchName").mockResolvedValue("main");
72
+
73
+ /*
74
+ * resolveTargetRepository now proves each row's GitHub App installation
75
+ * belongs to the row's project (GHSA-xx95-gmcf-7q86), which is a database
76
+ * read. That guard has its own tests; treat the fixture row as bound.
77
+ */
78
+ jest
79
+ .spyOn(GitHubInstallationBinding, "getBoundInstallationId")
80
+ .mockResolvedValue("12345");
71
81
  });
72
82
 
73
83
  afterEach(() => {
@@ -24,6 +24,7 @@ import IncludesNone from "../../../../Types/BaseDatabase/IncludesNone";
24
24
  import MultiSearch from "../../../../Types/BaseDatabase/MultiSearch";
25
25
  import Search from "../../../../Types/BaseDatabase/Search";
26
26
  import StartsWith from "../../../../Types/BaseDatabase/StartsWith";
27
+ import SortOrder from "../../../../Types/BaseDatabase/SortOrder";
27
28
  import BadDataException from "../../../../Types/Exception/BadDataException";
28
29
 
29
30
  function expectStatement(actual: Statement, expected: Statement): void {
@@ -1431,6 +1432,125 @@ describe("StatementGenerator", () => {
1431
1432
  });
1432
1433
  });
1433
1434
 
1435
+ /*
1436
+ * The separator here is load-bearing for pagination. `toSortStatement`
1437
+ * used to append sort keys with no delimiter, so anything past the first
1438
+ * key produced a malformed term (`col_1 DESCcol_2 ASC`) — which meant a
1439
+ * multi-key ORDER BY was unusable and, in practice, every paginated read
1440
+ * ran on a single, non-unique sort column. See the pagination-stability
1441
+ * suite in Tests/Server/Services/AnalyticsDatabasePaginationStability.
1442
+ */
1443
+ describe("toSortStatement", () => {
1444
+ /*
1445
+ * TestModel declares its columns through `tableColumns`, not as TS
1446
+ * properties, so a `Sort<TestModel>` literal would fail excess-property
1447
+ * checking on every key. Funnelling through one helper keeps the cast
1448
+ * in a single place instead of on each call.
1449
+ */
1450
+ const sortStatementFor: (sort: Record<string, SortOrder>) => Statement = (
1451
+ sort: Record<string, SortOrder>,
1452
+ ): Statement => {
1453
+ return generator.toSortStatement(sort as any);
1454
+ };
1455
+
1456
+ test("renders a single ascending key with no trailing separator", () => {
1457
+ const statement: Statement = sortStatementFor({
1458
+ column_1: SortOrder.Ascending,
1459
+ });
1460
+
1461
+ expect(statement.query).toBe("{p0:Identifier} ASC");
1462
+ expect(statement.query_params).toStrictEqual({ p0: "column_1" });
1463
+ });
1464
+
1465
+ test("renders a single descending key", () => {
1466
+ const statement: Statement = sortStatementFor({
1467
+ column_1: SortOrder.Descending,
1468
+ });
1469
+
1470
+ expect(statement.query).toBe("{p0:Identifier} DESC");
1471
+ expect(statement.query_params).toStrictEqual({ p0: "column_1" });
1472
+ });
1473
+
1474
+ test("comma separates two keys", () => {
1475
+ const statement: Statement = sortStatementFor({
1476
+ column_1: SortOrder.Descending,
1477
+ column_2: SortOrder.Ascending,
1478
+ });
1479
+
1480
+ expect(statement.query).toBe("{p0:Identifier} DESC, {p1:Identifier} ASC");
1481
+ expect(statement.query_params).toStrictEqual({
1482
+ p0: "column_1",
1483
+ p1: "column_2",
1484
+ });
1485
+ });
1486
+
1487
+ test("comma separates three keys and preserves declaration order", () => {
1488
+ const statement: Statement = sortStatementFor({
1489
+ column_2: SortOrder.Ascending,
1490
+ column_1: SortOrder.Descending,
1491
+ column_ObjectID: SortOrder.Ascending,
1492
+ });
1493
+
1494
+ expect(statement.query).toBe(
1495
+ "{p0:Identifier} ASC, {p1:Identifier} DESC, {p2:Identifier} ASC",
1496
+ );
1497
+ expect(statement.query_params).toStrictEqual({
1498
+ p0: "column_2",
1499
+ p1: "column_1",
1500
+ p2: "column_ObjectID",
1501
+ });
1502
+ });
1503
+
1504
+ /*
1505
+ * Regression guard for the exact malformed shape. Asserting on the
1506
+ * rendered text rather than the param map is deliberate: the old bug
1507
+ * produced correct PARAMS and broken SQL, so a params-only assertion
1508
+ * would have passed straight through it.
1509
+ */
1510
+ test("never concatenates a direction into the next identifier", () => {
1511
+ const statement: Statement = sortStatementFor({
1512
+ column_1: SortOrder.Descending,
1513
+ column_2: SortOrder.Ascending,
1514
+ });
1515
+
1516
+ expect(statement.query).not.toContain("DESC{");
1517
+ expect(statement.query).not.toContain("ASC{");
1518
+ expect(statement.query).toContain(", ");
1519
+ });
1520
+
1521
+ test("separator count is always one fewer than the key count", () => {
1522
+ const statement: Statement = sortStatementFor({
1523
+ column_ObjectID: SortOrder.Ascending,
1524
+ column_1: SortOrder.Ascending,
1525
+ column_2: SortOrder.Ascending,
1526
+ });
1527
+
1528
+ expect(statement.query.split(", ")).toHaveLength(3);
1529
+ });
1530
+
1531
+ test("an empty sort produces an empty statement", () => {
1532
+ const statement: Statement = sortStatementFor({});
1533
+
1534
+ expect(statement.query).toBe("");
1535
+ expect(statement.query_params).toStrictEqual({});
1536
+ });
1537
+
1538
+ test("still rejects an unknown column", () => {
1539
+ expect(() => {
1540
+ return sortStatementFor({ not_a_column: SortOrder.Ascending });
1541
+ }).toThrow(BadDataException);
1542
+ });
1543
+
1544
+ test("rejects an unknown column that appears after a valid one", () => {
1545
+ expect(() => {
1546
+ return sortStatementFor({
1547
+ column_1: SortOrder.Ascending,
1548
+ not_a_column: SortOrder.Descending,
1549
+ });
1550
+ }).toThrow(BadDataException);
1551
+ });
1552
+ });
1553
+
1434
1554
  describe("toColumnsCreateStatement", () => {
1435
1555
  test("should return the columns of a CREATE TABLE statement", () => {
1436
1556
  const statement: Statement = generator.toColumnsCreateStatement([