@highstate/backend 0.9.26 → 0.9.28

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 (99) hide show
  1. package/dist/{chunk-GJTMJQUW.js → chunk-QSHSXLO2.js} +18 -9
  2. package/dist/chunk-QSHSXLO2.js.map +1 -0
  3. package/dist/index.js +292 -132
  4. package/dist/index.js.map +1 -1
  5. package/dist/shared/index.js +1 -1
  6. package/package.json +3 -3
  7. package/prisma/backend/_schema/layout.prisma +3 -1
  8. package/prisma/backend/_schema/project.prisma +4 -2
  9. package/prisma/backend/_schema/unlock-method.prisma +19 -0
  10. package/prisma/backend/sqlite/migrations/{20250818082732_add_models → 20250928124105_initial_migration}/migration.sql +48 -16
  11. package/prisma/project/api-key.prisma +3 -1
  12. package/prisma/project/artifact.prisma +2 -2
  13. package/prisma/project/custom-status.prisma +1 -1
  14. package/prisma/project/layout.prisma +4 -0
  15. package/prisma/project/migrations/{20250816081310_initial → 20250928130725_initial_migration}/migration.sql +132 -46
  16. package/prisma/project/terminal.prisma +2 -2
  17. package/prisma/project/unlock-method.prisma +1 -1
  18. package/prisma/project/worker.prisma +1 -1
  19. package/src/business/backend-unlock.test.ts +133 -0
  20. package/src/business/backend-unlock.ts +76 -0
  21. package/src/business/index.ts +1 -0
  22. package/src/business/settings.test.ts +3 -2
  23. package/src/database/_generated/backend/postgresql/client.ts +9 -4
  24. package/src/database/_generated/backend/postgresql/internal/class.ts +147 -168
  25. package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +127 -40
  26. package/src/database/_generated/backend/postgresql/models/BackendUnlockMethod.ts +1156 -0
  27. package/src/database/_generated/backend/postgresql/models/Project.ts +2 -2
  28. package/src/database/_generated/backend/postgresql/models/ProjectSpace.ts +7 -1
  29. package/src/database/_generated/backend/postgresql/models/UserWorkspaceLayout.ts +1067 -0
  30. package/src/database/_generated/backend/postgresql/models.ts +2 -1
  31. package/src/database/_generated/backend/sqlite/client.ts +9 -4
  32. package/src/database/_generated/backend/sqlite/internal/class.ts +146 -165
  33. package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +127 -40
  34. package/src/database/_generated/backend/sqlite/models/BackendUnlockMethod.ts +1154 -0
  35. package/src/database/_generated/backend/sqlite/models/Project.ts +2 -2
  36. package/src/database/_generated/backend/sqlite/models/ProjectSpace.ts +7 -1
  37. package/src/database/_generated/backend/sqlite/models/UserWorkspaceLayout.ts +1065 -0
  38. package/src/database/_generated/backend/sqlite/models.ts +2 -1
  39. package/src/database/_generated/project/commonInputTypes.ts +26 -26
  40. package/src/database/_generated/project/internal/class.ts +7 -8
  41. package/src/database/_generated/project/internal/prismaNamespace.ts +8 -7
  42. package/src/database/_generated/project/models/ApiKey.ts +2 -0
  43. package/src/database/_generated/project/models/Artifact.ts +2 -2
  44. package/src/database/_generated/project/models/InstanceCustomStatus.ts +1 -1
  45. package/src/database/_generated/project/models/OperationLog.ts +49 -1
  46. package/src/database/_generated/project/models/UnlockMethod.ts +2 -2
  47. package/src/database/_generated/project/models/UserCompositeViewport.ts +16 -14
  48. package/src/database/_generated/project/models/UserProjectViewport.ts +11 -9
  49. package/src/database/_generated/project/models/WorkerVersion.ts +1 -5
  50. package/src/database/abstractions.ts +25 -3
  51. package/src/database/factory.ts +5 -6
  52. package/src/database/local/backend.ts +148 -18
  53. package/src/database/manager.ts +30 -2
  54. package/src/database/prisma.ts +1 -0
  55. package/src/orchestrator/operation-plan.ts +0 -19
  56. package/src/orchestrator/operation.ts +21 -4
  57. package/src/services.ts +12 -3
  58. package/src/shared/models/backend/unlock-method.ts +7 -13
  59. package/src/shared/models/errors.ts +14 -0
  60. package/src/shared/models/prisma.ts +10 -2
  61. package/src/test-utils/database.ts +34 -6
  62. package/dist/chunk-GJTMJQUW.js.map +0 -1
  63. package/prisma/backend/sqlite/migrations/20250817070609_initiial/migration.sql +0 -34
  64. package/prisma/backend/sqlite/migrations/20250817104948_add_fields/migration.sql +0 -59
  65. package/prisma/backend/sqlite/migrations/20250818083106_a/migration.sql +0 -19
  66. package/prisma/backend/sqlite/migrations/20250818101945_hi/migration.sql +0 -1
  67. package/prisma/backend/sqlite/migrations/20250819082315_a/migration.sql +0 -5
  68. package/prisma/project/migrations/20250816082523_test/migration.sql +0 -72
  69. package/prisma/project/migrations/20250818065643_update/migration.sql +0 -42
  70. package/prisma/project/migrations/20250818070758_a/migration.sql +0 -8
  71. package/prisma/project/migrations/20250818070913_a/migration.sql +0 -8
  72. package/prisma/project/migrations/20250818082720_add_motels/migration.sql +0 -11
  73. package/prisma/project/migrations/20250818112523_hello/migration.sql +0 -35
  74. package/prisma/project/migrations/20250819082305_a/migration.sql +0 -14
  75. package/prisma/project/migrations/20250819165004_add_missing_fields/migration.sql +0 -216
  76. package/prisma/project/migrations/20250819171309_a/migration.sql +0 -22
  77. package/prisma/project/migrations/20250820113949_a/migration.sql +0 -66
  78. package/prisma/project/migrations/20250820144256_b/migration.sql +0 -31
  79. package/prisma/project/migrations/20250820145547_a/migration.sql +0 -24
  80. package/prisma/project/migrations/20250820182517_b/migration.sql +0 -2
  81. package/prisma/project/migrations/20250821172324_a/migration.sql +0 -2
  82. package/prisma/project/migrations/20250822081339_a/migration.sql +0 -219
  83. package/prisma/project/migrations/20250822083742_b/migration.sql +0 -1
  84. package/prisma/project/migrations/20250822105134_boom/migration.sql +0 -1
  85. package/prisma/project/migrations/20250822141028_b/migration.sql +0 -1
  86. package/prisma/project/migrations/20250822142342_b/migration.sql +0 -16
  87. package/prisma/project/migrations/20250824072720_a/migration.sql +0 -1
  88. package/prisma/project/migrations/20250824093656_b/migration.sql +0 -21
  89. package/prisma/project/migrations/20250825082518_a/migration.sql +0 -1
  90. package/prisma/project/migrations/20250825085343_b/migration.sql +0 -1
  91. package/prisma/project/migrations/20250825091312_a/migration.sql +0 -1
  92. package/prisma/project/migrations/20250903095431_hi/migration.sql +0 -44
  93. package/prisma/project/migrations/20250903174255_a/migration.sql +0 -24
  94. package/prisma/project/migrations/20250908095205_hi/migration.sql +0 -18
  95. package/prisma/project/migrations/20250909155857_hi/migration.sql +0 -15
  96. package/prisma/project/migrations/20250921092621_b/migration.sql +0 -33
  97. package/prisma/project/migrations/20250921093911_b/migration.sql +0 -1
  98. package/src/database/_generated/backend/postgresql/models/UserWorkspaseLayout.ts +0 -1065
  99. package/src/database/_generated/backend/sqlite/models/UserWorkspaseLayout.ts +0 -1063
@@ -0,0 +1,133 @@
1
+ import { hostname } from "node:os"
2
+ import { describe, expect } from "vitest"
3
+ import { getInitialBackendUnlockMethodMeta } from "../database/local/backend"
4
+ import { test } from "../test-utils"
5
+ import { TestDatabaseManager } from "../test-utils/database"
6
+ import { BackendUnlockService } from "./backend-unlock"
7
+
8
+ const backendUnlockTest = test.extend<{
9
+ encryptedDatabase: TestDatabaseManager
10
+ backendUnlockService: BackendUnlockService
11
+ }>({
12
+ encryptedDatabase: [
13
+ async ({ logger }, use) => {
14
+ const database = await TestDatabaseManager.create(logger, {
15
+ isEncryptionEnabled: true,
16
+ })
17
+
18
+ try {
19
+ await use(database)
20
+ } finally {
21
+ await database.cleanup()
22
+ }
23
+ },
24
+ { scope: "file" },
25
+ ],
26
+ backendUnlockService: [
27
+ async ({ encryptedDatabase, logger }, use) => {
28
+ const service = new BackendUnlockService(
29
+ encryptedDatabase,
30
+ logger.child({ service: "BackendUnlockServiceTest" }),
31
+ )
32
+
33
+ await use(service)
34
+ },
35
+ { scope: "file" },
36
+ ],
37
+ })
38
+
39
+ describe("BackendUnlockService", () => {
40
+ backendUnlockTest(
41
+ "seeds an initial unlock method for the local machine",
42
+ async ({ encryptedDatabase }) => {
43
+ const methods = await encryptedDatabase.backend.backendUnlockMethod.findMany()
44
+ expect(methods.length).toBeGreaterThan(0)
45
+
46
+ const [initial] = methods
47
+ const meta = initial.meta as { title?: string; description?: string }
48
+
49
+ const expectedMeta = getInitialBackendUnlockMethodMeta(hostname())
50
+
51
+ expect(meta.title).toBe(expectedMeta.title)
52
+ expect(meta.description).toBe(expectedMeta.description)
53
+ },
54
+ )
55
+
56
+ backendUnlockTest("lists unlock methods", async ({ encryptedDatabase, backendUnlockService }) => {
57
+ await encryptedDatabase.backend.backendUnlockMethod.deleteMany()
58
+
59
+ const first = await encryptedDatabase.backend.backendUnlockMethod.create({
60
+ data: {
61
+ meta: { title: "Laptop" },
62
+ recipient: "age1example1",
63
+ },
64
+ })
65
+
66
+ const second = await encryptedDatabase.backend.backendUnlockMethod.create({
67
+ data: {
68
+ meta: { title: "Desktop", description: "Office" },
69
+ recipient: "age1example2",
70
+ },
71
+ })
72
+
73
+ const methods = await backendUnlockService.listUnlockMethods()
74
+
75
+ expect(methods).toHaveLength(2)
76
+ expect(methods[0].id).toBe(first.id)
77
+ expect(methods[0].meta.title).toBe("Laptop")
78
+ expect(methods[1].recipient).toBe(second.recipient)
79
+ expect(methods[1].meta.description).toBe("Office")
80
+ })
81
+
82
+ backendUnlockTest(
83
+ "adds unlock method and reencrypts master key",
84
+ async ({ encryptedDatabase, backendUnlockService }) => {
85
+ await encryptedDatabase.backend.backendUnlockMethod.deleteMany()
86
+ encryptedDatabase.backendUnlockRecipientUpdates.length = 0
87
+
88
+ const result = await backendUnlockService.addUnlockMethod({
89
+ meta: { title: "Laptop" },
90
+ recipient: "age1newrecipient",
91
+ })
92
+
93
+ expect(result.meta.title).toBe("Laptop")
94
+ const stored = await encryptedDatabase.backend.backendUnlockMethod.findUniqueOrThrow({
95
+ where: { id: result.id },
96
+ })
97
+ expect(stored.recipient).toBe("age1newrecipient")
98
+ expect(encryptedDatabase.backendUnlockRecipientUpdates).toHaveLength(1)
99
+ expect(encryptedDatabase.backendUnlockRecipientUpdates[0]).toEqual(["age1newrecipient"])
100
+ },
101
+ )
102
+
103
+ backendUnlockTest(
104
+ "deletes unlock method and reencrypts master key",
105
+ async ({ encryptedDatabase, backendUnlockService }) => {
106
+ await encryptedDatabase.backend.backendUnlockMethod.deleteMany()
107
+
108
+ const keep = await encryptedDatabase.backend.backendUnlockMethod.create({
109
+ data: {
110
+ meta: { title: "Primary" },
111
+ recipient: "age1primary",
112
+ },
113
+ })
114
+
115
+ const remove = await encryptedDatabase.backend.backendUnlockMethod.create({
116
+ data: {
117
+ meta: { title: "Old" },
118
+ recipient: "age1old",
119
+ },
120
+ })
121
+
122
+ encryptedDatabase.backendUnlockRecipientUpdates.length = 0
123
+
124
+ await backendUnlockService.deleteUnlockMethod(remove.id)
125
+
126
+ const remaining = await encryptedDatabase.backend.backendUnlockMethod.findMany()
127
+ expect(remaining).toHaveLength(1)
128
+ expect(remaining[0].id).toBe(keep.id)
129
+ expect(encryptedDatabase.backendUnlockRecipientUpdates).toHaveLength(1)
130
+ expect(encryptedDatabase.backendUnlockRecipientUpdates[0]).toEqual(["age1primary"])
131
+ },
132
+ )
133
+ })
@@ -0,0 +1,76 @@
1
+ import type { Logger } from "pino"
2
+ import type { BackendUnlockMethod, DatabaseManager } from "../database"
3
+ import {
4
+ type BackendUnlockMethodInput,
5
+ BackendUnlockMethodNotFoundError,
6
+ CannotDeleteLastBackendUnlockMethodError,
7
+ } from "../shared"
8
+
9
+ export class BackendUnlockService {
10
+ constructor(
11
+ private readonly database: DatabaseManager,
12
+ private readonly logger: Logger,
13
+ ) {}
14
+
15
+ /**
16
+ * Lists backend unlock methods ordered by creation time.
17
+ *
18
+ * @returns The ordered unlock method collection.
19
+ */
20
+ async listUnlockMethods(): Promise<BackendUnlockMethod[]> {
21
+ return await this.database.backend.backendUnlockMethod.findMany({
22
+ orderBy: { createdAt: "asc" },
23
+ })
24
+ }
25
+
26
+ /**
27
+ * Stores a new unlock method and refreshes master-key recipients.
28
+ *
29
+ * @param input Unlock method payload gathered from the CLI or automation.
30
+ * @returns The persisted unlock method.
31
+ */
32
+ async addUnlockMethod(input: BackendUnlockMethodInput): Promise<BackendUnlockMethod> {
33
+ const record = await this.database.backend.backendUnlockMethod.create({ data: input })
34
+
35
+ await this.reencryptBackendMasterKey()
36
+
37
+ return record
38
+ }
39
+
40
+ /**
41
+ * Removes an unlock method by identifier and rotates the encrypted master key.
42
+ *
43
+ * @param id Identifier of the unlock method to delete.
44
+ */
45
+ async deleteUnlockMethod(id: string): Promise<void> {
46
+ const methods = await this.database.backend.backendUnlockMethod.findMany()
47
+ const method = methods.find(m => m.id === id)
48
+ if (!method) {
49
+ throw new BackendUnlockMethodNotFoundError(id)
50
+ }
51
+
52
+ if (methods.length === 1) {
53
+ throw new CannotDeleteLastBackendUnlockMethodError()
54
+ }
55
+
56
+ await this.database.backend.backendUnlockMethod.delete({ where: { id } })
57
+ await this.reencryptBackendMasterKey()
58
+ }
59
+
60
+ private async reencryptBackendMasterKey(): Promise<void> {
61
+ if (!this.database.isEncryptionEnabled) {
62
+ return
63
+ }
64
+
65
+ const recipients = await this.database.backend.backendUnlockMethod.findMany({
66
+ select: { recipient: true },
67
+ })
68
+
69
+ await this.database.updateBackendUnlockRecipients(recipients.map(method => method.recipient))
70
+
71
+ this.logger.debug(
72
+ { recipientCount: recipients.length },
73
+ "updated backend master key recipients",
74
+ )
75
+ }
76
+ }
@@ -1,5 +1,6 @@
1
1
  export * from "./api-key"
2
2
  export * from "./artifact"
3
+ export * from "./backend-unlock"
3
4
  export * from "./instance-lock"
4
5
  export * from "./instance-state"
5
6
  export * from "./operation"
@@ -1,4 +1,5 @@
1
1
  import { createId } from "@paralleldrive/cuid2"
2
+ import { generateIdentity, identityToRecipient } from "age-encryption"
2
3
  import { describe } from "vitest"
3
4
  import { test } from "../test-utils"
4
5
  import { SettingsService } from "./settings"
@@ -460,7 +461,7 @@ describe("SettingsService", () => {
460
461
  type: "password",
461
462
  meta: { title: "Password Method" },
462
463
  encryptedIdentity: "encrypted-pwd",
463
- recipient: "user@example.com",
464
+ recipient: await identityToRecipient(await generateIdentity()),
464
465
  createdAt: new Date("2023-01-01"),
465
466
  updatedAt: new Date("2023-01-01"),
466
467
  },
@@ -472,7 +473,7 @@ describe("SettingsService", () => {
472
473
  type: "passkey",
473
474
  meta: { title: "Passkey Method" },
474
475
  encryptedIdentity: "encrypted-key",
475
- recipient: "user@example.com",
476
+ recipient: await identityToRecipient(await generateIdentity()),
476
477
  createdAt: new Date("2023-01-02"),
477
478
  updatedAt: new Date("2023-01-02"),
478
479
  },
@@ -26,8 +26,8 @@ export * as $Enums from './enums.ts'
26
26
  * @example
27
27
  * ```
28
28
  * const prisma = new PrismaClient()
29
- * // Fetch zero or more UserWorkspaseLayouts
30
- * const userWorkspaseLayouts = await prisma.userWorkspaseLayout.findMany()
29
+ * // Fetch zero or more UserWorkspaceLayouts
30
+ * const userWorkspaceLayouts = await prisma.userWorkspaceLayout.findMany()
31
31
  * ```
32
32
  *
33
33
  * Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client).
@@ -39,10 +39,10 @@ export { Prisma }
39
39
 
40
40
 
41
41
  /**
42
- * Model UserWorkspaseLayout
42
+ * Model UserWorkspaceLayout
43
43
  *
44
44
  */
45
- export type UserWorkspaseLayout = Prisma.UserWorkspaseLayoutModel
45
+ export type UserWorkspaceLayout = Prisma.UserWorkspaceLayoutModel
46
46
  /**
47
47
  * Model Library
48
48
  *
@@ -68,5 +68,10 @@ export type ProjectModelStorage = Prisma.ProjectModelStorageModel
68
68
  *
69
69
  */
70
70
  export type PulumiBackend = Prisma.PulumiBackendModel
71
+ /**
72
+ * Model BackendUnlockMethod
73
+ * Unlock methods describe trusted identities that can decrypt the backend master key.
74
+ */
75
+ export type BackendUnlockMethod = Prisma.BackendUnlockMethodModel
71
76
 
72
77