@highstate/backend 0.9.15 → 0.9.18

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 (187) hide show
  1. package/dist/chunk-NAAIDR4U.js +8499 -0
  2. package/dist/chunk-NAAIDR4U.js.map +1 -0
  3. package/dist/chunk-OU5OQBLB.js +74 -0
  4. package/dist/chunk-OU5OQBLB.js.map +1 -0
  5. package/dist/chunk-Y7DXREVO.js +1745 -0
  6. package/dist/chunk-Y7DXREVO.js.map +1 -0
  7. package/dist/highstate.manifest.json +4 -4
  8. package/dist/index.js +7227 -2501
  9. package/dist/index.js.map +1 -1
  10. package/dist/library/package-resolution-worker.js +7 -5
  11. package/dist/library/package-resolution-worker.js.map +1 -1
  12. package/dist/library/worker/main.js +76 -185
  13. package/dist/library/worker/main.js.map +1 -1
  14. package/dist/magic-string.es-5ABAC4JN.js +1292 -0
  15. package/dist/magic-string.es-5ABAC4JN.js.map +1 -0
  16. package/dist/shared/index.js +3 -98
  17. package/dist/shared/index.js.map +1 -1
  18. package/package.json +31 -10
  19. package/src/artifact/abstractions.ts +46 -0
  20. package/src/artifact/encryption.ts +109 -0
  21. package/src/artifact/factory.ts +36 -0
  22. package/src/artifact/index.ts +3 -0
  23. package/src/artifact/local.ts +138 -0
  24. package/src/business/__traces__/secret/update-instance-secrets/create-and-delete-secrets-simultaneously.md +356 -0
  25. package/src/business/__traces__/secret/update-instance-secrets/create-new-secrets-for-instance.md +274 -0
  26. package/src/business/__traces__/secret/update-instance-secrets/delete-existing-secrets.md +223 -0
  27. package/src/business/__traces__/secret/update-instance-secrets/no-op-when-no-changes.md +147 -0
  28. package/src/business/__traces__/secret/update-instance-secrets/update-existing-secrets.md +280 -0
  29. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration-when-other-exists.md +360 -0
  30. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration.md +215 -0
  31. package/src/business/__traces__/worker/update-unit-registrations/create-multiple-workers-with-different-identities.md +427 -0
  32. package/src/business/__traces__/worker/update-unit-registrations/handle-nonexistent-registration-id-gracefully.md +217 -0
  33. package/src/business/__traces__/worker/update-unit-registrations/no-op-when-no-changes.md +132 -0
  34. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-changes.md +454 -0
  35. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-version-changes.md +426 -0
  36. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-with-same-identity-reuses-service-account.md +372 -0
  37. package/src/business/__traces__/worker/update-unit-registrations/remove-one-of-multiple-unit-registrations.md +383 -0
  38. package/src/business/__traces__/worker/update-unit-registrations/remove-unit-registration.md +245 -0
  39. package/src/business/__traces__/worker/update-unit-registrations/update-existing-unit-registration-when-params-change.md +174 -0
  40. package/src/business/__traces__/worker/update-unit-registrations/update-params-and-image-simultaneously.md +432 -0
  41. package/src/business/__traces__/worker/update-unit-registrations/worker-with-multiple-registrations-not-deleted-when-one-removed.md +220 -0
  42. package/src/business/api-key.ts +65 -0
  43. package/src/business/artifact.ts +289 -0
  44. package/src/business/backend-unlock.ts +10 -0
  45. package/src/business/index.ts +10 -0
  46. package/src/business/instance-lock.ts +125 -0
  47. package/src/business/instance-state.ts +434 -0
  48. package/src/business/operation.ts +251 -0
  49. package/src/business/project-unlock.ts +260 -0
  50. package/src/business/project.ts +299 -0
  51. package/src/business/secret.test.ts +178 -0
  52. package/src/business/secret.ts +281 -0
  53. package/src/business/worker.test.ts +614 -0
  54. package/src/business/worker.ts +398 -0
  55. package/src/common/clock.ts +18 -0
  56. package/src/common/index.ts +5 -1
  57. package/src/common/performance.ts +44 -0
  58. package/src/common/random.ts +68 -0
  59. package/src/common/test/index.ts +2 -0
  60. package/src/common/test/render.ts +98 -0
  61. package/src/common/test/tracer.ts +359 -0
  62. package/src/common/tree.ts +33 -0
  63. package/src/common/utils.ts +40 -1
  64. package/src/config.ts +19 -11
  65. package/src/hotstate/abstractions.ts +48 -0
  66. package/src/hotstate/factory.ts +17 -0
  67. package/src/{secret → hotstate}/index.ts +1 -0
  68. package/src/hotstate/manager.ts +192 -0
  69. package/src/hotstate/memory.ts +100 -0
  70. package/src/hotstate/validation.ts +100 -0
  71. package/src/index.ts +2 -1
  72. package/src/library/abstractions.ts +24 -28
  73. package/src/library/factory.ts +2 -2
  74. package/src/library/local.ts +91 -111
  75. package/src/library/worker/evaluator.ts +36 -73
  76. package/src/library/worker/loader.lite.ts +54 -0
  77. package/src/library/worker/main.ts +15 -66
  78. package/src/library/worker/protocol.ts +6 -33
  79. package/src/lock/abstractions.ts +6 -0
  80. package/src/lock/factory.ts +15 -0
  81. package/src/lock/index.ts +4 -0
  82. package/src/lock/manager.ts +97 -0
  83. package/src/lock/memory.ts +19 -0
  84. package/src/lock/test.ts +108 -0
  85. package/src/orchestrator/manager.ts +118 -90
  86. package/src/orchestrator/operation-workset.ts +181 -93
  87. package/src/orchestrator/operation.ts +1021 -283
  88. package/src/project/abstractions.ts +27 -38
  89. package/src/project/evaluation.ts +248 -0
  90. package/src/project/factory.ts +1 -1
  91. package/src/project/index.ts +1 -2
  92. package/src/project/local.ts +107 -103
  93. package/src/pubsub/abstractions.ts +13 -0
  94. package/src/pubsub/factory.ts +19 -0
  95. package/src/{workspace → pubsub}/index.ts +1 -0
  96. package/src/pubsub/local.ts +36 -0
  97. package/src/pubsub/manager.ts +108 -0
  98. package/src/pubsub/validation.ts +33 -0
  99. package/src/runner/abstractions.ts +155 -68
  100. package/src/runner/artifact-env.ts +160 -0
  101. package/src/runner/factory.ts +20 -5
  102. package/src/runner/force-abort.ts +117 -0
  103. package/src/runner/local.ts +292 -372
  104. package/src/{common → runner}/pulumi.ts +89 -37
  105. package/src/services.ts +251 -40
  106. package/src/shared/index.ts +3 -11
  107. package/src/shared/models/backend/index.ts +3 -0
  108. package/src/shared/{library.ts → models/backend/library.ts} +4 -4
  109. package/src/shared/models/backend/project.ts +82 -0
  110. package/src/shared/models/backend/unlock-method.ts +20 -0
  111. package/src/shared/models/base.ts +68 -0
  112. package/src/shared/models/errors.ts +5 -0
  113. package/src/shared/models/index.ts +4 -0
  114. package/src/shared/models/project/api-key.ts +65 -0
  115. package/src/shared/models/project/artifact.ts +83 -0
  116. package/src/shared/models/project/index.ts +13 -0
  117. package/src/shared/models/project/lock.ts +91 -0
  118. package/src/shared/models/project/model.ts +14 -0
  119. package/src/shared/{operation.ts → models/project/operation.ts} +29 -8
  120. package/src/shared/models/project/page.ts +57 -0
  121. package/src/shared/models/project/secret.ts +98 -0
  122. package/src/shared/models/project/service-account.ts +22 -0
  123. package/src/shared/models/project/state.ts +449 -0
  124. package/src/shared/models/project/terminal.ts +98 -0
  125. package/src/shared/models/project/trigger.ts +56 -0
  126. package/src/shared/models/project/unlock-method.ts +38 -0
  127. package/src/shared/models/project/worker.ts +107 -0
  128. package/src/shared/resolvers/graph-resolver.ts +61 -18
  129. package/src/shared/resolvers/index.ts +5 -0
  130. package/src/shared/resolvers/input-hash.ts +53 -15
  131. package/src/shared/resolvers/input.ts +47 -13
  132. package/src/shared/resolvers/registry.ts +3 -2
  133. package/src/shared/resolvers/state.ts +2 -2
  134. package/src/shared/resolvers/validation.ts +82 -25
  135. package/src/shared/utils/args.ts +25 -0
  136. package/src/shared/{async-batcher.ts → utils/async-batcher.ts} +13 -1
  137. package/src/shared/utils/hash.ts +6 -0
  138. package/src/shared/utils/index.ts +4 -0
  139. package/src/shared/utils/promise-tracker.ts +23 -0
  140. package/src/state/abstractions.ts +199 -131
  141. package/src/state/encryption.ts +98 -0
  142. package/src/state/factory.ts +3 -5
  143. package/src/state/index.ts +4 -0
  144. package/src/state/keyring.ts +22 -0
  145. package/src/state/local/backend.ts +106 -0
  146. package/src/state/local/collection.ts +361 -0
  147. package/src/state/local/index.ts +2 -0
  148. package/src/state/manager.ts +875 -18
  149. package/src/state/memory/backend.ts +70 -0
  150. package/src/state/memory/collection.ts +270 -0
  151. package/src/state/memory/index.ts +2 -0
  152. package/src/state/repository/index.ts +2 -0
  153. package/src/state/repository/repository.index.ts +193 -0
  154. package/src/state/repository/repository.ts +507 -0
  155. package/src/state/test.ts +457 -0
  156. package/src/terminal/{shared.ts → abstractions.ts} +3 -3
  157. package/src/terminal/docker.ts +18 -14
  158. package/src/terminal/factory.ts +3 -3
  159. package/src/terminal/index.ts +1 -1
  160. package/src/terminal/manager.ts +131 -79
  161. package/src/terminal/run.sh.ts +21 -11
  162. package/src/unlock/abstractions.ts +49 -0
  163. package/src/unlock/index.ts +2 -0
  164. package/src/unlock/memory.ts +32 -0
  165. package/src/worker/abstractions.ts +42 -0
  166. package/src/worker/docker.ts +83 -0
  167. package/src/worker/factory.ts +20 -0
  168. package/src/worker/index.ts +3 -0
  169. package/src/worker/manager.ts +167 -0
  170. package/dist/chunk-KTGKNSKM.js +0 -979
  171. package/dist/chunk-KTGKNSKM.js.map +0 -1
  172. package/dist/chunk-WXDYCRTT.js +0 -234
  173. package/dist/chunk-WXDYCRTT.js.map +0 -1
  174. package/src/library/worker/loader.ts +0 -114
  175. package/src/preferences/shared.ts +0 -1
  176. package/src/project/lock.ts +0 -39
  177. package/src/project/manager.ts +0 -433
  178. package/src/secret/abstractions.ts +0 -59
  179. package/src/secret/factory.ts +0 -22
  180. package/src/secret/local.ts +0 -152
  181. package/src/shared/project.ts +0 -62
  182. package/src/shared/state.ts +0 -247
  183. package/src/shared/terminal.ts +0 -14
  184. package/src/state/local.ts +0 -612
  185. package/src/workspace/abstractions.ts +0 -41
  186. package/src/workspace/factory.ts +0 -14
  187. package/src/workspace/local.ts +0 -54
@@ -3,10 +3,22 @@ export type AsyncBatcherOptions = {
3
3
  maxWaitTimeMs?: number
4
4
  }
5
5
 
6
+ export type AsyncBatcher<T> = {
7
+ /**
8
+ * Add an item to the batch.
9
+ */
10
+ call(item: T): void
11
+
12
+ /**
13
+ * Immediately flush the pending batch (if any).
14
+ */
15
+ flush(): Promise<void>
16
+ }
17
+
6
18
  export function createAsyncBatcher<T>(
7
19
  fn: (items: T[]) => Promise<void> | void,
8
20
  { waitMs = 100, maxWaitTimeMs = 1000 }: AsyncBatcherOptions = {},
9
- ) {
21
+ ): AsyncBatcher<T> {
10
22
  let batch: T[] = []
11
23
  let activeTimeout: NodeJS.Timeout | null = null
12
24
  let maxWaitTimeout: NodeJS.Timeout | null = null
@@ -0,0 +1,6 @@
1
+ export function int32ToBytes(value: number): Uint8Array {
2
+ const buffer = new ArrayBuffer(4)
3
+ const view = new DataView(buffer)
4
+ view.setInt32(0, value, true) // true for little-endian
5
+ return new Uint8Array(buffer)
6
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./promise-tracker"
2
+ export * from "./async-batcher"
3
+ export * from "./hash"
4
+ export * from "./args"
@@ -0,0 +1,23 @@
1
+ export class PromiseTracker {
2
+ private readonly trackedPromises = new Set<Promise<unknown>>()
3
+
4
+ /**
5
+ * Tracks a promise to ensure its rejection is handled inside the tracker's scope.
6
+ */
7
+ track<T>(promise: Promise<T>): void {
8
+ this.trackedPromises.add(promise)
9
+
10
+ void promise.finally(() => this.trackedPromises.delete(promise))
11
+ }
12
+
13
+ /**
14
+ * Waits for all tracked promises to resolve or reject.
15
+ */
16
+ async waitForAll(): Promise<void> {
17
+ if (this.trackedPromises.size === 0) {
18
+ return
19
+ }
20
+
21
+ await Promise.allSettled(this.trackedPromises)
22
+ }
23
+ }
@@ -1,221 +1,289 @@
1
- import {
2
- type CompositeInstance,
3
- type InstanceState,
4
- type ProjectOperation,
5
- type TerminalSession,
6
- } from "../shared"
1
+ import type { KeyObfuscationBackend } from "./encryption"
2
+ import { AccessError, type CollectionQueryResult } from "../shared"
7
3
 
8
- export type LogEntry = [instanceId: string, line: string]
9
- export type TerminalHistoryEntry = [sessionId: string, entry: string]
10
-
11
- export interface StateBackend {
4
+ export type CollectionQuery = {
12
5
  /**
13
- * Gets the active operations which are not completed or failed.
6
+ * The search string to filter documents by display name, descriptiion, or other text fields.
7
+ *
8
+ * Not implemented yet.
14
9
  */
15
- getActiveOperations(signal?: AbortSignal): Promise<ProjectOperation[]>
10
+ search?: string
16
11
 
17
12
  /**
18
- * Gets recent operations of the project.
19
- * If `beforeOperationId` is provided, returns the operations before the specified operation ID.
20
- * The page size is fixed to 10.
13
+ * The ID of the document to start the query from.
21
14
  *
22
- * @param projectId The ID of the project.
23
- * @param beforeOperationId The ID of the operation to get the operations before.
15
+ * If "asc" sorting is used, this will return results after this ID.
16
+ * Otherwise, it will return results before this ID.
24
17
  */
25
- getOperations(projectId: string, beforeOperationId?: string): Promise<ProjectOperation[]>
18
+ cursor?: string
26
19
 
27
20
  /**
28
- * Gets the current (cached) instance states of the project.
29
- * Actual states should be retrieved from the runner (and they are still may not be up-to-date).
21
+ * The count of documents to return.
30
22
  *
31
- * @param projectId The ID of the project.
23
+ * Defaults to 20 if not specified.
24
+ * Maximum value is 100.
32
25
  */
33
- getAllInstanceStates(projectId: string, signal?: AbortSignal): Promise<InstanceState[]>
26
+ count?: number
34
27
 
35
28
  /**
36
- * Gets the current (cached) state of the instance.
29
+ * The sorting order for the results.
37
30
  *
38
- * @param projectId The ID of the project.
39
- * @param instanceId The ID of the instance.
31
+ * By default, "asc" is used.
40
32
  */
41
- getInstanceState(projectId: string, instanceId: string): Promise<InstanceState | null>
33
+ sort?: "asc" | "desc"
34
+ }
35
+
36
+ export const CollectionBackend = Symbol("CollectionBackend")
37
+
38
+ /**
39
+ * The batch interface for write operations.
40
+ *
41
+ * It ensures that all changes made within a batch are written atomically.
42
+ */
43
+ export type StateBatch = {
44
+ [CollectionBackend]: unknown
45
+ [Symbol.asyncDispose](): Promise<void>
42
46
 
43
47
  /**
44
- * Gets the current (cached) state of the instances.
45
- *
46
- * @param projectId The ID of the project.
47
- * @param instanceIds The IDs of the instances.
48
+ * Writes all changes made in this batch as a single atomic operation.
49
+ */
50
+ write(): Promise<void>
51
+ }
52
+
53
+ /**
54
+ * The snapshot interface for read operations.
55
+ *
56
+ * It ensure the consistency of read operations performed within a single snapshot.
57
+ */
58
+ export type StateSnapshot = {
59
+ [CollectionBackend]: unknown
60
+ [Symbol.asyncDispose](): Promise<void>
61
+ }
62
+
63
+ /**
64
+ * Low-level storage abstraction that operates on raw binary data.
65
+ * This interface provides basic CRUD operations without any knowledge of data structure or validation.
66
+ */
67
+ export interface StateCollection {
68
+ /**
69
+ * The name of the collection received from the state manager.
48
70
  */
49
- getInstanceStates(projectId: string, instanceIds: string[]): Promise<InstanceState[]>
71
+ readonly name: string
50
72
 
51
73
  /**
52
- * Gets the affected instance states at the moment of the operation completion.
53
- * The logs are not included.
54
- *
55
- * @param operationId The ID of the operation.
74
+ * The parameters received from the state manager.
56
75
  */
57
- getAffectedInstanceStates(operationId: string): Promise<InstanceState[]>
76
+ readonly params: Record<string, string>
58
77
 
59
78
  /**
60
- * Gets the full logs of the instance at the moment of the operation completion.
61
- *
62
- * @param operationId The ID of the operation.
63
- * @param instanceId The ID of the instance.
79
+ * The UUIDv5 namespace for this collection which combines the collection name and parameters + project namespace if applicable.
64
80
  */
65
- getInstanceLogs(operationId: string, instanceId: string): Promise<string[]>
81
+ readonly namespace: Promise<string>
66
82
 
67
83
  /**
68
- * Puts the operation to the state.
69
- * The operation must have a unique ID sortable chronologically.
70
- * Also, the operation must have the project ID.
84
+ * Retrieves a single item by its ID.
71
85
  *
72
- * @param operation The operation to put.
86
+ * @param id The ID of the item to retrieve.
87
+ * @param snapshot Optional snapshot for consistent reads.
73
88
  */
74
- putOperation(operation: ProjectOperation): Promise<void>
89
+ get(id: string, snapshot?: StateSnapshot): Promise<Uint8Array | undefined>
75
90
 
76
91
  /**
77
- * Puts the instance states affected by the operation to the state.
92
+ * Retrieves multiple items by their IDs.
93
+ * Returns a record where keys are IDs and values are the data (or undefined if not found).
94
+ * The order of the results matches the order of the input IDs.
78
95
  *
79
- * @param projectId The ID of the project.
80
- * @param operationId The ID of the operation.
81
- * @param states The instance states to put.
96
+ * @param ids An array of IDs to retrieve.
97
+ * @param snapshot Optional snapshot for consistent reads.
82
98
  */
83
- putAffectedInstanceStates(
84
- projectId: string,
85
- operationId: string,
86
- states: InstanceState[],
87
- ): Promise<void>
99
+ getMany(ids: string[], snapshot?: StateSnapshot): Promise<(Uint8Array | undefined)[]>
88
100
 
89
101
  /**
90
- * Puts the instance states of the project to the state.
102
+ * Retrieves all items in the collection as an array of tuples.
103
+ * Use carefully, as this may return a large amount of data.
91
104
  *
92
- * @param projectId The ID of the project.
93
- * @param states The instance states to put.
105
+ * @param snapshot Optional snapshot for consistent reads.
94
106
  */
95
- putInstanceStates(projectId: string, states: InstanceState[]): Promise<void>
107
+ getAll(snapshot?: StateSnapshot): Promise<[string, Uint8Array][]>
96
108
 
97
109
  /**
98
- * Appends the log lines to the instance logs.
110
+ * Retrieves items matching a query.
99
111
  *
100
- * @param operationId The ID of the operation.
101
- * @param logs The logs to append. Each log is a tuple of the instance ID and the log line.
112
+ * @param query The query object containing search, cursor, skip, count, and sort options.
113
+ * @param snapshot Optional snapshot for consistent reads.
102
114
  */
103
- appendInstanceLogs(operationId: string, logs: LogEntry[]): Promise<void>
115
+ query(
116
+ query: CollectionQuery,
117
+ snapshot?: StateSnapshot,
118
+ ): Promise<CollectionQueryResult<[string, Uint8Array]>>
104
119
 
105
120
  /**
106
- * Gets all the evaluated composite instances of the project.
121
+ * Retrieves the total count of items in the collection.
122
+ * This is useful for pagination and understanding the size of the collection.
107
123
  *
108
- * @param projectId The ID of the project.
124
+ * @param snapshot Optional snapshot for consistent reads.
109
125
  */
110
- getCompositeInstances(projectId: string, signal?: AbortSignal): Promise<CompositeInstance[]>
126
+ getTotalCount(snapshot?: StateSnapshot): Promise<number>
111
127
 
112
128
  /**
113
- * Gets the IDs of the composite instances which are children (level 2+) of the top-level composite instances.
129
+ * Returns an async iterator over all items in the collection.
130
+ * This is more memory-efficient than getAll() for large collections.
114
131
  *
115
- * Used to track the evaluated composite instances and clear them when the top-level composite instances are cleared/changed.
116
- *
117
- * @param projectId The ID of the project.
118
- * @param instanceIds The IDs of the top-level composite instances.
132
+ * @param snapshot Optional snapshot for consistent reads.
119
133
  */
120
- getTopLevelCompositeChildrenIds(
121
- projectId: string,
122
- instanceIds: string[],
123
- signal?: AbortSignal,
124
- ): Promise<Record<string, string[]>>
134
+ iterate(snapshot?: StateSnapshot): AsyncIterable<[string, Uint8Array]>
125
135
 
126
136
  /**
127
- * Puts the IDs of the composite instances which are children (level 2+) of the top-level composite instances.
137
+ * Stores a single item with the given ID.
128
138
  *
129
- * @param projectId The ID of the project.
130
- * @param childrenIds The IDs of the composite instances (key is the top-level instance ID).
139
+ * @param id The ID of the item to store.
140
+ * @param value The binary data to store.
141
+ * @param batch Optional batch for atomic writes.
131
142
  */
132
- putTopLevelCompositeChildrenIds(
133
- projectId: string,
134
- childrenIds: Record<string, string[]>,
135
- ): Promise<void>
143
+ put(id: string, value: Uint8Array, batch?: StateBatch): Promise<void>
136
144
 
137
145
  /**
138
- * Gets the evaluated composite instance of the project.
146
+ * Stores multiple items in a batch operation.
139
147
  *
140
- * @param projectId The ID of the project.
141
- * @param instanceId The ID of the instance.
148
+ * @param entries An array of key-value pairs where keys are IDs and values are binary data.
149
+ * @param batch Optional batch for atomic writes.
142
150
  */
143
- getCompositeInstance(projectId: string, instanceId: string): Promise<CompositeInstance | null>
151
+ putMany(entries: Array<[string, Uint8Array]>, batch?: StateBatch): Promise<void>
144
152
 
145
153
  /**
146
- * Gets the input hashes of the evaluated composite instances.
154
+ * Deletes a single item by its ID.
147
155
  *
148
- * @param projectId The ID of the project.
156
+ * @param id The ID of the item to delete.
157
+ * @param batch Optional batch for atomic writes.
149
158
  */
150
- getCompositeInstanceInputHashes(projectId: string): Promise<Record<string, string>>
159
+ delete(id: string, batch?: StateBatch): Promise<void>
151
160
 
152
161
  /**
153
- * Puts the evaluated composite instances of the project to the state.
162
+ * Deletes multiple items by their IDs.
154
163
  *
155
- * @param projectId The ID of the project.
156
- * @param instances The instances to put.
164
+ * @param ids An array of IDs to delete.
165
+ * @param batch Optional batch for atomic writes.
157
166
  */
158
- putCompositeInstances(projectId: string, instances: CompositeInstance[]): Promise<void>
167
+ deleteMany(ids: string[], batch?: StateBatch): Promise<void>
159
168
 
160
169
  /**
161
- * Clears the evaluation state of the instance.
162
- *
163
- * @param projectId The ID of the project.
164
- * @param instanceIds The IDs of the instances to clear.
170
+ * Deletes all items in the collection.
165
171
  */
166
- clearCompositeInstances(projectId: string, instanceIds: string[]): Promise<void>
172
+ clear(): Promise<void>
173
+ }
174
+
175
+ export type CollectionMap = {
176
+ // Backend System
177
+ projects: Record<never, never>
178
+ "project-name-index": Record<never, never>
179
+ "project-master-keys": Record<never, never>
180
+ "project-namespaces": Record<never, never>
181
+ "project-unlock-suites": Record<never, never>
182
+ "user-layouts": Record<never, never>
183
+
184
+ // Instances
185
+ "instance-states": { projectId: string }
186
+ "instance-locks": { projectId: string }
187
+ "virtual-instances": { projectId: string }
167
188
 
189
+ // Operations
190
+ operations: { projectId: string }
191
+ "active-operations": { projectId: string }
192
+ "operation-logs": { projectId: string; operationId: string }
193
+ "instance-log-index": { projectId: string; operationId: string; instanceId: string }
194
+
195
+ // Secrets
196
+ secrets: { projectId: string }
197
+ "secret-content": { projectId: string }
198
+ "secret-index": { projectId: string }
199
+
200
+ // Artifacts
201
+ artifacts: { projectId: string }
202
+ "artifact-hash-index": { projectId: string }
203
+
204
+ // Terminals
205
+ terminals: { projectId: string }
206
+ "terminal-specs": { projectId: string }
207
+ "terminal-sessions": { projectId: string }
208
+ "active-terminal-sessions": { projectId: string }
209
+ "instance-terminal-sessions": { projectId: string; instanceId: string }
210
+ "terminal-session-lines": { projectId: string; sessionId: string }
211
+
212
+ // Pages
213
+ pages: { projectId: string }
214
+ "page-contents": { projectId: string }
215
+
216
+ // Triggers
217
+ triggers: { projectId: string }
218
+
219
+ // Authentication
220
+ "unlock-methods": { projectId: string }
221
+
222
+ // Service Accounts
223
+ "service-accounts": { projectId: string }
224
+
225
+ // API Keys
226
+ "api-keys": { projectId: string }
227
+ "api-key-token-index": { projectId: string }
228
+
229
+ // Workers
230
+ workers: { projectId: string }
231
+ "worker-registrations": { projectId: string }
232
+ "worker-registration-index": { projectId: string; workerId: string }
233
+ "worker-logs": { projectId: string; workerId: string }
234
+
235
+ // Viewports
236
+ "project-viewports": { projectId: string }
237
+ "instance-viewports": { projectId: string; instanceId: string }
238
+ }
239
+
240
+ export interface StateBackend {
168
241
  /**
169
- * Gets all the terminal sessions of the instance.
242
+ * Returns the encrypted master key used for the encryption of backend-wide data.
170
243
  *
171
- * @param projectId The ID of the project.
172
- * @param instanceId The ID of the instance.
244
+ * The key is AGE-armored string.
173
245
  */
174
- getTerminalSessions(projectId: string, instanceId: string): Promise<TerminalSession[]>
246
+ getEncryptedBackendMasterKey(): Promise<string | undefined>
175
247
 
176
248
  /**
177
- * Gets the IDs of all the active terminal sessions.
249
+ * Sets the encrypted master key used for the encryption of backend-wide data.
250
+ *
251
+ * @param encryptedMasterKey The encrypted master key to set, as an AGE-armored string.
178
252
  */
179
- getActiveTerminalSessions(): Promise<TerminalSession[]>
253
+ setEncryptedBackendMasterKey(encryptedMasterKey: string): Promise<void>
180
254
 
181
255
  /**
182
- * Puts the IDs of the active terminal sessions.
256
+ * Returns the encrypted namespace used for the backend.
183
257
  */
184
- putActiveTerminalSessions(sessions: TerminalSession[]): Promise<void>
258
+ getEncryptedBackendNamespace(): Promise<Uint8Array | undefined>
185
259
 
186
260
  /**
187
- * Gets the terminal session.
188
- *
189
- * @param projectId The ID of the project.
190
- * @param instanceId The ID of the instance.
191
- * @param sessionId The ID of the session.
261
+ * Sets the static identifier for the backend.
192
262
  */
193
- getTerminalSession(
194
- projectId: string,
195
- instanceId: string,
196
- sessionId: string,
197
- ): Promise<TerminalSession | null>
263
+ setEncryptedBackendNamespace(encryptedNamespace: Uint8Array): Promise<void>
198
264
 
199
265
  /**
200
- * Puts the terminal sessions of the instance to the state.
201
- *
202
- * @param projectId The ID of the project.
203
- * @param instanceId The ID of the instance.
204
- * @param session The terminal session to put.
266
+ * Creates a batch for performing atomic write operations.
205
267
  */
206
- putTerminalSession(projectId: string, instanceId: string, session: TerminalSession): Promise<void>
268
+ createStateBatch(): StateBatch
207
269
 
208
270
  /**
209
- * Gets the history lines of the terminal session.
210
- *
211
- * @param sessionId The ID of the session.
271
+ * Creates a snapshot for consistent read operations.
212
272
  */
213
- getTerminalSessionHistory(sessionId: string): Promise<string[]>
273
+ createStateSnapshot(): StateSnapshot
214
274
 
215
275
  /**
216
- * Appends the history lines to the terminal sessions.
217
- *
218
- * @param entries The entries to append.
276
+ * Creates a collection by name with the appropriate parameters.
219
277
  */
220
- appendTerminalSessionHistory(entries: TerminalHistoryEntry[]): Promise<void>
278
+ createCollection<TName extends keyof CollectionMap>(
279
+ name: TName,
280
+ params: CollectionMap[TName],
281
+ keyObfuscationBackend: KeyObfuscationBackend,
282
+ ): StateCollection
283
+ }
284
+
285
+ export class ProjectLockedError extends AccessError {
286
+ constructor(projectId: string) {
287
+ super(`The project with ID "${projectId}" is locked, decryption is not possible.`)
288
+ }
221
289
  }
@@ -0,0 +1,98 @@
1
+ import { xchacha20poly1305 } from "@noble/ciphers/chacha.js"
2
+ import { managedNonce } from "@noble/ciphers/webcrypto.js"
3
+ import { v5 as uuidv5 } from "uuid"
4
+
5
+ export interface EncryptionBackend {
6
+ /**
7
+ * Encrypts the given binary data using the configured master key.
8
+ *
9
+ * @param data The binary data to encrypt, represented as a Uint8Array.
10
+ */
11
+ encrypt(data: Uint8Array): Promise<Uint8Array>
12
+
13
+ /**
14
+ * Decrypts the given binary data using the configured master key.
15
+ *
16
+ * @param data The binary data to decrypt, represented as a Uint8Array.
17
+ */
18
+ decrypt(data: Uint8Array): Promise<Uint8Array>
19
+ }
20
+
21
+ /**
22
+ * The no-op encryption backend that does not perform any encryption or decryption and simply
23
+ * returns the data as-is.
24
+ */
25
+ export class PassThroughEncryptionBackend implements EncryptionBackend {
26
+ static readonly instance = new PassThroughEncryptionBackend()
27
+
28
+ encrypt(data: Uint8Array): Promise<Uint8Array> {
29
+ return Promise.resolve(data)
30
+ }
31
+
32
+ decrypt(data: Uint8Array): Promise<Uint8Array> {
33
+ return Promise.resolve(data)
34
+ }
35
+ }
36
+
37
+ /**
38
+ * The MasterKeyEncryptionBackend encryption backend that uses the noble-ciphers library to encrypt and decrypt data.
39
+ *
40
+ * It requires a master key to be provided, which is used for the actual encryption and decryption operations.
41
+ * The master key should be a 32-byte Uint8Array.
42
+ * This backend uses XChaCha20-Poly1305 for encryption and decryption.
43
+ * The nonce is 24 bytes long and is prepended to the ciphertext.
44
+ */
45
+ export class MasterKeyEncryptionBackend implements EncryptionBackend {
46
+ constructor(private readonly masterKeyProvider: () => Promise<Uint8Array>) {}
47
+
48
+ async encrypt(data: Uint8Array): Promise<Uint8Array> {
49
+ const chacha = managedNonce(xchacha20poly1305)(await this.masterKeyProvider())
50
+ const encryptedData = chacha.encrypt(data)
51
+
52
+ return Promise.resolve(encryptedData)
53
+ }
54
+
55
+ async decrypt(data: Uint8Array): Promise<Uint8Array> {
56
+ const chacha = managedNonce(xchacha20poly1305)(await this.masterKeyProvider())
57
+ const decryptedData = chacha.decrypt(data)
58
+
59
+ return Promise.resolve(decryptedData)
60
+ }
61
+ }
62
+
63
+ export interface KeyObfuscationBackend {
64
+ /**
65
+ * Returns the obfuscated key for the given key.
66
+ *
67
+ * @param key The key to obfuscate, represented as a string.
68
+ */
69
+ obfuscateKey(key: string): Promise<string>
70
+ }
71
+
72
+ /**
73
+ * The PassThroughKeyObfuscationBackend does not obfuscate the key and returns it as-is.
74
+ *
75
+ * This is useful for cases where no obfuscation is needed.
76
+ */
77
+ export class PassThroughKeyObfuscationBackend implements KeyObfuscationBackend {
78
+ static readonly instance = new PassThroughKeyObfuscationBackend()
79
+
80
+ obfuscateKey(key: string): Promise<string> {
81
+ return Promise.resolve(key)
82
+ }
83
+ }
84
+
85
+ /**
86
+ * The NamespaceKeyObfuscationBackend obfuscates keys using a namespace derived from a hash function.
87
+ *
88
+ * It uses UUIDv5 to generate a consistent obfuscated key based on the provided key and namespace.
89
+ */
90
+ export class NamespaceKeyObfuscationBackend implements KeyObfuscationBackend {
91
+ constructor(private readonly hashNamespaceProvider: () => Promise<string>) {}
92
+
93
+ async obfuscateKey(key: string): Promise<string> {
94
+ const namespace = await this.hashNamespaceProvider()
95
+
96
+ return uuidv5(key, namespace)
97
+ }
98
+ }
@@ -1,22 +1,20 @@
1
1
  import type { StateBackend } from "./abstractions"
2
2
  import type { Logger } from "pino"
3
- import type { LocalPulumiHost } from "../common"
4
3
  import { z } from "zod"
5
4
  import { LocalStateBackend, localStateBackendConfig } from "./local"
6
5
 
7
6
  export const stateBackendConfig = z.object({
8
- HIGHSTATE_BACKEND_STATE_TYPE: z.enum(["local"]).default("local"),
7
+ HIGHSTATE_STATE_BACKEND_TYPE: z.enum(["local"]).default("local"),
9
8
  ...localStateBackendConfig.shape,
10
9
  })
11
10
 
12
11
  export function createStateBackend(
13
12
  config: z.infer<typeof stateBackendConfig>,
14
- localPulumiHost: LocalPulumiHost,
15
13
  logger: Logger,
16
14
  ): Promise<StateBackend> {
17
- switch (config.HIGHSTATE_BACKEND_STATE_TYPE) {
15
+ switch (config.HIGHSTATE_STATE_BACKEND_TYPE) {
18
16
  case "local": {
19
- return LocalStateBackend.create(config, localPulumiHost, logger)
17
+ return LocalStateBackend.create(config, logger)
20
18
  }
21
19
  }
22
20
  }
@@ -1,3 +1,7 @@
1
1
  export * from "./abstractions"
2
2
  export * from "./factory"
3
3
  export * from "./manager"
4
+ export * from "./repository"
5
+ export * from "./keyring"
6
+ export * from "./encryption"
7
+ export * from "./test"
@@ -0,0 +1,22 @@
1
+ import type { Logger } from "pino"
2
+ import { AsyncEntry, findCredentialsAsync } from "@napi-rs/keyring"
3
+ import { generateIdentity } from "age-encryption"
4
+
5
+ const serviceName = "io.highstate.backend"
6
+ const accountName = "identity"
7
+
8
+ export async function getOrCreateBackendIdentity(logger: Logger): Promise<string> {
9
+ const credentials = await findCredentialsAsync(serviceName)
10
+ const entry = credentials.find(entry => entry.account === accountName)
11
+
12
+ if (entry) {
13
+ logger.debug("found existing backend identity in keyring")
14
+ return entry.password
15
+ }
16
+
17
+ const newIdentity = await generateIdentity()
18
+ await new AsyncEntry(serviceName, accountName).setPassword(newIdentity)
19
+
20
+ logger.info("created new backend identity and stored it in the OS keyring")
21
+ return newIdentity
22
+ }