@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.
- package/dist/{chunk-GJTMJQUW.js → chunk-QSHSXLO2.js} +18 -9
- package/dist/chunk-QSHSXLO2.js.map +1 -0
- package/dist/index.js +292 -132
- package/dist/index.js.map +1 -1
- package/dist/shared/index.js +1 -1
- package/package.json +3 -3
- package/prisma/backend/_schema/layout.prisma +3 -1
- package/prisma/backend/_schema/project.prisma +4 -2
- package/prisma/backend/_schema/unlock-method.prisma +19 -0
- package/prisma/backend/sqlite/migrations/{20250818082732_add_models → 20250928124105_initial_migration}/migration.sql +48 -16
- package/prisma/project/api-key.prisma +3 -1
- package/prisma/project/artifact.prisma +2 -2
- package/prisma/project/custom-status.prisma +1 -1
- package/prisma/project/layout.prisma +4 -0
- package/prisma/project/migrations/{20250816081310_initial → 20250928130725_initial_migration}/migration.sql +132 -46
- package/prisma/project/terminal.prisma +2 -2
- package/prisma/project/unlock-method.prisma +1 -1
- package/prisma/project/worker.prisma +1 -1
- package/src/business/backend-unlock.test.ts +133 -0
- package/src/business/backend-unlock.ts +76 -0
- package/src/business/index.ts +1 -0
- package/src/business/settings.test.ts +3 -2
- package/src/database/_generated/backend/postgresql/client.ts +9 -4
- package/src/database/_generated/backend/postgresql/internal/class.ts +147 -168
- package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +127 -40
- package/src/database/_generated/backend/postgresql/models/BackendUnlockMethod.ts +1156 -0
- package/src/database/_generated/backend/postgresql/models/Project.ts +2 -2
- package/src/database/_generated/backend/postgresql/models/ProjectSpace.ts +7 -1
- package/src/database/_generated/backend/postgresql/models/UserWorkspaceLayout.ts +1067 -0
- package/src/database/_generated/backend/postgresql/models.ts +2 -1
- package/src/database/_generated/backend/sqlite/client.ts +9 -4
- package/src/database/_generated/backend/sqlite/internal/class.ts +146 -165
- package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +127 -40
- package/src/database/_generated/backend/sqlite/models/BackendUnlockMethod.ts +1154 -0
- package/src/database/_generated/backend/sqlite/models/Project.ts +2 -2
- package/src/database/_generated/backend/sqlite/models/ProjectSpace.ts +7 -1
- package/src/database/_generated/backend/sqlite/models/UserWorkspaceLayout.ts +1065 -0
- package/src/database/_generated/backend/sqlite/models.ts +2 -1
- package/src/database/_generated/project/commonInputTypes.ts +26 -26
- package/src/database/_generated/project/internal/class.ts +7 -8
- package/src/database/_generated/project/internal/prismaNamespace.ts +8 -7
- package/src/database/_generated/project/models/ApiKey.ts +2 -0
- package/src/database/_generated/project/models/Artifact.ts +2 -2
- package/src/database/_generated/project/models/InstanceCustomStatus.ts +1 -1
- package/src/database/_generated/project/models/OperationLog.ts +49 -1
- package/src/database/_generated/project/models/UnlockMethod.ts +2 -2
- package/src/database/_generated/project/models/UserCompositeViewport.ts +16 -14
- package/src/database/_generated/project/models/UserProjectViewport.ts +11 -9
- package/src/database/_generated/project/models/WorkerVersion.ts +1 -5
- package/src/database/abstractions.ts +25 -3
- package/src/database/factory.ts +5 -6
- package/src/database/local/backend.ts +148 -18
- package/src/database/manager.ts +30 -2
- package/src/database/prisma.ts +1 -0
- package/src/orchestrator/operation-plan.ts +0 -19
- package/src/orchestrator/operation.ts +21 -4
- package/src/services.ts +12 -3
- package/src/shared/models/backend/unlock-method.ts +7 -13
- package/src/shared/models/errors.ts +14 -0
- package/src/shared/models/prisma.ts +10 -2
- package/src/test-utils/database.ts +34 -6
- package/dist/chunk-GJTMJQUW.js.map +0 -1
- package/prisma/backend/sqlite/migrations/20250817070609_initiial/migration.sql +0 -34
- package/prisma/backend/sqlite/migrations/20250817104948_add_fields/migration.sql +0 -59
- package/prisma/backend/sqlite/migrations/20250818083106_a/migration.sql +0 -19
- package/prisma/backend/sqlite/migrations/20250818101945_hi/migration.sql +0 -1
- package/prisma/backend/sqlite/migrations/20250819082315_a/migration.sql +0 -5
- package/prisma/project/migrations/20250816082523_test/migration.sql +0 -72
- package/prisma/project/migrations/20250818065643_update/migration.sql +0 -42
- package/prisma/project/migrations/20250818070758_a/migration.sql +0 -8
- package/prisma/project/migrations/20250818070913_a/migration.sql +0 -8
- package/prisma/project/migrations/20250818082720_add_motels/migration.sql +0 -11
- package/prisma/project/migrations/20250818112523_hello/migration.sql +0 -35
- package/prisma/project/migrations/20250819082305_a/migration.sql +0 -14
- package/prisma/project/migrations/20250819165004_add_missing_fields/migration.sql +0 -216
- package/prisma/project/migrations/20250819171309_a/migration.sql +0 -22
- package/prisma/project/migrations/20250820113949_a/migration.sql +0 -66
- package/prisma/project/migrations/20250820144256_b/migration.sql +0 -31
- package/prisma/project/migrations/20250820145547_a/migration.sql +0 -24
- package/prisma/project/migrations/20250820182517_b/migration.sql +0 -2
- package/prisma/project/migrations/20250821172324_a/migration.sql +0 -2
- package/prisma/project/migrations/20250822081339_a/migration.sql +0 -219
- package/prisma/project/migrations/20250822083742_b/migration.sql +0 -1
- package/prisma/project/migrations/20250822105134_boom/migration.sql +0 -1
- package/prisma/project/migrations/20250822141028_b/migration.sql +0 -1
- package/prisma/project/migrations/20250822142342_b/migration.sql +0 -16
- package/prisma/project/migrations/20250824072720_a/migration.sql +0 -1
- package/prisma/project/migrations/20250824093656_b/migration.sql +0 -21
- package/prisma/project/migrations/20250825082518_a/migration.sql +0 -1
- package/prisma/project/migrations/20250825085343_b/migration.sql +0 -1
- package/prisma/project/migrations/20250825091312_a/migration.sql +0 -1
- package/prisma/project/migrations/20250903095431_hi/migration.sql +0 -44
- package/prisma/project/migrations/20250903174255_a/migration.sql +0 -24
- package/prisma/project/migrations/20250908095205_hi/migration.sql +0 -18
- package/prisma/project/migrations/20250909155857_hi/migration.sql +0 -15
- package/prisma/project/migrations/20250921092621_b/migration.sql +0 -33
- package/prisma/project/migrations/20250921093911_b/migration.sql +0 -1
- package/src/database/_generated/backend/postgresql/models/UserWorkspaseLayout.ts +0 -1065
- 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
|
+
}
|
package/src/business/index.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|
30
|
-
* const
|
|
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
|
|
42
|
+
* Model UserWorkspaceLayout
|
|
43
43
|
*
|
|
44
44
|
*/
|
|
45
|
-
export type
|
|
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
|
|