@highstate/backend-api 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.
@@ -0,0 +1,5 @@
1
+ {
2
+ "sourceHashes": {
3
+ "./dist/index.js": 3437641950
4
+ }
5
+ }
package/dist/index.js ADDED
@@ -0,0 +1,139 @@
1
+ import { createServer } from 'nice-grpc';
2
+ import '@highstate/backend';
3
+ import { InstanceServiceDefinition } from '@highstate/api/instance.v1';
4
+ import { SecretServiceDefinition } from '@highstate/api/secret.v1';
5
+ import { WorkerServiceDefinition } from '@highstate/api/worker.v1';
6
+ import { ServerError, Status } from 'nice-grpc-common';
7
+ import { isAbortError } from 'abort-controller-x';
8
+ import { AccessError, workerSchema, instanceCustomStatusSchema } from '@highstate/backend/shared';
9
+
10
+ // src/index.ts
11
+ async function authenticate(services, context) {
12
+ const token = context.metadata.get("api-key");
13
+ if (!token) {
14
+ throw new ServerError(Status.UNAUTHENTICATED, "No API key provided");
15
+ }
16
+ const projectId = context.metadata.get("project-id");
17
+ if (!projectId) {
18
+ throw new ServerError(Status.UNAUTHENTICATED, "No project ID provided");
19
+ }
20
+ const apiKey = await services.apiKeyService.getApiKeyByToken(projectId, token);
21
+ return [projectId, apiKey];
22
+ }
23
+ function createErrorHandlingMiddleware(services) {
24
+ return async function* errorHandlingMiddleware(call, context) {
25
+ try {
26
+ return yield* call.next(call.request, context);
27
+ } catch (error) {
28
+ if (error instanceof ServerError || isAbortError(error)) {
29
+ throw error;
30
+ }
31
+ if (error instanceof AccessError) {
32
+ services.logger.info({ error }, "access denied");
33
+ throw new ServerError(Status.UNAUTHENTICATED, "Access denied");
34
+ }
35
+ services.logger.error({ error }, "unexpected error");
36
+ throw new ServerError(Status.INTERNAL, "An unexpected error occurred");
37
+ }
38
+ };
39
+ }
40
+ function parseArgument(request, argumentName, schema) {
41
+ const result = schema.safeParse(request[argumentName]);
42
+ if (!result.success) {
43
+ throw new ServerError(
44
+ Status.INVALID_ARGUMENT,
45
+ `Invalid argument "${argumentName}": ${result.error.message}`
46
+ );
47
+ }
48
+ return result.data;
49
+ }
50
+ function createInstanceService(services) {
51
+ return {
52
+ async updateCustomStatus(request, context) {
53
+ const [projectId] = await authenticate(services, context);
54
+ const customStatus = parseArgument(request, "status", instanceCustomStatusSchema);
55
+ await services.instanceStateService.updateCustomStatus(
56
+ projectId,
57
+ request.instanceId,
58
+ customStatus
59
+ );
60
+ return {};
61
+ },
62
+ async removeCustomStatus(request, _context) {
63
+ await services.instanceStateService.removeCustomStatus(
64
+ "",
65
+ request.instanceId,
66
+ request.statusName
67
+ );
68
+ }
69
+ };
70
+ }
71
+
72
+ // src/handlers/secret.ts
73
+ function createSecretService(services) {
74
+ return {
75
+ async getSecretContent(request, context) {
76
+ const [projectId] = await authenticate(services, context);
77
+ const content = await services.stateManager.getSecretContentRepository(projectId).get(request.secretId);
78
+ return {
79
+ content
80
+ };
81
+ }
82
+ };
83
+ }
84
+ function createWorkerService(services) {
85
+ return {
86
+ async *connect(request, context) {
87
+ const [projectId] = await authenticate(services, context);
88
+ const workerId = parseArgument(request, "workerId", workerSchema.shape.id);
89
+ const registrationStream = services.pubsubManager.subscribe([
90
+ "worker-unit-registration",
91
+ projectId,
92
+ workerId
93
+ ]);
94
+ await services.workerManager.setWorkerRunning(projectId, workerId);
95
+ await services.workerManager.writeSystemMessage(projectId, workerId, "worker connected");
96
+ const registrations = await services.stateManager.getWorkerRegistrationIndexRepository(projectId, workerId).getAllItems();
97
+ for (const registration of registrations) {
98
+ yield {
99
+ event: {
100
+ $case: "unitRegistration",
101
+ value: {
102
+ instanceId: registration.id,
103
+ params: registration.params
104
+ }
105
+ }
106
+ };
107
+ }
108
+ for await (const event of registrationStream) {
109
+ yield {
110
+ event: {
111
+ $case: "unitRegistration",
112
+ value: {
113
+ instanceId: event.instanceId,
114
+ params: event.params
115
+ }
116
+ }
117
+ };
118
+ }
119
+ }
120
+ };
121
+ }
122
+
123
+ // src/index.ts
124
+ async function startBackedApi(services) {
125
+ const server = createServer();
126
+ server.use(createErrorHandlingMiddleware(services));
127
+ server.add(InstanceServiceDefinition, createInstanceService(services));
128
+ server.add(SecretServiceDefinition, createSecretService(services));
129
+ server.add(WorkerServiceDefinition, createWorkerService(services));
130
+ const uid = process.geteuid();
131
+ const sockPath = `/run/user/${uid}/highstate.sock`;
132
+ await server.listen(`unix:${sockPath}`);
133
+ services.workerManager.config.HIGHSTATE_WORKER_API_PATH = sockPath;
134
+ services.logger.info(`api listening at %s`, sockPath);
135
+ }
136
+
137
+ export { startBackedApi };
138
+ //# sourceMappingURL=index.js.map
139
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/authentication.ts","../src/shared/error-handling.ts","../src/shared/validation.ts","../src/handlers/instance.ts","../src/handlers/secret.ts","../src/handlers/worker.ts","../src/index.ts"],"names":["ServerError","Status"],"mappings":"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","file":"index.js","sourcesContent":["import type { Services } from \"@highstate/backend\"\nimport type { ProjectApiKey } from \"@highstate/backend/shared\"\nimport { ServerError, Status, type CallContext } from \"nice-grpc-common\"\n\nexport async function authenticate(\n services: Services,\n context: CallContext,\n): Promise<[projectId: string, apiKey: ProjectApiKey]> {\n const token = context.metadata.get(\"api-key\")\n if (!token) {\n throw new ServerError(Status.UNAUTHENTICATED, \"No API key provided\")\n }\n\n const projectId = context.metadata.get(\"project-id\")\n if (!projectId) {\n throw new ServerError(Status.UNAUTHENTICATED, \"No project ID provided\")\n }\n\n const apiKey = await services.apiKeyService.getApiKeyByToken(projectId, token)\n\n return [projectId, apiKey]\n}\n","import type { Services } from \"@highstate/backend\"\nimport { ServerError, Status, type CallContext, type ServerMiddlewareCall } from \"nice-grpc-common\"\nimport { isAbortError } from \"abort-controller-x\"\nimport { AccessError } from \"@highstate/backend/shared\"\n\nexport function createErrorHandlingMiddleware(services: Services) {\n return async function* errorHandlingMiddleware<TRequest, TResponse>(\n call: ServerMiddlewareCall<TRequest, TResponse>,\n context: CallContext,\n ) {\n try {\n return yield* call.next(call.request, context)\n } catch (error) {\n if (error instanceof ServerError || isAbortError(error)) {\n throw error\n }\n\n if (error instanceof AccessError) {\n services.logger.info({ error }, \"access denied\")\n throw new ServerError(Status.UNAUTHENTICATED, \"Access denied\")\n }\n\n services.logger.error({ error }, \"unexpected error\")\n throw new ServerError(Status.INTERNAL, \"An unexpected error occurred\")\n }\n }\n}\n","import type { z } from \"zod\"\nimport { ServerError, Status } from \"nice-grpc-common\"\n\nexport function parseArgument<\n TRequest,\n TArgumentName extends string & keyof TRequest,\n TSchema extends z.ZodType,\n>(request: TRequest, argumentName: TArgumentName, schema: TSchema): z.infer<TSchema> {\n const result = schema.safeParse(request[argumentName])\n if (!result.success) {\n throw new ServerError(\n Status.INVALID_ARGUMENT,\n `Invalid argument \"${argumentName}\": ${result.error.message}`,\n )\n }\n\n return result.data\n}\n","import type { Services } from \"@highstate/backend\"\nimport type { InstanceServiceImplementation } from \"@highstate/api/instance.v1\"\nimport { instanceCustomStatusSchema } from \"@highstate/backend/shared\"\nimport { authenticate, parseArgument } from \"../shared\"\n\nexport function createInstanceService(services: Services): InstanceServiceImplementation {\n return {\n async updateCustomStatus(request, context) {\n const [projectId] = await authenticate(services, context)\n\n // TODO: validate instance access\n\n const customStatus = parseArgument(request, \"status\", instanceCustomStatusSchema)\n\n await services.instanceStateService.updateCustomStatus(\n projectId,\n request.instanceId,\n customStatus,\n )\n\n return {}\n },\n\n async removeCustomStatus(request, _context) {\n await services.instanceStateService.removeCustomStatus(\n \"\",\n request.instanceId,\n request.statusName,\n )\n },\n }\n}\n","import type { SecretServiceImplementation } from \"@highstate/api/secret.v1\"\nimport type { Services } from \"@highstate/backend\"\nimport { authenticate } from \"../shared\"\n\nexport function createSecretService(services: Services): SecretServiceImplementation {\n return {\n async getSecretContent(request, context) {\n const [projectId] = await authenticate(services, context)\n\n // TODO: validate secret access\n\n const content = await services.stateManager\n .getSecretContentRepository(projectId)\n .get(request.secretId)\n\n return {\n content,\n }\n },\n }\n}\n","import type { Services } from \"@highstate/backend\"\nimport type { WorkerServiceImplementation } from \"@highstate/api/worker.v1\"\nimport { workerSchema } from \"@highstate/backend/shared\"\nimport { authenticate, parseArgument } from \"../shared\"\n\nexport function createWorkerService(services: Services): WorkerServiceImplementation {\n return {\n async *connect(request, context) {\n const [projectId] = await authenticate(services, context)\n\n // TODO: check worker access\n const workerId = parseArgument(request, \"workerId\", workerSchema.shape.id)\n\n const registrationStream = services.pubsubManager.subscribe([\n \"worker-unit-registration\",\n projectId,\n workerId,\n ])\n\n // update worker status\n await services.workerManager.setWorkerRunning(projectId, workerId)\n await services.workerManager.writeSystemMessage(projectId, workerId, \"worker connected\")\n\n // emit existing registrations\n const registrations = await services.stateManager\n .getWorkerRegistrationIndexRepository(projectId, workerId)\n .getAllItems()\n\n for (const registration of registrations) {\n yield {\n event: {\n $case: \"unitRegistration\",\n value: {\n instanceId: registration.id,\n params: registration.params,\n },\n },\n }\n }\n\n // emit new registrations\n for await (const event of registrationStream) {\n yield {\n event: {\n $case: \"unitRegistration\",\n value: {\n instanceId: event.instanceId,\n params: event.params,\n },\n },\n }\n }\n },\n }\n}\n","import { createServer } from \"nice-grpc\"\nimport { type Services } from \"@highstate/backend\"\nimport { InstanceServiceDefinition } from \"@highstate/api/instance.v1\"\nimport { SecretServiceDefinition } from \"@highstate/api/secret.v1\"\nimport { WorkerServiceDefinition } from \"@highstate/api/worker.v1\"\nimport { createErrorHandlingMiddleware } from \"./shared\"\nimport { createInstanceService } from \"./handlers/instance\"\nimport { createSecretService } from \"./handlers/secret\"\nimport { createWorkerService } from \"./handlers/worker\"\n\nexport async function startBackedApi(services: Services) {\n const server = createServer()\n server.use(createErrorHandlingMiddleware(services))\n\n server.add(InstanceServiceDefinition, createInstanceService(services))\n server.add(SecretServiceDefinition, createSecretService(services))\n server.add(WorkerServiceDefinition, createWorkerService(services))\n\n const uid = process.geteuid!()\n const sockPath = `/run/user/${uid}/highstate.sock`\n\n await server.listen(`unix:${sockPath}`)\n\n services.workerManager.config.HIGHSTATE_WORKER_API_PATH = sockPath\n services.logger.info(`api listening at %s`, sockPath)\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@highstate/backend-api",
3
+ "version": "0.9.16",
4
+ "type": "module",
5
+ "files": [
6
+ "dist",
7
+ "src",
8
+ "patches"
9
+ ],
10
+ "exports": {
11
+ ".": {
12
+ "types": "./src/index.ts",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "scripts": {
20
+ "build": "highstate build",
21
+ "generate-sdk": "./scripts/generate-sdk.sh"
22
+ },
23
+ "peerDependencies": {
24
+ "@pulumi/pulumi": "^3.163.0",
25
+ "classic-level": "^2.0.0"
26
+ },
27
+ "peerDependenciesMeta": {
28
+ "@pulumi/pulumi": {
29
+ "optional": true
30
+ },
31
+ "classic-level": {
32
+ "optional": true
33
+ }
34
+ },
35
+ "devDependencies": {
36
+ "ts-proto": "^2.7.5"
37
+ },
38
+ "dependencies": {
39
+ "@bufbuild/protobuf": "^2.6.1",
40
+ "@highstate/api": "^0.9.16",
41
+ "@highstate/backend": "^0.9.16",
42
+ "@highstate/cli": "^0.9.16",
43
+ "abort-controller-x": "^0.4.3",
44
+ "nice-grpc": "^2.1.12",
45
+ "nice-grpc-common": "^2.0.2",
46
+ "zod": "^4.0.5"
47
+ },
48
+ "gitHead": "458d6f1f9f6d4aec0ba75a2b2c4c01408cb9c8df"
49
+ }
@@ -0,0 +1,32 @@
1
+ import type { Services } from "@highstate/backend"
2
+ import type { InstanceServiceImplementation } from "@highstate/api/instance.v1"
3
+ import { instanceCustomStatusSchema } from "@highstate/backend/shared"
4
+ import { authenticate, parseArgument } from "../shared"
5
+
6
+ export function createInstanceService(services: Services): InstanceServiceImplementation {
7
+ return {
8
+ async updateCustomStatus(request, context) {
9
+ const [projectId] = await authenticate(services, context)
10
+
11
+ // TODO: validate instance access
12
+
13
+ const customStatus = parseArgument(request, "status", instanceCustomStatusSchema)
14
+
15
+ await services.instanceStateService.updateCustomStatus(
16
+ projectId,
17
+ request.instanceId,
18
+ customStatus,
19
+ )
20
+
21
+ return {}
22
+ },
23
+
24
+ async removeCustomStatus(request, _context) {
25
+ await services.instanceStateService.removeCustomStatus(
26
+ "",
27
+ request.instanceId,
28
+ request.statusName,
29
+ )
30
+ },
31
+ }
32
+ }
@@ -0,0 +1,21 @@
1
+ import type { SecretServiceImplementation } from "@highstate/api/secret.v1"
2
+ import type { Services } from "@highstate/backend"
3
+ import { authenticate } from "../shared"
4
+
5
+ export function createSecretService(services: Services): SecretServiceImplementation {
6
+ return {
7
+ async getSecretContent(request, context) {
8
+ const [projectId] = await authenticate(services, context)
9
+
10
+ // TODO: validate secret access
11
+
12
+ const content = await services.stateManager
13
+ .getSecretContentRepository(projectId)
14
+ .get(request.secretId)
15
+
16
+ return {
17
+ content,
18
+ }
19
+ },
20
+ }
21
+ }
@@ -0,0 +1,55 @@
1
+ import type { Services } from "@highstate/backend"
2
+ import type { WorkerServiceImplementation } from "@highstate/api/worker.v1"
3
+ import { workerSchema } from "@highstate/backend/shared"
4
+ import { authenticate, parseArgument } from "../shared"
5
+
6
+ export function createWorkerService(services: Services): WorkerServiceImplementation {
7
+ return {
8
+ async *connect(request, context) {
9
+ const [projectId] = await authenticate(services, context)
10
+
11
+ // TODO: check worker access
12
+ const workerId = parseArgument(request, "workerId", workerSchema.shape.id)
13
+
14
+ const registrationStream = services.pubsubManager.subscribe([
15
+ "worker-unit-registration",
16
+ projectId,
17
+ workerId,
18
+ ])
19
+
20
+ // update worker status
21
+ await services.workerManager.setWorkerRunning(projectId, workerId)
22
+ await services.workerManager.writeSystemMessage(projectId, workerId, "worker connected")
23
+
24
+ // emit existing registrations
25
+ const registrations = await services.stateManager
26
+ .getWorkerRegistrationIndexRepository(projectId, workerId)
27
+ .getAllItems()
28
+
29
+ for (const registration of registrations) {
30
+ yield {
31
+ event: {
32
+ $case: "unitRegistration",
33
+ value: {
34
+ instanceId: registration.id,
35
+ params: registration.params,
36
+ },
37
+ },
38
+ }
39
+ }
40
+
41
+ // emit new registrations
42
+ for await (const event of registrationStream) {
43
+ yield {
44
+ event: {
45
+ $case: "unitRegistration",
46
+ value: {
47
+ instanceId: event.instanceId,
48
+ params: event.params,
49
+ },
50
+ },
51
+ }
52
+ }
53
+ },
54
+ }
55
+ }
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { createServer } from "nice-grpc"
2
+ import { type Services } from "@highstate/backend"
3
+ import { InstanceServiceDefinition } from "@highstate/api/instance.v1"
4
+ import { SecretServiceDefinition } from "@highstate/api/secret.v1"
5
+ import { WorkerServiceDefinition } from "@highstate/api/worker.v1"
6
+ import { createErrorHandlingMiddleware } from "./shared"
7
+ import { createInstanceService } from "./handlers/instance"
8
+ import { createSecretService } from "./handlers/secret"
9
+ import { createWorkerService } from "./handlers/worker"
10
+
11
+ export async function startBackedApi(services: Services) {
12
+ const server = createServer()
13
+ server.use(createErrorHandlingMiddleware(services))
14
+
15
+ server.add(InstanceServiceDefinition, createInstanceService(services))
16
+ server.add(SecretServiceDefinition, createSecretService(services))
17
+ server.add(WorkerServiceDefinition, createWorkerService(services))
18
+
19
+ const uid = process.geteuid!()
20
+ const sockPath = `/run/user/${uid}/highstate.sock`
21
+
22
+ await server.listen(`unix:${sockPath}`)
23
+
24
+ services.workerManager.config.HIGHSTATE_WORKER_API_PATH = sockPath
25
+ services.logger.info(`api listening at %s`, sockPath)
26
+ }
@@ -0,0 +1,22 @@
1
+ import type { Services } from "@highstate/backend"
2
+ import type { ProjectApiKey } from "@highstate/backend/shared"
3
+ import { ServerError, Status, type CallContext } from "nice-grpc-common"
4
+
5
+ export async function authenticate(
6
+ services: Services,
7
+ context: CallContext,
8
+ ): Promise<[projectId: string, apiKey: ProjectApiKey]> {
9
+ const token = context.metadata.get("api-key")
10
+ if (!token) {
11
+ throw new ServerError(Status.UNAUTHENTICATED, "No API key provided")
12
+ }
13
+
14
+ const projectId = context.metadata.get("project-id")
15
+ if (!projectId) {
16
+ throw new ServerError(Status.UNAUTHENTICATED, "No project ID provided")
17
+ }
18
+
19
+ const apiKey = await services.apiKeyService.getApiKeyByToken(projectId, token)
20
+
21
+ return [projectId, apiKey]
22
+ }
@@ -0,0 +1,27 @@
1
+ import type { Services } from "@highstate/backend"
2
+ import { ServerError, Status, type CallContext, type ServerMiddlewareCall } from "nice-grpc-common"
3
+ import { isAbortError } from "abort-controller-x"
4
+ import { AccessError } from "@highstate/backend/shared"
5
+
6
+ export function createErrorHandlingMiddleware(services: Services) {
7
+ return async function* errorHandlingMiddleware<TRequest, TResponse>(
8
+ call: ServerMiddlewareCall<TRequest, TResponse>,
9
+ context: CallContext,
10
+ ) {
11
+ try {
12
+ return yield* call.next(call.request, context)
13
+ } catch (error) {
14
+ if (error instanceof ServerError || isAbortError(error)) {
15
+ throw error
16
+ }
17
+
18
+ if (error instanceof AccessError) {
19
+ services.logger.info({ error }, "access denied")
20
+ throw new ServerError(Status.UNAUTHENTICATED, "Access denied")
21
+ }
22
+
23
+ services.logger.error({ error }, "unexpected error")
24
+ throw new ServerError(Status.INTERNAL, "An unexpected error occurred")
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./authentication"
2
+ export * from "./error-handling"
3
+ export * from "./validation"
@@ -0,0 +1,18 @@
1
+ import type { z } from "zod"
2
+ import { ServerError, Status } from "nice-grpc-common"
3
+
4
+ export function parseArgument<
5
+ TRequest,
6
+ TArgumentName extends string & keyof TRequest,
7
+ TSchema extends z.ZodType,
8
+ >(request: TRequest, argumentName: TArgumentName, schema: TSchema): z.infer<TSchema> {
9
+ const result = schema.safeParse(request[argumentName])
10
+ if (!result.success) {
11
+ throw new ServerError(
12
+ Status.INVALID_ARGUMENT,
13
+ `Invalid argument "${argumentName}": ${result.error.message}`,
14
+ )
15
+ }
16
+
17
+ return result.data
18
+ }