@highstate/backend-api 0.20.0 → 0.26.0

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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "sourceHashes": {
3
- "./dist/index.js": 1785693892
3
+ "./dist/index.js": 575062251
4
4
  }
5
5
  }
package/dist/index.js CHANGED
@@ -1,14 +1,18 @@
1
- import { rm } from 'node:fs/promises';
2
- import { InstanceServiceDefinition } from '@highstate/api/instance.v1';
3
- import { SecretServiceDefinition } from '@highstate/api/secret.v1';
4
- import { WorkerServiceDefinition } from '@highstate/api/worker.v1';
5
- import { createServer } from 'nice-grpc';
6
- import { AccessError, instanceCustomStatusInputSchema } from '@highstate/backend/shared';
7
- import { z, commonObjectMetaSchema, serviceAccountMetaSchema } from '@highstate/contract';
8
- import { ServerError, Status } from 'nice-grpc-common';
9
- import { isAbortError } from 'abort-controller-x';
10
-
1
+ // @bun
11
2
  // src/index.ts
3
+ import { rm } from "fs/promises";
4
+ import { InstanceServiceDefinition } from "@highstate/api/instance.v1";
5
+ import { PanelServiceDefinition } from "@highstate/api/panel.v1";
6
+ import { SecretServiceDefinition } from "@highstate/api/secret.v1";
7
+ import { WorkerServiceDefinition } from "@highstate/api/worker.v1";
8
+ import { createServer } from "nice-grpc";
9
+
10
+ // src/handlers/instance.ts
11
+ import { instanceCustomStatusInputSchema } from "@highstate/backend/shared";
12
+ import { z } from "@highstate/contract";
13
+
14
+ // src/shared/authentication.ts
15
+ import { ServerError, Status } from "nice-grpc-common";
12
16
  async function authenticate(services, context) {
13
17
  const token = context.metadata.get("api-key");
14
18
  if (!token) {
@@ -21,34 +25,36 @@ async function authenticate(services, context) {
21
25
  const apiKey = await services.apiKeyService.getApiKeyByToken(projectId, token);
22
26
  return [projectId, apiKey];
23
27
  }
28
+ // src/shared/error-handling.ts
29
+ import { AccessError } from "@highstate/backend/shared";
30
+ import { isAbortError } from "abort-controller-x";
31
+ import { ServerError as ServerError2, Status as Status2 } from "nice-grpc-common";
24
32
  function createErrorHandlingMiddleware(services) {
25
33
  return async function* errorHandlingMiddleware(call, context) {
26
34
  try {
27
35
  return yield* call.next(call.request, context);
28
36
  } catch (error) {
29
- if (error instanceof ServerError || isAbortError(error)) {
37
+ if (error instanceof ServerError2 || isAbortError(error)) {
30
38
  throw error;
31
39
  }
32
40
  if (error instanceof AccessError) {
33
41
  services.logger.info({ error }, "access denied");
34
- throw new ServerError(Status.UNAUTHENTICATED, "Access denied");
42
+ throw new ServerError2(Status2.UNAUTHENTICATED, "Access denied");
35
43
  }
36
44
  services.logger.error({ error }, "unexpected error");
37
- throw new ServerError(Status.INTERNAL, "An unexpected error occurred");
45
+ throw new ServerError2(Status2.INTERNAL, "An unexpected error occurred");
38
46
  }
39
47
  };
40
48
  }
49
+ // src/shared/validation.ts
50
+ import { ServerError as ServerError3, Status as Status3 } from "nice-grpc-common";
41
51
  function parseArgument(request, argumentName, schema) {
42
52
  const result = schema.safeParse(request[argumentName]);
43
53
  if (!result.success) {
44
- throw new ServerError(
45
- Status.INVALID_ARGUMENT,
46
- `Invalid argument "${argumentName}": ${result.error.message}`
47
- );
54
+ throw new ServerError3(Status3.INVALID_ARGUMENT, `Invalid argument "${argumentName}": ${result.error.message}`);
48
55
  }
49
56
  return result.data;
50
57
  }
51
-
52
58
  // src/handlers/instance.ts
53
59
  function createInstanceService(services) {
54
60
  return {
@@ -56,28 +62,35 @@ function createInstanceService(services) {
56
62
  const [projectId, apiKey] = await authenticate(services, context);
57
63
  const stateId = parseArgument(request, "stateId", z.cuid2());
58
64
  const customStatus = parseArgument(request, "status", instanceCustomStatusInputSchema);
59
- await services.instanceStateService.updateCustomStatus(
60
- projectId,
61
- stateId,
62
- apiKey.serviceAccountId,
63
- customStatus
64
- );
65
+ await services.instanceStateService.updateCustomStatus(projectId, stateId, apiKey.serviceAccountId, customStatus);
65
66
  return {};
66
67
  },
67
68
  async removeCustomStatus(request, context) {
68
69
  const [projectId, apiKey] = await authenticate(services, context);
69
70
  const stateId = parseArgument(request, "stateId", z.cuid2());
70
- await services.instanceStateService.removeCustomStatus(
71
- projectId,
72
- stateId,
73
- apiKey.serviceAccountId,
74
- request.statusName
75
- );
71
+ await services.instanceStateService.removeCustomStatus(projectId, stateId, apiKey.serviceAccountId, request.statusName);
76
72
  return {};
77
73
  }
78
74
  };
79
75
  }
80
76
 
77
+ // src/handlers/panel.ts
78
+ import { panelInputSchema } from "@highstate/backend/shared";
79
+ import { z as z2 } from "@highstate/contract";
80
+ function createPanelService(services) {
81
+ return {
82
+ async setUnitPanels(request, context) {
83
+ const [projectId, apiKey] = await authenticate(services, context);
84
+ const workerVersionId = parseArgument(request, "workerVersionId", z2.cuid2());
85
+ const workerInstanceId = parseArgument(request, "workerInstanceId", z2.cuid2());
86
+ const stateId = parseArgument(request, "stateId", z2.cuid2());
87
+ const panels = parseArgument(request, "panels", z2.array(panelInputSchema));
88
+ const panelIds = await services.panelService.setUnitPanels(projectId, stateId, apiKey.id, workerVersionId, panels, workerInstanceId);
89
+ return { panelIds };
90
+ }
91
+ };
92
+ }
93
+
81
94
  // src/handlers/secret.ts
82
95
  function createSecretService(services) {
83
96
  return {
@@ -87,75 +100,83 @@ function createSecretService(services) {
87
100
  }
88
101
  };
89
102
  }
90
- var workerMetaUpdatePayloadSchema = z.union([
103
+
104
+ // src/handlers/worker.ts
105
+ import { commonObjectMetaSchema, serviceAccountMetaSchema, z as z3 } from "@highstate/contract";
106
+ var workerMetaUpdatePayloadSchema = z3.union([
91
107
  commonObjectMetaSchema,
92
- z.object({
108
+ z3.object({
93
109
  workerMeta: commonObjectMetaSchema,
94
110
  serviceAccountMeta: serviceAccountMetaSchema
95
111
  })
96
112
  ]);
97
113
  function createWorkerService(services) {
98
114
  return {
99
- async *connect(request, context) {
100
- const [projectId] = await authenticate(services, context);
101
- const workerVersionId = parseArgument(request, "workerVersionId", z.cuid2());
102
- services.workerManager.setWorkerRunning(projectId, workerVersionId);
115
+ async* connect(request, context) {
116
+ const [projectId, apiKey] = await authenticate(services, context);
117
+ const workerVersionId = parseArgument(request, "workerVersionId", z3.cuid2());
118
+ const workerInstanceId = parseArgument(request, "workerInstanceId", z3.cuid2());
119
+ const dataEndpoint = parseArgument(request, "dataEndpoint", z3.string().min(1));
103
120
  const database = await services.database.forProject(projectId);
104
- const existingRegistrations = await database.workerUnitRegistration.findMany({
105
- where: { workerVersionId },
106
- select: { stateId: true, params: true }
121
+ const workerVersion = await database.workerVersion.findFirst({
122
+ where: { id: workerVersionId, apiKeyId: apiKey.id },
123
+ select: { id: true }
107
124
  });
108
- for (const registration of existingRegistrations) {
109
- yield {
110
- event: {
111
- $case: "unitRegistration",
112
- value: {
113
- stateId: registration.stateId,
114
- params: registration.params
115
- }
116
- }
117
- };
125
+ if (!workerVersion) {
126
+ throw new Error(`Worker version "${workerVersionId}" is not owned by the API key`);
118
127
  }
119
- const registrationStream = await services.pubsubManager.subscribe([
120
- "worker-unit-registration",
121
- projectId,
122
- workerVersionId
123
- ]);
124
- for await (const event of registrationStream) {
125
- if (event.type === "registered") {
128
+ services.workerManager.assertWorkerInstance(projectId, workerVersionId, workerInstanceId);
129
+ services.panelEndpointManager.connect(projectId, workerVersionId, workerInstanceId, dataEndpoint);
130
+ try {
131
+ await services.workerManager.setWorkerRunning(projectId, workerVersionId, workerInstanceId);
132
+ const existingRegistrations = await database.workerUnitRegistration.findMany({
133
+ where: { workerVersionId },
134
+ select: { stateId: true, params: true }
135
+ });
136
+ for (const registration of existingRegistrations) {
126
137
  yield {
127
138
  event: {
128
139
  $case: "unitRegistration",
129
140
  value: {
130
- instanceId: event.instanceId,
131
- params: event.params
141
+ stateId: registration.stateId,
142
+ params: registration.params
132
143
  }
133
144
  }
134
145
  };
135
- } else if (event.type === "deregistered") {
136
- yield {
137
- event: {
138
- $case: "unitDeregistration",
139
- value: {
140
- instanceId: event.instanceId
146
+ }
147
+ const registrationStream = await services.pubsubManager.subscribe([
148
+ "worker-unit-registration",
149
+ projectId,
150
+ workerVersionId
151
+ ]);
152
+ for await (const event of registrationStream) {
153
+ if (event.type === "registered") {
154
+ yield {
155
+ event: {
156
+ $case: "unitRegistration",
157
+ value: { stateId: event.stateId, params: event.params }
141
158
  }
142
- }
143
- };
159
+ };
160
+ } else if (event.type === "deregistered") {
161
+ yield {
162
+ event: {
163
+ $case: "unitDeregistration",
164
+ value: { stateId: event.stateId }
165
+ }
166
+ };
167
+ }
144
168
  }
169
+ } finally {
170
+ services.panelEndpointManager.disconnect(workerInstanceId);
145
171
  }
146
172
  },
147
173
  async updateWorkerVersionMeta(request, context) {
148
174
  const [projectId] = await authenticate(services, context);
149
- const workerVersionId = parseArgument(request, "workerVersionId", z.string());
175
+ const workerVersionId = parseArgument(request, "workerVersionId", z3.string());
150
176
  const payload = parseArgument(request, "meta", workerMetaUpdatePayloadSchema);
151
177
  const workerMeta = "workerMeta" in payload ? payload.workerMeta : payload;
152
- const serviceAccountMeta = "serviceAccountMeta" in payload ? payload.serviceAccountMeta : void 0;
153
- await services.workerService.updateWorkerVersionMeta(
154
- projectId,
155
- workerVersionId,
156
- workerMeta,
157
- serviceAccountMeta
158
- );
178
+ const serviceAccountMeta = "serviceAccountMeta" in payload ? payload.serviceAccountMeta : undefined;
179
+ await services.workerService.updateWorkerVersionMeta(projectId, workerVersionId, workerMeta, serviceAccountMeta);
159
180
  return {};
160
181
  }
161
182
  };
@@ -166,6 +187,7 @@ async function startBackedApi(services) {
166
187
  const server = createServer();
167
188
  server.use(createErrorHandlingMiddleware(services));
168
189
  server.add(InstanceServiceDefinition, createInstanceService(services));
190
+ server.add(PanelServiceDefinition, createPanelService(services));
169
191
  server.add(SecretServiceDefinition, createSecretService(services));
170
192
  server.add(WorkerServiceDefinition, createWorkerService(services));
171
193
  const uid = process.geteuid?.();
@@ -173,7 +195,7 @@ async function startBackedApi(services) {
173
195
  try {
174
196
  await rm(sockPath, { force: true });
175
197
  } catch (error) {
176
- if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
198
+ if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT")) {
177
199
  services.logger.error({ error }, "failed to remove existing socket file");
178
200
  }
179
201
  }
@@ -181,7 +203,6 @@ async function startBackedApi(services) {
181
203
  services.workerManager.config.HIGHSTATE_WORKER_API_PATH = sockPath;
182
204
  services.logger.info(`api listening at %s`, sockPath);
183
205
  }
184
-
185
- export { startBackedApi };
186
- //# sourceMappingURL=index.js.map
187
- //# sourceMappingURL=index.js.map
206
+ export {
207
+ startBackedApi
208
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@highstate/backend-api",
3
- "version": "0.20.0",
3
+ "version": "0.26.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -21,8 +21,14 @@
21
21
  "platform"
22
22
  ]
23
23
  },
24
+ "scripts": {
25
+ "build": "highstate build",
26
+ "typecheck": "tsgo --noEmit --skipLibCheck",
27
+ "biome": "biome check --write --unsafe --error-on-warnings",
28
+ "biome:check": "biome check --error-on-warnings"
29
+ },
24
30
  "peerDependencies": {
25
- "@pulumi/pulumi": "3.220.0",
31
+ "@pulumi/pulumi": "3.232.0",
26
32
  "classic-level": "^2.0.0"
27
33
  },
28
34
  "peerDependenciesMeta": {
@@ -35,14 +41,14 @@
35
41
  },
36
42
  "dependencies": {
37
43
  "@bufbuild/protobuf": "^2.6.1",
44
+ "@highstate/api": "0.25.0",
45
+ "@highstate/backend": "0.25.0",
46
+ "@highstate/cli": "0.25.0",
47
+ "@highstate/contract": "0.25.0",
38
48
  "abort-controller-x": "^0.4.3",
39
49
  "nice-grpc": "^2.1.12",
40
50
  "nice-grpc-common": "^2.0.2",
41
- "zod": "^4.0.5",
42
- "@highstate/cli": "0.20.0",
43
- "@highstate/api": "0.20.0",
44
- "@highstate/backend": "0.20.0",
45
- "@highstate/contract": "0.20.0"
51
+ "zod": "^4.0.5"
46
52
  },
47
53
  "devDependencies": {
48
54
  "@biomejs/biome": "2.2.0",
@@ -50,11 +56,5 @@
50
56
  },
51
57
  "repository": {
52
58
  "url": "https://github.com/highstate-io/highstate"
53
- },
54
- "scripts": {
55
- "build": "highstate build",
56
- "typecheck": "tsgo --noEmit --skipLibCheck",
57
- "biome": "biome check --write --unsafe --error-on-warnings",
58
- "biome:check": "biome check --error-on-warnings"
59
59
  }
60
- }
60
+ }
@@ -0,0 +1,28 @@
1
+ import type { PanelServiceImplementation } from "@highstate/api/panel.v1"
2
+ import type { Services } from "@highstate/backend"
3
+ import { panelInputSchema } from "@highstate/backend/shared"
4
+ import { z } from "@highstate/contract"
5
+ import { authenticate, parseArgument } from "../shared"
6
+
7
+ export function createPanelService(services: Services): PanelServiceImplementation {
8
+ return {
9
+ async setUnitPanels(request, context) {
10
+ const [projectId, apiKey] = await authenticate(services, context)
11
+ const workerVersionId = parseArgument(request, "workerVersionId", z.cuid2())
12
+ const workerInstanceId = parseArgument(request, "workerInstanceId", z.cuid2())
13
+ const stateId = parseArgument(request, "stateId", z.cuid2())
14
+ const panels = parseArgument(request, "panels", z.array(panelInputSchema))
15
+
16
+ const panelIds = await services.panelService.setUnitPanels(
17
+ projectId,
18
+ stateId,
19
+ apiKey.id,
20
+ workerVersionId,
21
+ panels,
22
+ workerInstanceId,
23
+ )
24
+
25
+ return { panelIds }
26
+ },
27
+ }
28
+ }
@@ -14,62 +14,69 @@ const workerMetaUpdatePayloadSchema = z.union([
14
14
  export function createWorkerService(services: Services): WorkerServiceImplementation {
15
15
  return {
16
16
  async *connect(request, context) {
17
- const [projectId] = await authenticate(services, context)
17
+ const [projectId, apiKey] = await authenticate(services, context)
18
18
 
19
19
  const workerVersionId = parseArgument(request, "workerVersionId", z.cuid2())
20
-
21
- // set worker as running
22
- services.workerManager.setWorkerRunning(projectId, workerVersionId)
23
-
24
- // get existing registrations for this worker version
20
+ const workerInstanceId = parseArgument(request, "workerInstanceId", z.cuid2())
21
+ const dataEndpoint = parseArgument(request, "dataEndpoint", z.string().min(1))
25
22
  const database = await services.database.forProject(projectId)
26
- const existingRegistrations = await database.workerUnitRegistration.findMany({
27
- where: { workerVersionId },
28
- select: { stateId: true, params: true },
23
+ const workerVersion = await database.workerVersion.findFirst({
24
+ where: { id: workerVersionId, apiKeyId: apiKey.id },
25
+ select: { id: true },
29
26
  })
30
-
31
- // emit existing registrations
32
- for (const registration of existingRegistrations) {
33
- yield {
34
- event: {
35
- $case: "unitRegistration",
36
- value: {
37
- stateId: registration.stateId,
38
- params: registration.params,
39
- },
40
- },
41
- }
27
+ if (!workerVersion) {
28
+ throw new Error(`Worker version "${workerVersionId}" is not owned by the API key`)
42
29
  }
43
30
 
44
- // subscribe to new registration events for this worker version
45
- const registrationStream = await services.pubsubManager.subscribe([
46
- "worker-unit-registration",
31
+ services.workerManager.assertWorkerInstance(projectId, workerVersionId, workerInstanceId)
32
+ services.panelEndpointManager.connect(
47
33
  projectId,
48
34
  workerVersionId,
49
- ])
35
+ workerInstanceId,
36
+ dataEndpoint,
37
+ )
50
38
 
51
- // emit new registration/deregistration events
52
- for await (const event of registrationStream) {
53
- if (event.type === "registered") {
39
+ try {
40
+ await services.workerManager.setWorkerRunning(projectId, workerVersionId, workerInstanceId)
41
+ const existingRegistrations = await database.workerUnitRegistration.findMany({
42
+ where: { workerVersionId },
43
+ select: { stateId: true, params: true },
44
+ })
45
+ for (const registration of existingRegistrations) {
54
46
  yield {
55
47
  event: {
56
48
  $case: "unitRegistration",
57
49
  value: {
58
- instanceId: event.instanceId,
59
- params: event.params,
50
+ stateId: registration.stateId,
51
+ params: registration.params,
60
52
  },
61
53
  },
62
54
  }
63
- } else if (event.type === "deregistered") {
64
- yield {
65
- event: {
66
- $case: "unitDeregistration",
67
- value: {
68
- instanceId: event.instanceId,
55
+ }
56
+ const registrationStream = await services.pubsubManager.subscribe([
57
+ "worker-unit-registration",
58
+ projectId,
59
+ workerVersionId,
60
+ ])
61
+ for await (const event of registrationStream) {
62
+ if (event.type === "registered") {
63
+ yield {
64
+ event: {
65
+ $case: "unitRegistration",
66
+ value: { stateId: event.stateId, params: event.params },
69
67
  },
70
- },
68
+ }
69
+ } else if (event.type === "deregistered") {
70
+ yield {
71
+ event: {
72
+ $case: "unitDeregistration",
73
+ value: { stateId: event.stateId },
74
+ },
75
+ }
71
76
  }
72
77
  }
78
+ } finally {
79
+ services.panelEndpointManager.disconnect(workerInstanceId)
73
80
  }
74
81
  },
75
82
 
package/src/index.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import type { Services } from "@highstate/backend"
2
2
  import { rm } from "node:fs/promises"
3
3
  import { InstanceServiceDefinition } from "@highstate/api/instance.v1"
4
+ import { PanelServiceDefinition } from "@highstate/api/panel.v1"
4
5
  import { SecretServiceDefinition } from "@highstate/api/secret.v1"
5
6
  import { WorkerServiceDefinition } from "@highstate/api/worker.v1"
6
7
  import { createServer } from "nice-grpc"
7
8
  import { createInstanceService } from "./handlers/instance"
9
+ import { createPanelService } from "./handlers/panel"
8
10
  import { createSecretService } from "./handlers/secret"
9
11
  import { createWorkerService } from "./handlers/worker"
10
12
  import { createErrorHandlingMiddleware } from "./shared"
@@ -14,6 +16,7 @@ export async function startBackedApi(services: Services) {
14
16
  server.use(createErrorHandlingMiddleware(services))
15
17
 
16
18
  server.add(InstanceServiceDefinition, createInstanceService(services))
19
+ server.add(PanelServiceDefinition, createPanelService(services))
17
20
  server.add(SecretServiceDefinition, createSecretService(services))
18
21
  server.add(WorkerServiceDefinition, createWorkerService(services))
19
22
 
@@ -29,7 +32,6 @@ export async function startBackedApi(services: Services) {
29
32
  }
30
33
 
31
34
  await server.listen(`unix:${sockPath}`)
32
-
33
35
  services.workerManager.config.HIGHSTATE_WORKER_API_PATH = sockPath
34
36
  services.logger.info(`api listening at %s`, sockPath)
35
37
  }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Exeteres
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
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","z"],"mappings":"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","file":"index.js","sourcesContent":["import type { ApiKey, Services } from \"@highstate/backend\"\nimport { type CallContext, ServerError, Status } from \"nice-grpc-common\"\n\nexport async function authenticate(\n services: Services,\n context: CallContext,\n): Promise<[projectId: string, apiKey: ApiKey]> {\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 { AccessError } from \"@highstate/backend/shared\"\nimport { isAbortError } from \"abort-controller-x\"\nimport { type CallContext, ServerError, type ServerMiddlewareCall, Status } from \"nice-grpc-common\"\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 { InstanceServiceImplementation } from \"@highstate/api/instance.v1\"\nimport type { Services } from \"@highstate/backend\"\nimport { instanceCustomStatusInputSchema } from \"@highstate/backend/shared\"\nimport { z } from \"@highstate/contract\"\nimport { authenticate, parseArgument } from \"../shared\"\n\nexport function createInstanceService(services: Services): InstanceServiceImplementation {\n return {\n async updateCustomStatus(request, context) {\n const [projectId, apiKey] = await authenticate(services, context)\n\n // TODO: validate instance access\n\n const stateId = parseArgument(request, \"stateId\", z.cuid2())\n const customStatus = parseArgument(request, \"status\", instanceCustomStatusInputSchema)\n\n await services.instanceStateService.updateCustomStatus(\n projectId,\n stateId,\n apiKey.serviceAccountId,\n customStatus,\n )\n\n return {}\n },\n\n async removeCustomStatus(request, context) {\n const [projectId, apiKey] = await authenticate(services, context)\n\n // TODO: validate instance access\n\n const stateId = parseArgument(request, \"stateId\", z.cuid2())\n\n await services.instanceStateService.removeCustomStatus(\n projectId,\n stateId,\n apiKey.serviceAccountId,\n request.statusName,\n )\n\n return {}\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 throw new Error(\"Not implemented\")\n\n // return {\n // content,\n // }\n },\n }\n}\n","import type { WorkerServiceImplementation } from \"@highstate/api/worker.v1\"\nimport type { Services } from \"@highstate/backend\"\nimport { commonObjectMetaSchema, serviceAccountMetaSchema, z } from \"@highstate/contract\"\nimport { authenticate, parseArgument } from \"../shared\"\n\nconst workerMetaUpdatePayloadSchema = z.union([\n commonObjectMetaSchema,\n z.object({\n workerMeta: commonObjectMetaSchema,\n serviceAccountMeta: serviceAccountMetaSchema,\n }),\n])\n\nexport function createWorkerService(services: Services): WorkerServiceImplementation {\n return {\n async *connect(request, context) {\n const [projectId] = await authenticate(services, context)\n\n const workerVersionId = parseArgument(request, \"workerVersionId\", z.cuid2())\n\n // set worker as running\n services.workerManager.setWorkerRunning(projectId, workerVersionId)\n\n // get existing registrations for this worker version\n const database = await services.database.forProject(projectId)\n const existingRegistrations = await database.workerUnitRegistration.findMany({\n where: { workerVersionId },\n select: { stateId: true, params: true },\n })\n\n // emit existing registrations\n for (const registration of existingRegistrations) {\n yield {\n event: {\n $case: \"unitRegistration\",\n value: {\n stateId: registration.stateId,\n params: registration.params,\n },\n },\n }\n }\n\n // subscribe to new registration events for this worker version\n const registrationStream = await services.pubsubManager.subscribe([\n \"worker-unit-registration\",\n projectId,\n workerVersionId,\n ])\n\n // emit new registration/deregistration events\n for await (const event of registrationStream) {\n if (event.type === \"registered\") {\n yield {\n event: {\n $case: \"unitRegistration\",\n value: {\n instanceId: event.instanceId,\n params: event.params,\n },\n },\n }\n } else if (event.type === \"deregistered\") {\n yield {\n event: {\n $case: \"unitDeregistration\",\n value: {\n instanceId: event.instanceId,\n },\n },\n }\n }\n }\n },\n\n async updateWorkerVersionMeta(request, context) {\n const [projectId] = await authenticate(services, context)\n\n const workerVersionId = parseArgument(request, \"workerVersionId\", z.string())\n const payload = parseArgument(request, \"meta\", workerMetaUpdatePayloadSchema)\n\n const workerMeta = \"workerMeta\" in payload ? payload.workerMeta : payload\n const serviceAccountMeta =\n \"serviceAccountMeta\" in payload ? payload.serviceAccountMeta : undefined\n\n await services.workerService.updateWorkerVersionMeta(\n projectId,\n workerVersionId,\n workerMeta,\n serviceAccountMeta,\n )\n\n return {}\n },\n }\n}\n","import type { Services } from \"@highstate/backend\"\nimport { rm } from \"node:fs/promises\"\nimport { InstanceServiceDefinition } from \"@highstate/api/instance.v1\"\nimport { SecretServiceDefinition } from \"@highstate/api/secret.v1\"\nimport { WorkerServiceDefinition } from \"@highstate/api/worker.v1\"\nimport { createServer } from \"nice-grpc\"\nimport { createInstanceService } from \"./handlers/instance\"\nimport { createSecretService } from \"./handlers/secret\"\nimport { createWorkerService } from \"./handlers/worker\"\nimport { createErrorHandlingMiddleware } from \"./shared\"\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 try {\n await rm(sockPath, { force: true })\n } catch (error) {\n if (!(error instanceof Error && \"code\" in error && error.code === \"ENOENT\")) {\n services.logger.error({ error }, \"failed to remove existing socket file\")\n }\n }\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"]}