@highstate/backend 0.9.14 → 0.9.16

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 (144) hide show
  1. package/dist/chunk-RCB4AFGD.js +159 -0
  2. package/dist/chunk-RCB4AFGD.js.map +1 -0
  3. package/dist/chunk-WHALQHEZ.js +2017 -0
  4. package/dist/chunk-WHALQHEZ.js.map +1 -0
  5. package/dist/highstate.manifest.json +3 -3
  6. package/dist/index.js +6146 -2174
  7. package/dist/index.js.map +1 -1
  8. package/dist/library/worker/main.js +51 -159
  9. package/dist/library/worker/main.js.map +1 -1
  10. package/dist/shared/index.js +159 -43
  11. package/package.json +25 -7
  12. package/src/artifact/abstractions.ts +46 -0
  13. package/src/artifact/encryption.ts +69 -0
  14. package/src/artifact/factory.ts +36 -0
  15. package/src/artifact/index.ts +3 -0
  16. package/src/artifact/local.ts +142 -0
  17. package/src/business/api-key.ts +65 -0
  18. package/src/business/artifact.ts +288 -0
  19. package/src/business/backend-unlock.ts +10 -0
  20. package/src/business/index.ts +9 -0
  21. package/src/business/instance-lock.ts +124 -0
  22. package/src/business/instance-state.ts +292 -0
  23. package/src/business/operation.ts +251 -0
  24. package/src/business/project-unlock.ts +242 -0
  25. package/src/business/secret.ts +187 -0
  26. package/src/business/worker.ts +161 -0
  27. package/src/common/index.ts +2 -1
  28. package/src/common/performance.ts +44 -0
  29. package/src/common/tree.ts +33 -0
  30. package/src/common/utils.ts +40 -1
  31. package/src/config.ts +14 -10
  32. package/src/hotstate/abstractions.ts +48 -0
  33. package/src/hotstate/factory.ts +17 -0
  34. package/src/{secret → hotstate}/index.ts +1 -0
  35. package/src/hotstate/manager.ts +192 -0
  36. package/src/hotstate/memory.ts +100 -0
  37. package/src/hotstate/validation.ts +101 -0
  38. package/src/index.ts +2 -1
  39. package/src/library/abstractions.ts +10 -23
  40. package/src/library/factory.ts +2 -2
  41. package/src/library/local.ts +89 -102
  42. package/src/library/worker/evaluator.ts +14 -47
  43. package/src/library/worker/loader.lite.ts +41 -0
  44. package/src/library/worker/main.ts +14 -65
  45. package/src/library/worker/protocol.ts +8 -24
  46. package/src/lock/abstractions.ts +6 -0
  47. package/src/lock/factory.ts +15 -0
  48. package/src/{workspace → lock}/index.ts +1 -0
  49. package/src/lock/manager.ts +82 -0
  50. package/src/lock/memory.ts +19 -0
  51. package/src/orchestrator/manager.ts +131 -82
  52. package/src/orchestrator/operation-workset.ts +188 -77
  53. package/src/orchestrator/operation.ts +975 -284
  54. package/src/project/abstractions.ts +20 -7
  55. package/src/project/factory.ts +1 -1
  56. package/src/project/index.ts +0 -1
  57. package/src/project/local.ts +73 -17
  58. package/src/project/manager.ts +272 -131
  59. package/src/pubsub/abstractions.ts +13 -0
  60. package/src/pubsub/factory.ts +19 -0
  61. package/src/pubsub/index.ts +3 -0
  62. package/src/pubsub/local.ts +36 -0
  63. package/src/pubsub/manager.ts +100 -0
  64. package/src/pubsub/validation.ts +33 -0
  65. package/src/runner/abstractions.ts +135 -68
  66. package/src/runner/artifact-env.ts +160 -0
  67. package/src/runner/factory.ts +20 -5
  68. package/src/runner/force-abort.ts +117 -0
  69. package/src/runner/local.ts +281 -372
  70. package/src/{common → runner}/pulumi.ts +86 -37
  71. package/src/services.ts +193 -35
  72. package/src/shared/index.ts +3 -11
  73. package/src/shared/models/backend/index.ts +3 -0
  74. package/src/shared/models/backend/project.ts +63 -0
  75. package/src/shared/models/backend/unlock-method.ts +20 -0
  76. package/src/shared/models/base.ts +151 -0
  77. package/src/shared/models/errors.ts +5 -0
  78. package/src/shared/models/index.ts +4 -0
  79. package/src/shared/models/project/api-key.ts +62 -0
  80. package/src/shared/models/project/artifact.ts +113 -0
  81. package/src/shared/models/project/component.ts +45 -0
  82. package/src/shared/models/project/index.ts +14 -0
  83. package/src/shared/{project.ts → models/project/instance.ts} +12 -0
  84. package/src/shared/models/project/lock.ts +91 -0
  85. package/src/shared/{operation.ts → models/project/operation.ts} +43 -8
  86. package/src/shared/models/project/page.ts +57 -0
  87. package/src/shared/models/project/secret.ts +112 -0
  88. package/src/shared/models/project/service-account.ts +22 -0
  89. package/src/shared/models/project/state.ts +432 -0
  90. package/src/shared/models/project/terminal.ts +99 -0
  91. package/src/shared/models/project/trigger.ts +56 -0
  92. package/src/shared/models/project/unlock-method.ts +31 -0
  93. package/src/shared/models/project/worker.ts +105 -0
  94. package/src/shared/resolvers/graph-resolver.ts +74 -13
  95. package/src/shared/resolvers/index.ts +5 -0
  96. package/src/shared/resolvers/input-hash.ts +53 -15
  97. package/src/shared/resolvers/input.ts +1 -9
  98. package/src/shared/resolvers/registry.ts +7 -2
  99. package/src/shared/resolvers/state.ts +12 -0
  100. package/src/shared/resolvers/validation.ts +61 -20
  101. package/src/shared/{async-batcher.ts → utils/async-batcher.ts} +13 -1
  102. package/src/shared/utils/hash.ts +6 -0
  103. package/src/shared/utils/index.ts +3 -0
  104. package/src/shared/utils/promise-tracker.ts +23 -0
  105. package/src/state/abstractions.ts +330 -101
  106. package/src/state/encryption.ts +59 -0
  107. package/src/state/factory.ts +3 -5
  108. package/src/state/index.ts +3 -0
  109. package/src/state/keyring.ts +22 -0
  110. package/src/state/local/backend.ts +299 -0
  111. package/src/state/local/collection.ts +342 -0
  112. package/src/state/local/index.ts +2 -0
  113. package/src/state/manager.ts +804 -18
  114. package/src/state/repository/index.ts +2 -0
  115. package/src/state/repository/repository.index.ts +193 -0
  116. package/src/state/repository/repository.ts +458 -0
  117. package/src/terminal/{shared.ts → abstractions.ts} +3 -3
  118. package/src/terminal/docker.ts +18 -14
  119. package/src/terminal/factory.ts +3 -3
  120. package/src/terminal/index.ts +1 -1
  121. package/src/terminal/manager.ts +134 -80
  122. package/src/terminal/run.sh.ts +22 -10
  123. package/src/worker/abstractions.ts +42 -0
  124. package/src/worker/docker.ts +83 -0
  125. package/src/worker/factory.ts +20 -0
  126. package/src/worker/index.ts +3 -0
  127. package/src/worker/manager.ts +139 -0
  128. package/dist/chunk-C2TJAQAD.js +0 -937
  129. package/dist/chunk-C2TJAQAD.js.map +0 -1
  130. package/dist/chunk-WXDYCRTT.js +0 -234
  131. package/dist/chunk-WXDYCRTT.js.map +0 -1
  132. package/src/library/worker/loader.ts +0 -114
  133. package/src/preferences/shared.ts +0 -1
  134. package/src/project/lock.ts +0 -39
  135. package/src/secret/abstractions.ts +0 -59
  136. package/src/secret/factory.ts +0 -22
  137. package/src/secret/local.ts +0 -152
  138. package/src/shared/state.ts +0 -270
  139. package/src/shared/terminal.ts +0 -13
  140. package/src/state/local.ts +0 -612
  141. package/src/workspace/abstractions.ts +0 -41
  142. package/src/workspace/factory.ts +0 -14
  143. package/src/workspace/local.ts +0 -54
  144. /package/src/shared/{library.ts → models/backend/library.ts} +0 -0
@@ -1,33 +1,819 @@
1
- import type { InstanceState } from "../shared"
2
- import EventEmitter, { on } from "node:events"
1
+ import type { Logger } from "pino"
2
+ import type { HotStateManager } from "../hotstate"
3
+ import { randomBytes } from "node:crypto"
4
+ import { readFile } from "node:fs/promises"
5
+ import { z } from "zod"
6
+ import { BetterLock } from "better-lock"
7
+ import { v4 as uuidv4, v5 as uuidv5 } from "uuid"
8
+ import { armor, Decrypter, Encrypter, identityToRecipient } from "age-encryption"
9
+ import { LRUCache } from "lru-cache"
10
+ import {
11
+ terminalSessionSchema,
12
+ triggerSchema,
13
+ unlockMethodSchema,
14
+ terminalSchema,
15
+ terminalSpecSchema,
16
+ pageSchema,
17
+ artifactSchema,
18
+ secretSchema,
19
+ operationSchema,
20
+ instanceStateSchema,
21
+ serviceAccountSchema,
22
+ projectApiKeySchema,
23
+ workerSchema,
24
+ workerUnitRegistrationSchema,
25
+ instanceLockSchema,
26
+ compositeInstanceSchema,
27
+ pageBlockSchema,
28
+ projectSchema,
29
+ projectUnlockSuiteSchema,
30
+ } from "../shared"
31
+ import {
32
+ ProjectLockedError,
33
+ type StateBackend,
34
+ type StateBatch,
35
+ type StateSnapshot,
36
+ } from "./abstractions"
37
+ import { MasterKeyEncryptionBackend, type EncryptionBackend } from "./encryption"
38
+ import { StateIndexRepository, StateRepository, NO_KEY_HASHING } from "./repository"
39
+ import { getOrCreateBackendIdentity } from "./keyring"
3
40
 
4
- export type StateEvents = Record<string, [Partial<InstanceState>]>
41
+ export const stateManagerConfig = z.object({
42
+ HIGHSTATE_BACKEND_MASTER_KEY: z.string().optional(),
43
+ HIGHSTATE_BACKEND_MASTER_KEY_PATH: z.string().optional(),
44
+ })
5
45
 
46
+ /**
47
+ * StateManager handles the creation of repositories and provides a unified interface
48
+ * for accessing typed repositories with proper configuration.
49
+ */
6
50
  export class StateManager {
7
- private readonly stateEE = new EventEmitter<StateEvents>()
51
+ constructor(
52
+ private readonly backendNamespace: string,
53
+ private readonly encryptionBackend: EncryptionBackend,
54
+ private readonly backend: StateBackend,
55
+ private readonly hotStateManager: HotStateManager,
56
+ private readonly logger: Logger,
57
+ ) {}
8
58
 
9
59
  /**
10
- * Watches for all instance state changes in the project.
60
+ * Creates a new batch for atomic operations.
61
+ * This allows multiple operations to be executed atomically.
62
+ */
63
+ batch(): StateBatch {
64
+ return this.backend.createStateBatch()
65
+ }
66
+
67
+ /**
68
+ * Creates a snapshot of the current state.
69
+ * This allows for consistent reads without affecting ongoing writes.
70
+ */
71
+ snapshot(): StateSnapshot {
72
+ return this.backend.createStateSnapshot()
73
+ }
74
+
75
+ /**
76
+ * Gets a repository with projects.
77
+ */
78
+ getProjectRepository(): StateRepository<typeof projectSchema> {
79
+ return new StateRepository(
80
+ this.backend.createProjectCollection(),
81
+ projectSchema,
82
+ this.encryptionBackend,
83
+ this.configureBackendKeyHashing(), // to hide project IDs
84
+ this.logger,
85
+ )
86
+ }
87
+
88
+ /**
89
+ * Gets a repository for encrypted project master keys armored in AGE's format.
90
+ * These project keys are not ready to be used for encryption/decryption
91
+ * and must be first decrypted using one of the project unlock methods.
92
+ */
93
+ getProjectMasterKeyRepository(): StateRepository<z.ZodString> {
94
+ return new StateRepository(
95
+ this.backend.createProjectMasterKeyCollection(),
96
+ z.string(),
97
+ this.encryptionBackend,
98
+ this.configureBackendKeyHashing(), // to hide project IDs
99
+ this.logger,
100
+ )
101
+ }
102
+
103
+ /**
104
+ * Creates a repository for project unlock suites.
105
+ * They contain encrypted AGE identities which can be used to unlock project master keys.
106
+ */
107
+ getProjectUnlockSuiteRepository(): StateRepository<typeof projectUnlockSuiteSchema> {
108
+ return new StateRepository(
109
+ this.backend.createProjectUnlockSuiteCollection(),
110
+ projectUnlockSuiteSchema,
111
+ this.encryptionBackend,
112
+ this.configureBackendKeyHashing(), // to hide project IDs
113
+ this.logger,
114
+ )
115
+ }
116
+
117
+ /**
118
+ * Gets a repository for terminal sessions.
119
+ *
120
+ * @param projectId The ID of the project owning the terminal sessions.
121
+ */
122
+ getTerminalSessionRepository(projectId: string): StateRepository<typeof terminalSessionSchema> {
123
+ return new StateRepository(
124
+ this.backend.createTerminalSessionCollection(projectId),
125
+ terminalSessionSchema,
126
+ this.getEncryptionBackend(projectId),
127
+ NO_KEY_HASHING, // we want to sort by UUIDv7
128
+ this.logger,
129
+ )
130
+ }
131
+
132
+ /**
133
+ * Creates index repository which keeps the list of terminal sessions for a specific instance.
11
134
  *
12
- * @param projectId The project ID to watch.
13
- * @param signal The signal to abort the operation.
135
+ * @param projectId The ID of the project owning the terminal sessions.
136
+ * @param instanceId The ID of the instance to which the terminal sessions belong.
14
137
  */
15
- public async *watchInstanceStates(
138
+ getInstanceTerminalSessionIndexRepository(
16
139
  projectId: string,
17
- signal?: AbortSignal,
18
- ): AsyncIterable<Partial<InstanceState>> {
19
- for await (const [state] of on(this.stateEE, projectId, { signal })) {
20
- yield state
21
- }
140
+ instanceId: string,
141
+ ): StateIndexRepository<typeof terminalSessionSchema> {
142
+ return new StateIndexRepository(
143
+ this,
144
+ this.getTerminalSessionRepository(projectId),
145
+ new StateRepository(
146
+ this.backend.createInstanceTerminalSessionIndexCollection(projectId, instanceId),
147
+ z.string(),
148
+ this.getEncryptionBackend(projectId),
149
+ NO_KEY_HASHING, // we want to sort by UUIDv7
150
+ this.logger,
151
+ ),
152
+ this.logger,
153
+ )
154
+ }
155
+
156
+ /**
157
+ * Creates index repository which keeps the list of active terminal sessions.
158
+ *
159
+ * @param projectId The ID of the project owning the terminal sessions.
160
+ */
161
+ getActiveTerminalSessionIndexRepository(
162
+ projectId: string,
163
+ ): StateIndexRepository<typeof terminalSessionSchema> {
164
+ return new StateIndexRepository(
165
+ this,
166
+ this.getTerminalSessionRepository(projectId),
167
+ new StateRepository(
168
+ this.backend.createActiveTerminalSessionIndexCollection(projectId),
169
+ z.string(),
170
+ this.getEncryptionBackend(projectId),
171
+ this.configureKeyHashing(projectId), // to hide which terminal sessions are active
172
+ this.logger,
173
+ ),
174
+ this.logger,
175
+ )
176
+ }
177
+
178
+ /**
179
+ * Creates a repository for terminal session lines.
180
+ *
181
+ * @param projectId The ID of the project owning the terminal session.
182
+ * @param sessionId The ID of the terminal session to which the lines belong.
183
+ */
184
+ getTerminalSessionLineRepository(
185
+ projectId: string,
186
+ sessionId: string,
187
+ ): StateRepository<z.ZodString> {
188
+ return new StateRepository(
189
+ this.backend.createTerminalSessionLineCollection(projectId, sessionId),
190
+ z.string(),
191
+ this.getEncryptionBackend(projectId),
192
+ NO_KEY_HASHING, // we want to sort by UUIDv7
193
+ this.logger,
194
+ )
195
+ }
196
+
197
+ /**
198
+ * Creates a repository for project terminals.
199
+ *
200
+ * @param projectId The ID of the project owning the terminals.
201
+ */
202
+ getTerminalRepository(projectId: string): StateRepository<typeof terminalSchema> {
203
+ return new StateRepository(
204
+ this.backend.createTerminalCollection(projectId),
205
+ terminalSchema,
206
+ this.getEncryptionBackend(projectId),
207
+ NO_KEY_HASHING, // we want to sort by UUIDv7
208
+ this.logger,
209
+ )
210
+ }
211
+
212
+ /**
213
+ * Creates a repository for specifications of project terminals.
214
+ *
215
+ * @param projectId The ID of the project owning the terminals.
216
+ */
217
+ getTerminalSpecRepository(projectId: string): StateRepository<typeof terminalSpecSchema> {
218
+ return new StateRepository(
219
+ this.backend.createTerminalSpecCollection(projectId),
220
+ terminalSpecSchema,
221
+ this.getEncryptionBackend(projectId),
222
+ NO_KEY_HASHING, // nothing to hide
223
+ this.logger,
224
+ )
225
+ }
226
+
227
+ /**
228
+ * Creates a repository for pages.
229
+ *
230
+ * @param projectId The ID of the project owning the pages.
231
+ */
232
+ getPageRepository(projectId: string): StateRepository<typeof pageSchema> {
233
+ return new StateRepository(
234
+ this.backend.createPageCollection(projectId),
235
+ pageSchema,
236
+ this.getEncryptionBackend(projectId),
237
+ NO_KEY_HASHING, // we want to sort by UUIDv7
238
+ this.logger,
239
+ )
240
+ }
241
+
242
+ /**
243
+ * Creates a repository for page content.
244
+ *
245
+ * @param projectId The ID of the project owning the page content.
246
+ */
247
+ getPageContentRepository(projectId: string): StateRepository<z.ZodArray<typeof pageBlockSchema>> {
248
+ return new StateRepository(
249
+ this.backend.createPageContentCollection(projectId),
250
+ pageBlockSchema.array(),
251
+ this.getEncryptionBackend(projectId),
252
+ NO_KEY_HASHING, // nothing to hide
253
+ this.logger,
254
+ )
255
+ }
256
+
257
+ /**
258
+ * Creates a repository for service accounts.
259
+ *
260
+ * @param projectId The ID of the project owning the service accounts.
261
+ */
262
+ getServiceAccountRepository(projectId: string): StateRepository<typeof serviceAccountSchema> {
263
+ return new StateRepository(
264
+ this.backend.createServiceAccountCollection(projectId),
265
+ serviceAccountSchema,
266
+ this.getEncryptionBackend(projectId),
267
+ NO_KEY_HASHING, // we want to sort by UUIDv7
268
+ this.logger,
269
+ )
270
+ }
271
+
272
+ /**
273
+ * Creates a repository for API keys.
274
+ *
275
+ * @param projectId The ID of the project owning the API keys.
276
+ */
277
+ getApiKeyRepository(projectId: string): StateRepository<typeof projectApiKeySchema> {
278
+ return new StateRepository(
279
+ this.backend.createApiKeyCollection(projectId),
280
+ projectApiKeySchema,
281
+ this.getEncryptionBackend(projectId),
282
+ NO_KEY_HASHING, // we want to sort by ULID
283
+ this.logger,
284
+ )
285
+ }
286
+
287
+ /**
288
+ * Creates an index repository which maps API key tokens to API key IDs.
289
+ * The tokens are hashed to prevent leaking sensitive information.
290
+ *
291
+ * @param projectId The ID of the project owning the API keys.
292
+ */
293
+ getApiKeyTokenIndexRepository(
294
+ projectId: string,
295
+ ): StateIndexRepository<typeof projectApiKeySchema> {
296
+ return new StateIndexRepository(
297
+ this,
298
+ this.getApiKeyRepository(projectId),
299
+ new StateRepository(
300
+ this.backend.createApiKeyTokenIndexCollection(projectId),
301
+ z.string(),
302
+ this.getEncryptionBackend(projectId),
303
+ this.configureKeyHashing(projectId), // to hide the tokens
304
+ this.logger,
305
+ ),
306
+ this.logger,
307
+ )
308
+ }
309
+
310
+ /**
311
+ * Creates a repository for workers.
312
+ *
313
+ * @param projectId The ID of the project owning the workers.
314
+ */
315
+ getWorkerRepository(projectId: string): StateRepository<typeof workerSchema> {
316
+ return new StateRepository(
317
+ this.backend.createWorkerCollection(projectId),
318
+ workerSchema,
319
+ this.getEncryptionBackend(projectId),
320
+ NO_KEY_HASHING, // we want to sort by UUIDv7
321
+ this.logger,
322
+ )
323
+ }
324
+
325
+ /**
326
+ * Creates a repository for worker logs.
327
+ *
328
+ * @param projectId The ID of the project owning the worker logs.
329
+ * @param workerId The ID of the worker to which the logs belong.
330
+ */
331
+ getWorkerLogRepository(projectId: string, workerId: string): StateRepository<z.ZodString> {
332
+ return new StateRepository(
333
+ this.backend.createWorkerLogCollection(projectId, workerId),
334
+ z.string(),
335
+ this.getEncryptionBackend(projectId),
336
+ NO_KEY_HASHING, // we want to sort by UUIDv7
337
+ this.logger,
338
+ )
339
+ }
340
+
341
+ /**
342
+ * Creates a repository for worker registrations.
343
+ *
344
+ * @param projectId The ID of the project owning the worker registrations.
345
+ */
346
+ getWorkerRegistrationRepository(
347
+ projectId: string,
348
+ ): StateRepository<typeof workerUnitRegistrationSchema> {
349
+ return new StateRepository(
350
+ this.backend.createWorkerRegistrationCollection(projectId),
351
+ workerUnitRegistrationSchema,
352
+ this.getEncryptionBackend(projectId),
353
+ NO_KEY_HASHING, // we want to sort by UUIDv7
354
+ this.logger,
355
+ )
356
+ }
357
+
358
+ /**
359
+ * Creates an index repository which holds the list of worker registrations.
360
+ *
361
+ * @param projectId The ID of the project owning the workers.
362
+ * @param workerId The ID of the worker.
363
+ */
364
+ getWorkerRegistrationIndexRepository(
365
+ projectId: string,
366
+ workerId: string,
367
+ ): StateIndexRepository<typeof workerUnitRegistrationSchema> {
368
+ return new StateIndexRepository(
369
+ this,
370
+ this.getWorkerRegistrationRepository(projectId),
371
+ new StateRepository(
372
+ this.backend.createWorkerRegistrationIndexCollection(projectId, workerId),
373
+ z.string(),
374
+ this.getEncryptionBackend(projectId),
375
+ NO_KEY_HASHING, // we want to sort by UUIDv7
376
+ this.logger,
377
+ ),
378
+ this.logger,
379
+ )
380
+ }
381
+
382
+ /**
383
+ * Gets a repository for artifacts.
384
+ *
385
+ * @param projectId The ID of the project owning the artifacts.
386
+ */
387
+ getArtifactRepository(projectId: string): StateRepository<typeof artifactSchema> {
388
+ return new StateRepository(
389
+ this.backend.createArtifactCollection(projectId),
390
+ artifactSchema,
391
+ this.getEncryptionBackend(projectId),
392
+ NO_KEY_HASHING, // we want to sort by UUIDv7
393
+ this.logger,
394
+ )
395
+ }
396
+
397
+ /**
398
+ * Creates an index repository which maps artifact hashes to artifact IDs.
399
+ *
400
+ * @param projectId The ID of the project owning the artifacts.
401
+ */
402
+ getArtifactHashIndexRepository(projectId: string): StateIndexRepository<typeof artifactSchema> {
403
+ return new StateIndexRepository(
404
+ this,
405
+ this.getArtifactRepository(projectId),
406
+ new StateRepository(
407
+ this.backend.createArtifactHashIndexCollection(projectId),
408
+ z.string(),
409
+ this.getEncryptionBackend(projectId),
410
+ this.configureKeyHashing(projectId), // to hide the original sha256 hash
411
+ this.logger,
412
+ ),
413
+ this.logger,
414
+ )
415
+ }
416
+
417
+ /**
418
+ * Creates a repository for trigger info.
419
+ *
420
+ * @param projectId The ID of the project owning the triggers.
421
+ */
422
+ getTriggerRepository(projectId: string): StateRepository<typeof triggerSchema> {
423
+ return new StateRepository(
424
+ this.backend.createTriggerCollection(projectId),
425
+ triggerSchema,
426
+ this.getEncryptionBackend(projectId),
427
+ NO_KEY_HASHING, // we want to sort by UUIDv7
428
+ this.logger,
429
+ )
430
+ }
431
+
432
+ /**
433
+ * Creates a repository for unlock methods.
434
+ *
435
+ * @param projectId The ID of the project owning the unlock methods.
436
+ */
437
+ getUnlockMethodRepository(projectId: string): StateRepository<typeof unlockMethodSchema> {
438
+ return new StateRepository(
439
+ this.backend.createUnlockMethodCollection(projectId),
440
+ unlockMethodSchema,
441
+ this.getEncryptionBackend(projectId),
442
+ NO_KEY_HASHING, // we want to sort by UUIDv7
443
+ this.logger,
444
+ )
445
+ }
446
+
447
+ /**
448
+ * Creates a repository for secret info.
449
+ *
450
+ * @param projectId The ID of the project owning the secrets.
451
+ */
452
+ getSecretRepository(projectId: string): StateRepository<typeof secretSchema> {
453
+ const collection = this.backend.createSecretCollection(projectId)
454
+
455
+ return new StateRepository(
456
+ //
457
+ collection,
458
+ secretSchema,
459
+ this.getEncryptionBackend(projectId),
460
+ NO_KEY_HASHING, // we want to sort by UUIDv7
461
+ this.logger,
462
+ )
463
+ }
464
+
465
+ /**
466
+ * Creates a repository for secret content.
467
+ *
468
+ * The ID is secret ID, not UUIDv7.
469
+ *
470
+ * @param projectId The ID of the project owning the secret content.
471
+ */
472
+ getSecretContentRepository(projectId: string): StateRepository<z.ZodUnknown> {
473
+ return new StateRepository(
474
+ this.backend.createSecretContentCollection(projectId),
475
+ z.unknown(),
476
+ this.getEncryptionBackend(projectId),
477
+ NO_KEY_HASHING, // nothing to hide
478
+ this.logger,
479
+ )
480
+ }
481
+
482
+ /**
483
+ * Creates an index repository to map descriptors like `instance:{instanceId}:{secretName}` or `system:{systemSecretName}`
484
+ * to UUIDv7 IDs of these secrets.
485
+ *
486
+ * @param projectId The ID of the project owning the secrets.
487
+ */
488
+ createSecretIndexRepository(projectId: string): StateIndexRepository<typeof secretSchema> {
489
+ return new StateIndexRepository(
490
+ this,
491
+ this.getSecretRepository(projectId),
492
+ new StateRepository(
493
+ this.backend.createSecretIndexCollection(projectId),
494
+ z.string(),
495
+ this.getEncryptionBackend(projectId),
496
+ this.configureKeyHashing(projectId), // to hide the descriptor
497
+ this.logger,
498
+ ),
499
+ this.logger,
500
+ )
501
+ }
502
+
503
+ /**
504
+ * Creates a repository for operation logs.
505
+ *
506
+ * @param projectId The ID of the project owning the operation logs.
507
+ * @param operationId The ID of the operation to which the logs belong.
508
+ */
509
+ getOperationLogRepository(projectId: string, operationId: string): StateRepository<z.ZodString> {
510
+ return new StateRepository(
511
+ this.backend.createOperationLogCollection(projectId, operationId),
512
+ z.string(),
513
+ this.getEncryptionBackend(projectId),
514
+ NO_KEY_HASHING, // we want to sort by UUIDv7
515
+ this.logger,
516
+ )
517
+ }
518
+
519
+ /**
520
+ * Creates an index repository for instance logs.
521
+ * This repository allows to retrieve operation logs by instance ID.
522
+ *
523
+ * @param projectId The ID of the project owning the operation logs.
524
+ * @param operationId The ID of the operation to which the logs belong.
525
+ * @param instanceId The ID of the instance to which the logs belong.
526
+ */
527
+ getInstanceLogIndexRepository(
528
+ projectId: string,
529
+ operationId: string,
530
+ instanceId: string,
531
+ ): StateIndexRepository<z.ZodString> {
532
+ return new StateIndexRepository(
533
+ this,
534
+ this.getOperationLogRepository(projectId, operationId),
535
+ new StateRepository(
536
+ this.backend.createInstanceLogIndexCollection(projectId, operationId, instanceId),
537
+ z.string(),
538
+ this.getEncryptionBackend(projectId),
539
+ NO_KEY_HASHING, // we want to sort by UUIDv7
540
+ this.logger,
541
+ ),
542
+ this.logger,
543
+ )
544
+ }
545
+
546
+ /**
547
+ * Creates a repository for operation info.
548
+ *
549
+ * @param projectId The ID of the project owning the operations.
550
+ */
551
+ getOperationRepository(projectId: string): StateRepository<typeof operationSchema> {
552
+ return new StateRepository(
553
+ this.backend.createOperationCollection(projectId),
554
+ operationSchema,
555
+ this.getEncryptionBackend(projectId),
556
+ NO_KEY_HASHING, // we want to sort by UUIDv7
557
+ this.logger,
558
+ )
559
+ }
560
+
561
+ /**
562
+ * Creates an index repository for active operations.
563
+ *
564
+ * @param projectId The ID of the project owning the operations.
565
+ */
566
+ getActiveOperationIndexRepository(
567
+ projectId: string,
568
+ ): StateIndexRepository<typeof operationSchema> {
569
+ return new StateIndexRepository(
570
+ this,
571
+ this.getOperationRepository(projectId),
572
+ new StateRepository(
573
+ this.backend.createActiveOperationIndexCollection(projectId),
574
+ z.string(),
575
+ this.getEncryptionBackend(projectId),
576
+ this.configureKeyHashing(projectId), // to hide which operations are active
577
+ this.logger,
578
+ ),
579
+ this.logger,
580
+ )
581
+ }
582
+
583
+ /**
584
+ * Creates a repository for instance states with sensitive ID hashing.
585
+ *
586
+ * @param projectId The ID of the project owning the instance states.
587
+ */
588
+ getInstanceStateRepository(projectId: string): StateRepository<typeof instanceStateSchema> {
589
+ return new StateRepository(
590
+ this.backend.createInstanceStateCollection(projectId),
591
+ instanceStateSchema,
592
+ this.getEncryptionBackend(projectId),
593
+ this.configureKeyHashing(projectId), // to hide the instance ID
594
+ this.logger,
595
+ )
596
+ }
597
+
598
+ /**
599
+ * Creates a repository for instance locks.
600
+ *
601
+ * @param projectId The ID of the project owning the instance locks.
602
+ */
603
+ getInstanceLockRepository(projectId: string): StateRepository<typeof instanceLockSchema> {
604
+ return new StateRepository(
605
+ this.backend.createInstanceLockCollection(projectId),
606
+ instanceLockSchema,
607
+ this.getEncryptionBackend(projectId),
608
+ this.configureKeyHashing(projectId), // to hide the instance ID
609
+ this.logger,
610
+ )
611
+ }
612
+
613
+ /**
614
+ * Creates a repository for composite instances with sensitive ID hashing.
615
+ */
616
+ getCompositeInstanceRepository(
617
+ projectId: string,
618
+ ): StateRepository<typeof compositeInstanceSchema> {
619
+ return new StateRepository(
620
+ this.backend.createCompositeInstanceCollection(projectId),
621
+ compositeInstanceSchema,
622
+ this.getEncryptionBackend(projectId),
623
+ this.configureKeyHashing(projectId), // to hide the composite instance ID
624
+ this.logger,
625
+ )
626
+ }
627
+
628
+ /**
629
+ * Creates a repository for user layouts.
630
+ */
631
+ getUserLayoutRepository(): StateRepository<z.ZodUnknown> {
632
+ return new StateRepository(
633
+ this.backend.createUserLayoutCollection(),
634
+ z.unknown(),
635
+ this.encryptionBackend,
636
+ this.configureBackendKeyHashing(), // to hide user IDs
637
+ this.logger,
638
+ )
639
+ }
640
+
641
+ /**
642
+ * Creates a repository for project viewports.
643
+ *
644
+ * @param projectId The ID of the project owning the viewports.
645
+ */
646
+ getProjectViewportRepository(projectId: string): StateRepository<z.ZodUnknown> {
647
+ return new StateRepository(
648
+ this.backend.createProjectViewportCollection(projectId),
649
+ z.unknown(),
650
+ this.getEncryptionBackend(projectId),
651
+ this.configureKeyHashing(projectId), // to hide user IDs
652
+ this.logger,
653
+ )
22
654
  }
23
655
 
24
656
  /**
25
- * Emits a state patch for the instance in the project.
657
+ * Creates a repository for instance viewports.
26
658
  *
27
- * @param projectId The project ID to emit the state patch for.
28
- * @param patch The state patch to emit.
659
+ * @param projectId The ID of the project owning the viewports.
660
+ * @param instanceId The ID of the instance to which the viewports belong.
29
661
  */
30
- public emitStatePatch(projectId: string, patch: Partial<InstanceState>): void {
31
- this.stateEE.emit(projectId, patch)
662
+ getInstanceViewportRepository(
663
+ projectId: string,
664
+ instanceId: string,
665
+ ): StateRepository<z.ZodUnknown> {
666
+ return new StateRepository(
667
+ this.backend.createInstanceViewportCollection(projectId, instanceId),
668
+ z.unknown(),
669
+ this.getEncryptionBackend(projectId),
670
+ this.configureKeyHashing(projectId), // to hide user IDs
671
+ this.logger,
672
+ )
673
+ }
674
+
675
+ private idNamespaceHashCache = new Map<string, string>()
676
+ private idNamespaceHashLock = new BetterLock()
677
+
678
+ private getProjectIdHashNamespaceRepository(): StateRepository<z.ZodString> {
679
+ return new StateRepository(
680
+ this.backend.createProjectIdHashNamespaceCollection(),
681
+ z.string(),
682
+ this.encryptionBackend,
683
+ () => Promise.resolve(this.backendNamespace),
684
+ this.logger,
685
+ )
686
+ }
687
+
688
+ private configureBackendKeyHashing(): () => Promise<string> {
689
+ return () => Promise.resolve(this.backendNamespace)
690
+ }
691
+
692
+ public configureKeyHashing(projectId: string): () => Promise<string> {
693
+ return async () => {
694
+ const existing = this.idNamespaceHashCache.get(projectId)
695
+ if (existing) {
696
+ return existing
697
+ }
698
+
699
+ return await this.idNamespaceHashLock.acquire(projectId, async () => {
700
+ let stored = await this.getProjectIdHashNamespaceRepository().get(projectId)
701
+
702
+ if (!stored) {
703
+ stored = uuidv4()
704
+ await this.getProjectIdHashNamespaceRepository().put(projectId, stored)
705
+ }
706
+
707
+ this.idNamespaceHashCache.set(projectId, stored)
708
+ return stored
709
+ })
710
+ }
711
+ }
712
+
713
+ // store the master keys in memory cache for 30 seconds
714
+ private readonly masterKeyCache = new LRUCache<string, Uint8Array>({
715
+ ttl: 30_000,
716
+ ttlAutopurge: true,
717
+ })
718
+
719
+ async getProjectMasterKey(projectId: string): Promise<Uint8Array> {
720
+ const cachedKey = this.masterKeyCache.get(projectId)
721
+ if (cachedKey) {
722
+ return cachedKey
723
+ }
724
+
725
+ const masterKey = await this.hotStateManager.get(["project-master-key", projectId])
726
+ if (!masterKey) {
727
+ throw new ProjectLockedError(projectId)
728
+ }
729
+
730
+ this.masterKeyCache.set(projectId, masterKey)
731
+ return masterKey
732
+ }
733
+
734
+ public getEncryptionBackend(projectId: string): EncryptionBackend {
735
+ return new MasterKeyEncryptionBackend(() => this.getProjectMasterKey(projectId))
736
+ }
737
+
738
+ public getHashedProjectId(projectId: string): string {
739
+ return uuidv5(projectId, this.backendNamespace)
740
+ }
741
+
742
+ static async create(
743
+ config: z.infer<typeof stateManagerConfig>,
744
+ backend: StateBackend,
745
+ hotStateManager: HotStateManager,
746
+ logger: Logger,
747
+ ): Promise<StateManager> {
748
+ let backendId = await backend.getStaticBackendId()
749
+ if (!backendId) {
750
+ backendId = uuidv4()
751
+ await backend.setStaticBackendId(backendId)
752
+ }
753
+
754
+ const masterKey = await StateManager.loadBackendMasterKey(config, backend, logger)
755
+ const encryptionBackend = new MasterKeyEncryptionBackend(() => Promise.resolve(masterKey))
756
+
757
+ return new StateManager(backendId, encryptionBackend, backend, hotStateManager, logger)
758
+ }
759
+
760
+ static async loadBackendMasterKey(
761
+ config: z.infer<typeof stateManagerConfig>,
762
+ backend: StateBackend,
763
+ logger: Logger,
764
+ ): Promise<Uint8Array> {
765
+ // 1. try to load the master key from HIGHSTATE_BACKEND_MASTER_KEY
766
+ const configMasterKey = config.HIGHSTATE_BACKEND_MASTER_KEY
767
+ if (configMasterKey) {
768
+ logger.info("loaded backend master key from HIGHSTATE_BACKEND_MASTER_KEY")
769
+
770
+ return Buffer.from(configMasterKey, "hex")
771
+ }
772
+
773
+ // 2. try to load the master key from file path provided in HIGHSTATE_BACKEND_MASTER_KEY_PATH
774
+ const configMasterKeyPath = config.HIGHSTATE_BACKEND_MASTER_KEY_PATH
775
+ if (configMasterKeyPath) {
776
+ try {
777
+ const content = await readFile(configMasterKeyPath, "utf-8")
778
+
779
+ return Buffer.from(content.trim(), "hex")
780
+ } catch (error) {
781
+ throw new Error(`Failed to read backend master key from path: ${configMasterKeyPath}`, {
782
+ cause: error,
783
+ })
784
+ }
785
+ }
786
+
787
+ // 3. try to decrypt the master key using the keyring
788
+ try {
789
+ const identity = await getOrCreateBackendIdentity(logger)
790
+
791
+ let armoredMasterKey = await backend.getEncryptedBackendMasterKey()
792
+ if (!armoredMasterKey) {
793
+ const masterKey = randomBytes(32)
794
+ const encrypter = new Encrypter()
795
+
796
+ const recipient = await identityToRecipient(identity)
797
+ encrypter.addRecipient(recipient)
798
+
799
+ const encryptedMasterKey = await encrypter.encrypt(masterKey)
800
+ armoredMasterKey = armor.encode(encryptedMasterKey)
801
+ await backend.setEncryptedBackendMasterKey(armoredMasterKey)
802
+
803
+ logger.info("generated and stored new backend master key")
804
+ }
805
+
806
+ const decrypter = new Decrypter()
807
+ decrypter.addIdentity(identity)
808
+
809
+ const encryptedMasterKey = armor.decode(armoredMasterKey)
810
+ const masterKey = await decrypter.decrypt(encryptedMasterKey)
811
+
812
+ logger.info("loaded backend master key using OS keyring")
813
+
814
+ return masterKey
815
+ } catch (error) {
816
+ throw new Error("Failed to load master key using OS keyring", { cause: error })
817
+ }
32
818
  }
33
819
  }