@highstate/backend-api 0.26.0 → 0.28.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.
package/dist/index.js CHANGED
@@ -1,74 +1,1018 @@
1
1
  // @bun
2
2
  // src/index.ts
3
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";
4
+ import { createServer } from "http";
5
+ import { connectNodeAdapter } from "@connectrpc/connect-node";
6
+ import {
7
+ InstanceStateService,
8
+ LibraryService,
9
+ OperationService,
10
+ PanelService,
11
+ ProjectModelService,
12
+ ProjectService,
13
+ SecretService
14
+ } from "@highstate/api/v1";
15
+ import { WorkerService } from "@highstate/api/worker.v1";
9
16
 
10
- // src/handlers/instance.ts
11
- import { instanceCustomStatusInputSchema } from "@highstate/backend/shared";
12
- import { z } from "@highstate/contract";
17
+ // src/handlers/instance-state.ts
18
+ import { instanceCustomStatusInputSchema as instanceCustomStatusInputSchema2 } from "@highstate/backend/shared";
19
+ import { z as z2 } from "@highstate/contract";
13
20
 
21
+ // src/shared/api-error.ts
22
+ import { create } from "@bufbuild/protobuf";
23
+ import { ConnectError } from "@connectrpc/connect";
24
+ import { BadRequestSchema, ErrorInfoSchema } from "@highstate/api/v1";
25
+ function createApiError(options) {
26
+ const details = [
27
+ {
28
+ desc: ErrorInfoSchema,
29
+ value: create(ErrorInfoSchema, {
30
+ reason: options.reason,
31
+ domain: "highstate.io",
32
+ metadata: options.metadata
33
+ })
34
+ }
35
+ ];
36
+ if (options.fieldViolations?.length) {
37
+ details.push({
38
+ desc: BadRequestSchema,
39
+ value: create(BadRequestSchema, {
40
+ fieldViolations: options.fieldViolations.map((violation) => ({ ...violation }))
41
+ })
42
+ });
43
+ }
44
+ return new ConnectError(options.message, options.code, undefined, details);
45
+ }
14
46
  // src/shared/authentication.ts
15
- import { ServerError, Status } from "nice-grpc-common";
16
- async function authenticate(services, context) {
17
- const token = context.metadata.get("api-key");
18
- if (!token) {
19
- throw new ServerError(Status.UNAUTHENTICATED, "No API key provided");
47
+ import { Code as Code2 } from "@connectrpc/connect";
48
+ import { ProjectLockedError, ProjectNotFoundError } from "@highstate/backend/shared";
49
+
50
+ // src/shared/authorization-header.ts
51
+ import { Code } from "@connectrpc/connect";
52
+ function parseBearerToken(authorization) {
53
+ const match = /^Bearer ([^\s]+)$/i.exec(authorization ?? "");
54
+ if (!match || [...match[1]].some((character) => character.charCodeAt(0) <= 31 || character === "\x7F")) {
55
+ throw createApiError({
56
+ message: "Invalid authorization header",
57
+ code: Code.Unauthenticated,
58
+ reason: "AUTHORIZATION_HEADER_INVALID"
59
+ });
20
60
  }
21
- const projectId = context.metadata.get("project-id");
61
+ return match[1];
62
+ }
63
+
64
+ // src/shared/authentication.ts
65
+ async function authenticateBackend(services, context) {
66
+ const token = getBearerToken(context);
67
+ const apiKey = await services.apiKeyService.getBackendApiKeyByToken(token);
68
+ const roleBindings = await services.database.backend.serviceAccountBackendRoleBinding.findMany({
69
+ where: { serviceAccountId: apiKey.serviceAccountId },
70
+ include: { role: { select: { rules: true } } }
71
+ });
72
+ return {
73
+ realm: "backend",
74
+ subject: {
75
+ type: "service-account",
76
+ serviceAccountId: apiKey.serviceAccountId,
77
+ apiKeyId: apiKey.id
78
+ },
79
+ permissions: resolveBackendPermissions(roleBindings.flatMap((binding) => binding.role.rules.map((rule) => ({
80
+ permissions: rule.permissions,
81
+ restrictions: rule.restrictions ?? []
82
+ }))), apiKey.restrictionRules)
83
+ };
84
+ }
85
+ async function authenticateProject(services, request, context) {
86
+ const projectId = request.projectId;
22
87
  if (!projectId) {
23
- throw new ServerError(Status.UNAUTHENTICATED, "No project ID provided");
88
+ throw createApiError({
89
+ message: "No project ID provided",
90
+ code: Code2.InvalidArgument,
91
+ reason: "PROJECT_ID_REQUIRED",
92
+ fieldViolations: [
93
+ {
94
+ field: "project_id",
95
+ reason: "REQUIRED",
96
+ description: "The project ID is required"
97
+ }
98
+ ]
99
+ });
100
+ }
101
+ const project = await services.database.backend.project.findUnique({
102
+ where: { id: projectId },
103
+ select: { id: true }
104
+ });
105
+ if (!project) {
106
+ throw new ProjectNotFoundError(projectId);
107
+ }
108
+ if (!await services.projectUnlockService.checkProjectUnlocked(projectId)) {
109
+ throw new ProjectLockedError(projectId);
110
+ }
111
+ const token = getBearerToken(context);
112
+ const apiKey = await services.apiKeyService.getProjectCredentialByToken(projectId, token);
113
+ const projectDatabase = await services.database.forProject(projectId);
114
+ const roleBindings = await projectDatabase.serviceAccountRoleBinding.findMany({
115
+ where: { serviceAccountId: apiKey.serviceAccountId },
116
+ include: { role: { select: { rules: true } } }
117
+ });
118
+ return {
119
+ realm: "project",
120
+ projectId,
121
+ subject: {
122
+ type: "service-account",
123
+ serviceAccountId: apiKey.serviceAccountId,
124
+ apiKeyId: apiKey.id
125
+ },
126
+ permissions: resolveProjectPermissions(roleBindings.flatMap((binding) => binding.role.rules.map((rule) => ({
127
+ permissions: rule.permissions,
128
+ restrictions: rule.restrictions ?? []
129
+ }))), apiKey.restrictionRules)
130
+ };
131
+ }
132
+ function getBearerToken(context) {
133
+ return parseBearerToken(context.requestHeader.get("authorization"));
134
+ }
135
+ function resolveBackendPermissions(grants, restrictions) {
136
+ return resolvePermissions(grants, restrictions);
137
+ }
138
+ function resolveProjectPermissions(grants, restrictions) {
139
+ return resolvePermissions(grants, restrictions);
140
+ }
141
+ function resolvePermissions(grants, restrictions) {
142
+ const restrictionByPermission = new Map(restrictions.flatMap((rule) => rule.permissions.map((permission) => [permission, rule.restrictions ?? []])));
143
+ const permissions = new Map;
144
+ for (const grant of grants) {
145
+ for (const permission of grant.permissions) {
146
+ const keyRestrictions = restrictionByPermission.get(permission);
147
+ if (restrictions.length > 0 && !keyRestrictions) {
148
+ continue;
149
+ }
150
+ const permissionGrants = permissions.get(permission) ?? [];
151
+ permissionGrants.push({ restrictions: [...grant.restrictions, ...keyRestrictions ?? []] });
152
+ permissions.set(permission, permissionGrants);
153
+ }
154
+ }
155
+ return permissions;
156
+ }
157
+ // src/shared/conversion.ts
158
+ import { create as create2, fromJson, toJson } from "@bufbuild/protobuf";
159
+ import { timestampFromDate, ValueSchema } from "@bufbuild/protobuf/wkt";
160
+ import {
161
+ ComponentKind,
162
+ ComponentSchema,
163
+ EntitySchema,
164
+ EvaluationStatus,
165
+ HubSchema,
166
+ InstanceCustomStatusSchema,
167
+ InstanceOperationStatus,
168
+ InstanceSchema,
169
+ InstanceSource,
170
+ InstanceStateSchema,
171
+ InstanceStatus,
172
+ LibrarySchema,
173
+ OperationLogSchema,
174
+ OperationPhaseSchema,
175
+ OperationPhaseType,
176
+ OperationSchema,
177
+ OperationStatus,
178
+ OperationType,
179
+ ProjectModelSchema,
180
+ ProjectSchema
181
+ } from "@highstate/api/v1";
182
+ import {
183
+ instanceCustomStatusInputSchema,
184
+ operationMetaSchema,
185
+ operationOptionsSchema,
186
+ operationPhaseSchema
187
+ } from "@highstate/backend/shared";
188
+ import {
189
+ instanceInputSchema,
190
+ instanceModelSchema,
191
+ z
192
+ } from "@highstate/contract";
193
+ var jsonValueSchema = z.lazy(() => z.union([
194
+ z.string(),
195
+ z.number(),
196
+ z.boolean(),
197
+ z.null(),
198
+ z.array(jsonValueSchema),
199
+ z.record(z.string(), jsonValueSchema)
200
+ ]));
201
+ var jsonObjectSchema = z.record(z.string(), jsonValueSchema);
202
+ function toProject(project) {
203
+ return create2(ProjectSchema, {
204
+ id: project.id,
205
+ name: project.name,
206
+ meta: project.meta,
207
+ spaceId: project.spaceId,
208
+ modelStorageId: project.modelStorageId,
209
+ libraryId: project.libraryId,
210
+ pulumiBackendId: project.pulumiBackendId,
211
+ createdAt: toTimestamp(project.createdAt),
212
+ updatedAt: toTimestamp(project.updatedAt)
213
+ });
214
+ }
215
+ function toProjectModel(model) {
216
+ return create2(ProjectModelSchema, {
217
+ instances: [...model.instances, ...model.virtualInstances, ...model.ghostInstances].map(toInstance),
218
+ hubs: model.hubs.map(toHub)
219
+ });
220
+ }
221
+ function toInstance(instance) {
222
+ return create2(InstanceSchema, {
223
+ id: instance.id,
224
+ kind: toComponentKind(instance.kind),
225
+ type: instance.type,
226
+ name: instance.name,
227
+ arguments: Object.entries(instance.args ?? {}).map(([key, value]) => ({
228
+ key,
229
+ value: fromJson(ValueSchema, value)
230
+ })),
231
+ inputs: toInstanceReferenceMap(instance.inputs),
232
+ hubInputs: Object.fromEntries(Object.entries(instance.hubInputs ?? {}).map(([key, values]) => [
233
+ key,
234
+ { values: values.map((value) => ({ hubId: value.hubId })) }
235
+ ])),
236
+ injectionInputs: (instance.injectionInputs ?? []).map((value) => ({ hubId: value.hubId })),
237
+ position: instance.position ?? undefined,
238
+ resolvedInputs: toInstanceReferenceMap(instance.resolvedInputs),
239
+ parentId: instance.parentId,
240
+ outputs: toInstanceReferenceMap(instance.outputs),
241
+ resolvedOutputs: toInstanceReferenceMap(instance.resolvedOutputs)
242
+ });
243
+ }
244
+ function fromInstance(instance) {
245
+ return instanceModelSchema.parse({
246
+ id: instance.id,
247
+ kind: fromComponentKind(instance.kind),
248
+ type: instance.type,
249
+ name: instance.name,
250
+ args: Object.fromEntries(instance.arguments.map((argument) => [
251
+ argument.key,
252
+ argument.value ? toJson(ValueSchema, argument.value) : null
253
+ ])),
254
+ inputs: fromInstanceReferenceMap(instance.inputs),
255
+ hubInputs: Object.fromEntries(Object.entries(instance.hubInputs).map(([key, list]) => [
256
+ key,
257
+ list.values.map((value) => ({ hubId: value.hubId }))
258
+ ])),
259
+ injectionInputs: instance.injectionInputs.map((value) => ({ hubId: value.hubId })),
260
+ position: instance.position
261
+ });
262
+ }
263
+ function toHub(hub) {
264
+ return create2(HubSchema, {
265
+ id: hub.id,
266
+ position: hub.position ?? undefined,
267
+ inputs: hub.inputs ?? [],
268
+ injectionInputs: (hub.injectionInputs ?? []).map((value) => ({ hubId: value.hubId }))
269
+ });
270
+ }
271
+ function fromHub(hub) {
272
+ return {
273
+ id: hub.id,
274
+ position: hub.position,
275
+ inputs: hub.inputs.map(fromInstanceReference),
276
+ injectionInputs: hub.injectionInputs.map((value) => ({ hubId: value.hubId }))
277
+ };
278
+ }
279
+ function toInstancePatch(instance, paths) {
280
+ const patch = {};
281
+ for (const path of paths) {
282
+ switch (path) {
283
+ case "arguments":
284
+ patch.args = Object.fromEntries(instance.arguments.map((argument) => [
285
+ argument.key,
286
+ argument.value ? toJson(ValueSchema, argument.value) : null
287
+ ]));
288
+ break;
289
+ case "inputs":
290
+ patch.inputs = fromInstanceReferenceMap(instance.inputs);
291
+ break;
292
+ case "hub_inputs":
293
+ patch.hubInputs = Object.fromEntries(Object.entries(instance.hubInputs).map(([key, list]) => [
294
+ key,
295
+ list.values.map((value) => ({ hubId: value.hubId }))
296
+ ]));
297
+ break;
298
+ case "injection_inputs":
299
+ patch.injectionInputs = instance.injectionInputs.map((value) => ({ hubId: value.hubId }));
300
+ break;
301
+ case "position":
302
+ patch.position = instance.position ? { x: instance.position.x, y: instance.position.y } : null;
303
+ break;
304
+ case "position.x":
305
+ patch.position = { x: instance.position?.x ?? 0 };
306
+ break;
307
+ case "position.y":
308
+ patch.position = { y: instance.position?.y ?? 0 };
309
+ break;
310
+ }
311
+ }
312
+ return patch;
313
+ }
314
+ function toHubPatch(hub, paths) {
315
+ const patch = {};
316
+ for (const path of paths) {
317
+ switch (path) {
318
+ case "position":
319
+ patch.position = hub.position ? { x: hub.position.x, y: hub.position.y } : null;
320
+ break;
321
+ case "position.x":
322
+ patch.position = { x: hub.position?.x ?? 0 };
323
+ break;
324
+ case "position.y":
325
+ patch.position = { y: hub.position?.y ?? 0 };
326
+ break;
327
+ case "inputs":
328
+ patch.inputs = hub.inputs.map(fromInstanceReference);
329
+ break;
330
+ case "injection_inputs":
331
+ patch.injectionInputs = hub.injectionInputs.map((value) => ({ hubId: value.hubId }));
332
+ break;
333
+ }
334
+ }
335
+ return patch;
336
+ }
337
+ function toInstanceState(state) {
338
+ const model = state.model ? instanceModelSchema.parse(state.model) : undefined;
339
+ const resolvedInputs = state.resolvedInputs ? fromUnknownInstanceReferenceMap(state.resolvedInputs) : undefined;
340
+ return create2(InstanceStateSchema, {
341
+ id: state.id,
342
+ instanceId: state.instanceId,
343
+ status: toInstanceStatus(state.status),
344
+ source: toInstanceSource(state.source),
345
+ kind: toComponentKind(state.kind),
346
+ parentInstanceId: state.parentInstanceId ?? undefined,
347
+ evaluationState: state.evaluationState ? {
348
+ status: toEvaluationStatus(state.evaluationState.status),
349
+ message: state.evaluationState.message ?? undefined,
350
+ model: state.evaluationState.model ? toInstance(instanceModelSchema.parse(state.evaluationState.model)) : undefined,
351
+ evaluatedAt: toTimestamp(state.evaluationState.evaluatedAt)
352
+ } : undefined,
353
+ lastOperationState: state.lastOperationState ? {
354
+ operationId: state.lastOperationState.operationId,
355
+ stateId: state.lastOperationState.stateId,
356
+ status: toInstanceOperationStatus(state.lastOperationState.status),
357
+ currentResourceCount: state.lastOperationState.currentResourceCount ?? undefined,
358
+ totalResourceCount: state.lastOperationState.totalResourceCount ?? undefined,
359
+ model: toInstance(instanceModelSchema.parse(state.lastOperationState.model)),
360
+ resolvedInputs: toInstanceReferenceMap(fromUnknownInstanceReferenceMap(state.lastOperationState.resolvedInputs)),
361
+ startedAt: toNullableTimestamp(state.lastOperationState.startedAt),
362
+ finishedAt: toNullableTimestamp(state.lastOperationState.finishedAt)
363
+ } : undefined,
364
+ terminalIds: state.terminalIds ?? [],
365
+ pageIds: state.pageIds ?? [],
366
+ panelIds: state.panelIds ?? [],
367
+ secretNames: state.secretNames ?? [],
368
+ customStatuses: (state.customStatuses ?? []).map(toInstanceCustomStatus),
369
+ currentResourceCount: state.currentResourceCount ?? undefined,
370
+ hasResourceHooks: state.hasResourceHooks,
371
+ statusFields: state.statusFields ? fromJson(ValueSchema, jsonValueSchema.parse(state.statusFields)) : undefined,
372
+ model: model ? toInstance(model) : undefined,
373
+ resolvedInputs: toInstanceReferenceMap(resolvedInputs)
374
+ });
375
+ }
376
+ function toInstanceCustomStatus(status) {
377
+ const meta = instanceCustomStatusInputSchema.shape.meta.parse(status.meta);
378
+ return create2(InstanceCustomStatusSchema, {
379
+ name: status.name,
380
+ meta: {
381
+ title: meta.title ?? status.name,
382
+ description: meta.description,
383
+ icon: meta.icon,
384
+ iconColor: meta.iconColor
385
+ },
386
+ value: status.value,
387
+ message: status.message ?? undefined,
388
+ order: status.order,
389
+ serviceAccountId: status.serviceAccountId,
390
+ createdAt: toTimestamp(status.createdAt),
391
+ updatedAt: toTimestamp(status.updatedAt)
392
+ });
393
+ }
394
+ function toOperation(operation) {
395
+ const meta = operationMetaSchema.parse(operation.meta);
396
+ const options = operationOptionsSchema.partial().parse(operation.options);
397
+ const phases = operation.phases ? operationPhaseSchema.array().parse(operation.phases) : [];
398
+ return create2(OperationSchema, {
399
+ id: operation.id,
400
+ meta,
401
+ type: toOperationType(operation.type),
402
+ status: toOperationStatus(operation.status),
403
+ options: {
404
+ forceUpdateDependencies: options.forceUpdateDependencies ?? false,
405
+ ignoreChangedDependencies: options.ignoreChangedDependencies ?? false,
406
+ ignoreDependencies: options.ignoreDependencies ?? false,
407
+ forceUpdateChildren: options.forceUpdateChildren ?? false,
408
+ onlyDestroyGhosts: options.onlyDestroyGhosts ?? false,
409
+ firstDestroyGhosts: options.firstDestroyGhosts ?? false,
410
+ ignoreGhosts: options.ignoreGhosts ?? false,
411
+ destroyDependentInstances: options.destroyDependentInstances ?? false,
412
+ invokeDestroyTriggers: options.invokeDestroyTriggers ?? false,
413
+ deleteUnreachableResources: options.deleteUnreachableResources ?? false,
414
+ forceDeleteState: options.forceDeleteState ?? false,
415
+ allowPartialCompositeInstanceUpdate: options.allowPartialCompositeInstanceUpdate ?? false,
416
+ allowPartialCompositeInstanceDestruction: options.allowPartialCompositeInstanceDestruction ?? false,
417
+ refresh: options.refresh ?? false,
418
+ debug: options.debug ?? false
419
+ },
420
+ requestedInstanceIds: z.string().array().parse(operation.requestedInstanceIds),
421
+ phases: phases.map(toOperationPhase),
422
+ startedAt: toTimestamp(operation.startedAt),
423
+ updatedAt: toTimestamp(operation.updatedAt),
424
+ finishedAt: toNullableTimestamp(operation.finishedAt)
425
+ });
426
+ }
427
+ function toOperationPhase(phase) {
428
+ return create2(OperationPhaseSchema, {
429
+ type: toOperationPhaseType(phase.type),
430
+ instances: phase.instances
431
+ });
432
+ }
433
+ function toOperationLog(operationId, log) {
434
+ return create2(OperationLogSchema, {
435
+ id: log.id,
436
+ operationId,
437
+ stateId: log.stateId ?? undefined,
438
+ isSystem: log.isSystem ?? false,
439
+ content: log.content
440
+ });
441
+ }
442
+ function toLibrary(library) {
443
+ return create2(LibrarySchema, {
444
+ components: Object.fromEntries(Object.entries(library.components).map(([type, component]) => [type, toComponent(component)])),
445
+ entities: Object.fromEntries(Object.entries(library.entities).map(([type, entity]) => [type, toEntity(entity)]))
446
+ });
447
+ }
448
+ function toComponent(component) {
449
+ return create2(ComponentSchema, {
450
+ type: component.type,
451
+ kind: toComponentKind(component.kind),
452
+ arguments: Object.fromEntries(Object.entries(component.args).map(([name, argument]) => [
453
+ name,
454
+ {
455
+ schema: jsonObjectSchema.parse(argument.schema),
456
+ required: argument.required,
457
+ meta: argument.meta
458
+ }
459
+ ])),
460
+ inputs: Object.fromEntries(Object.entries(component.inputs).map(([name, port]) => [name, toComponentPort(port)])),
461
+ outputs: Object.fromEntries(Object.entries(component.outputs).map(([name, port]) => [name, toComponentPort(port)])),
462
+ meta: component.meta,
463
+ definitionHash: component.definitionHash
464
+ });
465
+ }
466
+ function toEntity(entity) {
467
+ return create2(EntitySchema, {
468
+ type: entity.type,
469
+ extensions: entity.extensions ?? [],
470
+ directExtensions: entity.directExtensions ?? [],
471
+ inclusions: entity.inclusions ?? [],
472
+ directInclusions: entity.directInclusions ?? [],
473
+ schema: jsonObjectSchema.parse(entity.schema),
474
+ meta: entity.meta,
475
+ definitionHash: entity.definitionHash
476
+ });
477
+ }
478
+ function toTimestamp(value) {
479
+ if (!Number.isFinite(value.getTime())) {
480
+ throw new Error("Cannot convert invalid date to timestamp");
24
481
  }
25
- const apiKey = await services.apiKeyService.getApiKeyByToken(projectId, token);
26
- return [projectId, apiKey];
482
+ return timestampFromDate(value);
483
+ }
484
+ function toNullableTimestamp(value) {
485
+ return value ? toTimestamp(value) : undefined;
486
+ }
487
+ function toComponentPort(port) {
488
+ return {
489
+ entityType: port.type,
490
+ fromInput: port.fromInput,
491
+ required: port.required,
492
+ multiple: port.multiple,
493
+ meta: port.meta
494
+ };
495
+ }
496
+ function toInstanceReferenceMap(values) {
497
+ return Object.fromEntries(Object.entries(values ?? {}).map(([key, references]) => [
498
+ key,
499
+ {
500
+ values: references
501
+ }
502
+ ]));
503
+ }
504
+ function fromInstanceReferenceMap(values) {
505
+ return Object.fromEntries(Object.entries(values).map(([key, list]) => [key, list.values.map(fromInstanceReference)]));
506
+ }
507
+ function fromUnknownInstanceReferenceMap(value) {
508
+ return instanceModelSchema.shape.inputs.unwrap().parse(value);
509
+ }
510
+ function fromInstanceReference(value) {
511
+ return instanceInputSchema.parse({
512
+ instanceId: value.instanceId,
513
+ output: value.output,
514
+ path: value.path
515
+ });
516
+ }
517
+ function toComponentKind(value) {
518
+ if (value === "unit")
519
+ return ComponentKind.UNIT;
520
+ if (value === "composite")
521
+ return ComponentKind.COMPOSITE;
522
+ throw new Error(`Unknown component kind "${value}"`);
523
+ }
524
+ function fromComponentKind(value) {
525
+ if (value === ComponentKind.UNIT)
526
+ return "unit";
527
+ if (value === ComponentKind.COMPOSITE)
528
+ return "composite";
529
+ throw new Error("Component kind must be specified");
530
+ }
531
+ function toInstanceStatus(value) {
532
+ const statuses = {
533
+ undeployed: InstanceStatus.UNDEPLOYED,
534
+ attempted: InstanceStatus.ATTEMPTED,
535
+ deployed: InstanceStatus.DEPLOYED,
536
+ failed: InstanceStatus.FAILED
537
+ };
538
+ const status = statuses[value];
539
+ if (status === undefined)
540
+ throw new Error(`Unknown instance status "${value}"`);
541
+ return status;
542
+ }
543
+ function toInstanceSource(value) {
544
+ if (value === "resident")
545
+ return InstanceSource.RESIDENT;
546
+ if (value === "virtual")
547
+ return InstanceSource.VIRTUAL;
548
+ throw new Error(`Unknown instance source "${value}"`);
549
+ }
550
+ function toEvaluationStatus(value) {
551
+ if (value === "evaluating")
552
+ return EvaluationStatus.EVALUATING;
553
+ if (value === "evaluated")
554
+ return EvaluationStatus.EVALUATED;
555
+ if (value === "error")
556
+ return EvaluationStatus.ERROR;
557
+ throw new Error(`Unknown evaluation status "${value}"`);
558
+ }
559
+ function toInstanceOperationStatus(value) {
560
+ const statuses = {
561
+ updating: InstanceOperationStatus.UPDATING,
562
+ processing_triggers: InstanceOperationStatus.PROCESSING_TRIGGERS,
563
+ previewing: InstanceOperationStatus.PREVIEWING,
564
+ destroying: InstanceOperationStatus.DESTROYING,
565
+ refreshing: InstanceOperationStatus.REFRESHING,
566
+ pending: InstanceOperationStatus.PENDING,
567
+ cancelling: InstanceOperationStatus.CANCELLING,
568
+ updated: InstanceOperationStatus.UPDATED,
569
+ previewed: InstanceOperationStatus.PREVIEWED,
570
+ skipped: InstanceOperationStatus.SKIPPED,
571
+ destroyed: InstanceOperationStatus.DESTROYED,
572
+ refreshed: InstanceOperationStatus.REFRESHED,
573
+ cancelled: InstanceOperationStatus.CANCELLED,
574
+ failed: InstanceOperationStatus.FAILED
575
+ };
576
+ const status = statuses[value];
577
+ if (status === undefined)
578
+ throw new Error(`Unknown instance operation status "${value}"`);
579
+ return status;
580
+ }
581
+ function fromOperationType(value) {
582
+ if (value === OperationType.UPDATE)
583
+ return "update";
584
+ if (value === OperationType.PREVIEW)
585
+ return "preview";
586
+ if (value === OperationType.DESTROY)
587
+ return "destroy";
588
+ if (value === OperationType.RECREATE)
589
+ return "recreate";
590
+ if (value === OperationType.REFRESH)
591
+ return "refresh";
592
+ throw new Error("Operation type must be specified");
593
+ }
594
+ function fromOperationPhase(phase) {
595
+ return operationPhaseSchema.parse({
596
+ type: fromOperationPhaseType(phase.type),
597
+ instances: phase.instances.map((instance) => ({
598
+ id: instance.id,
599
+ parentId: instance.parentId,
600
+ message: instance.message
601
+ }))
602
+ });
603
+ }
604
+ function toOperationType(value) {
605
+ const types = {
606
+ update: OperationType.UPDATE,
607
+ preview: OperationType.PREVIEW,
608
+ destroy: OperationType.DESTROY,
609
+ recreate: OperationType.RECREATE,
610
+ refresh: OperationType.REFRESH
611
+ };
612
+ const type = types[value];
613
+ if (type === undefined)
614
+ throw new Error(`Unknown operation type "${value}"`);
615
+ return type;
616
+ }
617
+ function toOperationStatus(value) {
618
+ const statuses = {
619
+ pending: OperationStatus.PENDING,
620
+ running: OperationStatus.RUNNING,
621
+ failing: OperationStatus.FAILING,
622
+ cancelling: OperationStatus.CANCELLING,
623
+ completed: OperationStatus.COMPLETED,
624
+ failed: OperationStatus.FAILED,
625
+ cancelled: OperationStatus.CANCELLED
626
+ };
627
+ const status = statuses[value];
628
+ if (status === undefined)
629
+ throw new Error(`Unknown operation status "${value}"`);
630
+ return status;
631
+ }
632
+ function toOperationPhaseType(value) {
633
+ const types = {
634
+ destroy: OperationPhaseType.DESTROY,
635
+ preview: OperationPhaseType.PREVIEW,
636
+ update: OperationPhaseType.UPDATE,
637
+ refresh: OperationPhaseType.REFRESH
638
+ };
639
+ const type = types[value];
640
+ if (type === undefined)
641
+ throw new Error(`Unknown operation phase type "${value}"`);
642
+ return type;
643
+ }
644
+ function fromOperationPhaseType(value) {
645
+ const types = {
646
+ [OperationPhaseType.DESTROY]: "destroy",
647
+ [OperationPhaseType.PREVIEW]: "preview",
648
+ [OperationPhaseType.UPDATE]: "update",
649
+ [OperationPhaseType.REFRESH]: "refresh"
650
+ };
651
+ const type = types[value];
652
+ if (!type)
653
+ throw new Error("Operation phase type must be specified");
654
+ return type;
27
655
  }
28
656
  // src/shared/error-handling.ts
29
- import { AccessError } from "@highstate/backend/shared";
657
+ import { create as create3 } from "@bufbuild/protobuf";
658
+ import { durationFromMs } from "@bufbuild/protobuf/wkt";
659
+ import { Code as Code3, ConnectError as ConnectError2 } from "@connectrpc/connect";
660
+ import {
661
+ BadRequestSchema as BadRequestSchema2,
662
+ ErrorInfoSchema as ErrorInfoSchema2,
663
+ PreconditionFailureSchema,
664
+ RequestInfoSchema,
665
+ RetryInfoSchema
666
+ } from "@highstate/api/v1";
667
+ import { BackendError, BackendErrorCategory } from "@highstate/backend/shared";
30
668
  import { isAbortError } from "abort-controller-x";
31
- import { ServerError as ServerError2, Status as Status2 } from "nice-grpc-common";
32
- function createErrorHandlingMiddleware(services) {
33
- return async function* errorHandlingMiddleware(call, context) {
669
+ var sensitiveMetadataKey = /credential|token|secret|encrypted|cause|stack|path/i;
670
+ function createErrorHandlingInterceptor(services) {
671
+ return (next) => async (request) => {
34
672
  try {
35
- return yield* call.next(call.request, context);
673
+ return await next(request);
36
674
  } catch (error) {
37
- if (error instanceof ServerError2 || isAbortError(error)) {
675
+ if (isAbortError(error) || error instanceof ConnectError2 && error.code === Code3.Canceled) {
38
676
  throw error;
39
677
  }
40
- if (error instanceof AccessError) {
41
- services.logger.info({ error }, "access denied");
42
- throw new ServerError2(Status2.UNAUTHENTICATED, "Access denied");
678
+ if (error instanceof BackendError) {
679
+ const code = categoryToCode(error.category);
680
+ if (code === Code3.Internal) {
681
+ services.logger.error({ error, method: request.method.name }, "unexpected backend error");
682
+ throw new ConnectError2("An unexpected error occurred", Code3.Internal);
683
+ }
684
+ throw new ConnectError2(error.message, code, undefined, backendErrorDetails(error, request));
685
+ }
686
+ if (error instanceof ConnectError2) {
687
+ throw error;
43
688
  }
44
- services.logger.error({ error }, "unexpected error");
45
- throw new ServerError2(Status2.INTERNAL, "An unexpected error occurred");
689
+ services.logger.error({ error, method: request.method.name }, "unexpected error");
690
+ throw new ConnectError2("An unexpected error occurred", Code3.Internal);
46
691
  }
47
692
  };
48
693
  }
694
+ function backendErrorDetails(error, request) {
695
+ const details = [
696
+ {
697
+ desc: ErrorInfoSchema2,
698
+ value: create3(ErrorInfoSchema2, {
699
+ reason: error.reason,
700
+ domain: "highstate.io",
701
+ metadata: Object.fromEntries(Object.entries(error.metadata).filter(([key]) => !sensitiveMetadataKey.test(key)))
702
+ })
703
+ }
704
+ ];
705
+ if (error.fieldViolations.length > 0) {
706
+ details.push({
707
+ desc: BadRequestSchema2,
708
+ value: create3(BadRequestSchema2, {
709
+ fieldViolations: error.fieldViolations.map((violation) => ({
710
+ field: toProtobufPath(violation.field),
711
+ reason: violation.reason,
712
+ description: violation.description
713
+ }))
714
+ })
715
+ });
716
+ }
717
+ if (error.preconditionViolations.length > 0) {
718
+ details.push({
719
+ desc: PreconditionFailureSchema,
720
+ value: create3(PreconditionFailureSchema, {
721
+ violations: error.preconditionViolations.map((violation) => ({ ...violation }))
722
+ })
723
+ });
724
+ }
725
+ if (error.retry && Number.isFinite(error.retry.delayMs) && error.retry.delayMs >= 0) {
726
+ details.push({
727
+ desc: RetryInfoSchema,
728
+ value: create3(RetryInfoSchema, { retryDelay: durationFromMs(error.retry.delayMs) })
729
+ });
730
+ }
731
+ const requestId = request.header.get("x-request-id")?.trim();
732
+ if (requestId) {
733
+ details.push({
734
+ desc: RequestInfoSchema,
735
+ value: create3(RequestInfoSchema, { requestId })
736
+ });
737
+ }
738
+ return details;
739
+ }
740
+ function toProtobufPath(path) {
741
+ return path.split(".").map((segment) => segment.replace(/[A-Z]/g, (character) => `_${character.toLowerCase()}`)).join(".");
742
+ }
743
+ function categoryToCode(category) {
744
+ switch (category) {
745
+ case BackendErrorCategory.InvalidArgument:
746
+ return Code3.InvalidArgument;
747
+ case BackendErrorCategory.Unauthenticated:
748
+ return Code3.Unauthenticated;
749
+ case BackendErrorCategory.PermissionDenied:
750
+ return Code3.PermissionDenied;
751
+ case BackendErrorCategory.NotFound:
752
+ return Code3.NotFound;
753
+ case BackendErrorCategory.AlreadyExists:
754
+ return Code3.AlreadyExists;
755
+ case BackendErrorCategory.FailedPrecondition:
756
+ return Code3.FailedPrecondition;
757
+ case BackendErrorCategory.Aborted:
758
+ return Code3.Aborted;
759
+ case BackendErrorCategory.Unavailable:
760
+ return Code3.Unavailable;
761
+ case BackendErrorCategory.Internal:
762
+ return Code3.Internal;
763
+ }
764
+ }
765
+ // src/shared/field-mask.ts
766
+ import { Code as Code4 } from "@connectrpc/connect";
767
+ function validateUpdateMask(mask, schema, mutablePaths) {
768
+ if (!mask || mask.paths.length === 0) {
769
+ throw fieldMaskError("update_mask", "REQUIRED", "The update mask must not be empty");
770
+ }
771
+ const paths = new Set;
772
+ for (const path of mask.paths) {
773
+ const normalized = normalizePath(schema, path);
774
+ if (!mutablePaths.has(normalized)) {
775
+ throw fieldMaskError(`update_mask.paths`, "IMMUTABLE_OR_UNKNOWN", `The path "${path}" is unknown, immutable, output-only, or traverses a collection`);
776
+ }
777
+ paths.add(normalized);
778
+ }
779
+ return [...paths];
780
+ }
781
+ function normalizePath(schema, path) {
782
+ if (!path || path === "*")
783
+ return path;
784
+ let descriptor = schema;
785
+ const normalized = [];
786
+ const segments = path.split(".");
787
+ for (const [index, segment] of segments.entries()) {
788
+ const field = descriptor.fields.find((candidate) => candidate.name === segment || candidate.localName === segment || candidate.jsonName === segment);
789
+ if (!field)
790
+ return path;
791
+ normalized.push(field.name);
792
+ if (index === segments.length - 1)
793
+ break;
794
+ if (field.fieldKind !== "message" || !field.message)
795
+ return path;
796
+ descriptor = field.message;
797
+ }
798
+ return normalized.join(".");
799
+ }
800
+ function fieldMaskError(field, reason, description) {
801
+ return createApiError({
802
+ message: "Invalid update mask",
803
+ code: Code4.InvalidArgument,
804
+ reason: "UPDATE_MASK_INVALID",
805
+ fieldViolations: [{ field, reason, description }]
806
+ });
807
+ }
808
+ // src/shared/serialization.ts
809
+ function toJsonObject(value) {
810
+ return normalizeJsonValue(value);
811
+ }
812
+ function normalizeJsonValue(value) {
813
+ if (value instanceof Date) {
814
+ return value.toISOString();
815
+ }
816
+ if (Array.isArray(value)) {
817
+ return value.map(normalizeJsonValue);
818
+ }
819
+ if (value && typeof value === "object") {
820
+ return Object.fromEntries(Object.entries(value).flatMap(([key, entry]) => {
821
+ if (entry === undefined) {
822
+ return [];
823
+ }
824
+ return [[key, normalizeJsonValue(entry)]];
825
+ }));
826
+ }
827
+ return value;
828
+ }
49
829
  // src/shared/validation.ts
50
- import { ServerError as ServerError3, Status as Status3 } from "nice-grpc-common";
830
+ import { Code as Code5 } from "@connectrpc/connect";
51
831
  function parseArgument(request, argumentName, schema) {
52
832
  const result = schema.safeParse(request[argumentName]);
53
833
  if (!result.success) {
54
- throw new ServerError3(Status3.INVALID_ARGUMENT, `Invalid argument "${argumentName}": ${result.error.message}`);
834
+ throw validationError(`Invalid argument "${argumentName}"`, argumentName, result.error);
835
+ }
836
+ return result.data;
837
+ }
838
+ function parseValue(value, name, schema) {
839
+ const result = schema.safeParse(value);
840
+ if (!result.success) {
841
+ throw validationError(`Invalid ${name}`, name, result.error);
55
842
  }
56
843
  return result.data;
57
844
  }
58
- // src/handlers/instance.ts
59
- function createInstanceService(services) {
845
+ function validationError(message, field, error) {
846
+ return createApiError({
847
+ message,
848
+ code: Code5.InvalidArgument,
849
+ reason: "REQUEST_INVALID",
850
+ fieldViolations: error.issues.map((issue) => ({
851
+ field: [field, ...issue.path.map(String)].join("."),
852
+ reason: issue.code.toUpperCase(),
853
+ description: issue.message
854
+ }))
855
+ });
856
+ }
857
+ // src/handlers/instance-state.ts
858
+ function createInstanceStateService(services) {
60
859
  return {
860
+ async listInstanceStates(request, context) {
861
+ const requestContext = await authenticateProject(services, request, context);
862
+ const page = await services.instanceStateService.getInstanceStates(requestContext, {
863
+ includeEvaluationState: request.includeEvaluationState,
864
+ includeLastOperationState: request.includeLastOperationState,
865
+ includeParentInstanceId: request.includeParentInstanceId,
866
+ includeExtra: request.includeExtra,
867
+ loadCustomStatuses: request.includeCustomStatuses
868
+ }, { pageSize: request.pageSize, pageToken: request.pageToken });
869
+ return { states: page.items.map(toInstanceState), nextPageToken: page.nextPageToken };
870
+ },
871
+ async getInstanceState(request, context) {
872
+ const requestContext = await authenticateProject(services, request, context);
873
+ const stateId = parseArgument(request, "stateId", z2.cuid2());
874
+ const state = await services.instanceStateService.getInstanceStateOrThrow(requestContext, stateId, {
875
+ includeEvaluationState: request.includeEvaluationState,
876
+ includeLastOperationState: request.includeLastOperationState,
877
+ includeParentInstanceId: request.includeParentInstanceId,
878
+ includeExtra: request.includeExtra,
879
+ loadCustomStatuses: request.includeCustomStatuses
880
+ });
881
+ return { state: toInstanceState(state) };
882
+ },
61
883
  async updateCustomStatus(request, context) {
62
- const [projectId, apiKey] = await authenticate(services, context);
63
- const stateId = parseArgument(request, "stateId", z.cuid2());
64
- const customStatus = parseArgument(request, "status", instanceCustomStatusInputSchema);
65
- await services.instanceStateService.updateCustomStatus(projectId, stateId, apiKey.serviceAccountId, customStatus);
66
- return {};
884
+ const requestContext = await authenticateProject(services, request, context);
885
+ const stateId = parseArgument(request, "stateId", z2.cuid2());
886
+ const customStatus = parseArgument(request, "status", instanceCustomStatusInputSchema2);
887
+ await services.instanceStateService.updateCustomStatus(requestContext, stateId, requestContext.subject.serviceAccountId, customStatus);
888
+ const state = await services.instanceStateService.getInstanceStateOrThrow(requestContext, stateId, { loadCustomStatuses: true });
889
+ const status = state.customStatuses?.find((value) => value.name === customStatus.name && value.serviceAccountId === requestContext.subject.serviceAccountId);
890
+ if (!status) {
891
+ throw new Error("Updated custom status was not returned by the backend");
892
+ }
893
+ return { status: toInstanceCustomStatus(status) };
67
894
  },
68
895
  async removeCustomStatus(request, context) {
69
- const [projectId, apiKey] = await authenticate(services, context);
70
- const stateId = parseArgument(request, "stateId", z.cuid2());
71
- await services.instanceStateService.removeCustomStatus(projectId, stateId, apiKey.serviceAccountId, request.statusName);
896
+ const requestContext = await authenticateProject(services, request, context);
897
+ const stateId = parseArgument(request, "stateId", z2.cuid2());
898
+ const statusName = parseArgument(request, "statusName", z2.string().min(1));
899
+ await services.instanceStateService.removeCustomStatus(requestContext, stateId, requestContext.subject.serviceAccountId, statusName);
900
+ return {};
901
+ }
902
+ };
903
+ }
904
+
905
+ // src/handlers/library.ts
906
+ import { ComponentKind as ComponentKind2 } from "@highstate/api/v1";
907
+ import { ComponentNotFoundError } from "@highstate/backend/shared";
908
+ import { z as z3 } from "@highstate/contract";
909
+ function createLibraryService(services) {
910
+ return {
911
+ async getLibrary(request, context) {
912
+ const requestContext = await authenticateProject(services, request, context);
913
+ const library = await services.libraryService.getLibraryModel(requestContext, context.signal);
914
+ return {
915
+ library: toLibrary({
916
+ components: { ...library.components },
917
+ entities: { ...library.entities }
918
+ })
919
+ };
920
+ },
921
+ async listComponents(request, context) {
922
+ const requestContext = await authenticateProject(services, request, context);
923
+ const page = await services.libraryService.getComponents(requestContext, { pageSize: request.pageSize, pageToken: request.pageToken }, context.signal);
924
+ return {
925
+ components: page.items.map((component) => ({
926
+ type: component.type,
927
+ kind: component.kind === "unit" ? ComponentKind2.UNIT : ComponentKind2.COMPOSITE,
928
+ meta: {
929
+ title: component.meta.title,
930
+ description: component.meta.description,
931
+ color: component.meta.color,
932
+ icon: component.meta.icon,
933
+ iconColor: component.meta.iconColor,
934
+ secondaryIcon: component.meta.secondaryIcon,
935
+ secondaryIconColor: component.meta.secondaryIconColor,
936
+ category: component.meta.category,
937
+ defaultNamePrefix: component.meta.defaultNamePrefix
938
+ }
939
+ })),
940
+ nextPageToken: page.nextPageToken
941
+ };
942
+ },
943
+ async getComponent(request, context) {
944
+ const requestContext = await authenticateProject(services, request, context);
945
+ const type = parseArgument(request, "type", z3.string().min(1));
946
+ const library = await services.libraryService.getLibraryModel(requestContext, context.signal);
947
+ const component = library.components[type];
948
+ if (!component) {
949
+ throw new ComponentNotFoundError(requestContext.projectId, type);
950
+ }
951
+ return { component: toComponent(component) };
952
+ }
953
+ };
954
+ }
955
+
956
+ // src/handlers/operation.ts
957
+ import { operationLaunchInputSchema, operationPlanInputSchema } from "@highstate/backend/shared";
958
+ import { instanceIdSchema, z as z4 } from "@highstate/contract";
959
+ function createOperationService(services) {
960
+ return {
961
+ async planOperation(request, context) {
962
+ const requestContext = await authenticateProject(services, request, context);
963
+ const input = parseValue({ ...request, projectId: requestContext.projectId, type: fromOperationType(request.type) }, "request", operationPlanInputSchema);
964
+ const phases = await services.operationManager.plan(input);
965
+ return { phases: phases.map(toOperationPhase) };
966
+ },
967
+ async launchOperation(request, context) {
968
+ const requestContext = await authenticateProject(services, request, context);
969
+ const input = parseValue({
970
+ ...request,
971
+ projectId: requestContext.projectId,
972
+ type: fromOperationType(request.type),
973
+ plan: request.plan.length > 0 ? request.plan.map(fromOperationPhase) : undefined
974
+ }, "request", operationLaunchInputSchema);
975
+ const operation = await services.operationManager.launch(input);
976
+ return { operation: toOperation(operation) };
977
+ },
978
+ async getOperation(request, context) {
979
+ const requestContext = await authenticateProject(services, request, context);
980
+ const operationId = parseValue(request.operationId, "operationId", z4.string().min(1));
981
+ const operation = await services.operationService.getOperationOrThrow(requestContext, operationId);
982
+ return { operation: toOperation(operation) };
983
+ },
984
+ async listOperations(request, context) {
985
+ const requestContext = await authenticateProject(services, request, context);
986
+ const page = await services.operationService.getOperations(requestContext, {
987
+ pageSize: request.pageSize,
988
+ pageToken: request.pageToken
989
+ });
990
+ return { operations: page.items.map(toOperation), nextPageToken: page.nextPageToken };
991
+ },
992
+ async listOperationLogs(request, context) {
993
+ const requestContext = await authenticateProject(services, request, context);
994
+ const operationId = parseValue(request.operationId, "operationId", z4.string().min(1));
995
+ const stateId = parseValue(request.stateId, "stateId", z4.cuid2().optional());
996
+ await services.operationService.getOperationOrThrow(requestContext, operationId);
997
+ const page = await services.operationService.getOperationLogs(requestContext, operationId, stateId, { pageSize: request.pageSize, pageToken: request.pageToken });
998
+ return {
999
+ logs: page.items.map((log) => toOperationLog(operationId, log)),
1000
+ nextPageToken: page.nextPageToken
1001
+ };
1002
+ },
1003
+ async cancelOperation(request, context) {
1004
+ const requestContext = await authenticateProject(services, request, context);
1005
+ const operationId = parseValue(request.operationId, "operationId", z4.string().min(1));
1006
+ await services.operationService.getOperationOrThrow(requestContext, operationId);
1007
+ services.operationManager.cancel(operationId);
1008
+ return {};
1009
+ },
1010
+ async cancelInstanceOperation(request, context) {
1011
+ const requestContext = await authenticateProject(services, request, context);
1012
+ const operationId = parseValue(request.operationId, "operationId", z4.string().min(1));
1013
+ const instanceId = parseValue(request.instanceId, "instanceId", instanceIdSchema);
1014
+ await services.operationService.getOperationOrThrow(requestContext, operationId);
1015
+ services.operationManager.cancelInstance(operationId, instanceId);
72
1016
  return {};
73
1017
  }
74
1018
  };
@@ -76,54 +1020,166 @@ function createInstanceService(services) {
76
1020
 
77
1021
  // src/handlers/panel.ts
78
1022
  import { panelInputSchema } from "@highstate/backend/shared";
79
- import { z as z2 } from "@highstate/contract";
1023
+ import { z as z5 } from "@highstate/contract";
80
1024
  function createPanelService(services) {
81
1025
  return {
82
1026
  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);
1027
+ const requestContext = await authenticateProject(services, request, context);
1028
+ const workerVersionId = parseArgument(request, "workerVersionId", z5.cuid2());
1029
+ const workerInstanceId = parseArgument(request, "workerInstanceId", z5.cuid2());
1030
+ const stateId = parseArgument(request, "stateId", z5.cuid2());
1031
+ const panels = parseArgument(request, "panels", z5.array(panelInputSchema));
1032
+ const panelIds = await services.panelService.setUnitPanels(requestContext, stateId, requestContext.subject.apiKeyId, workerVersionId, panels, workerInstanceId);
89
1033
  return { panelIds };
90
1034
  }
91
1035
  };
92
1036
  }
93
1037
 
1038
+ // src/handlers/project.ts
1039
+ function createProjectService(services) {
1040
+ return {
1041
+ async getProject(request, context) {
1042
+ const requestContext = await authenticateProject(services, request, context);
1043
+ const [project, unlockState] = await Promise.all([
1044
+ services.projectService.getProjectOrThrowCore(requestContext.projectId),
1045
+ services.projectUnlockService.getProjectUnlockStateCore(requestContext.projectId)
1046
+ ]);
1047
+ return {
1048
+ project: toProject(project),
1049
+ isLocked: unlockState.type === "locked"
1050
+ };
1051
+ },
1052
+ async listProjects(request, context) {
1053
+ const requestContext = await authenticateBackend(services, context);
1054
+ const result = await services.projectService.getProjects(requestContext, {
1055
+ pageSize: request.pageSize,
1056
+ pageToken: request.pageToken
1057
+ });
1058
+ const projects = await Promise.all(result.items.map(async (project) => {
1059
+ const unlockState = await services.projectUnlockService.getProjectUnlockStateCore(project.id);
1060
+ return {
1061
+ project: toProject(project),
1062
+ isLocked: unlockState.type === "locked"
1063
+ };
1064
+ }));
1065
+ return {
1066
+ projects,
1067
+ nextPageToken: result.nextPageToken
1068
+ };
1069
+ }
1070
+ };
1071
+ }
1072
+
1073
+ // src/handlers/project-model.ts
1074
+ import {
1075
+ HubSchema as HubSchema2,
1076
+ InstanceSchema as InstanceSchema2
1077
+ } from "@highstate/api/v1";
1078
+ import { projectModelInstanceSchema } from "@highstate/backend/shared";
1079
+ import { hubModelSchema, instanceIdSchema as instanceIdSchema2, z as z6 } from "@highstate/contract";
1080
+ var instanceMutablePaths = new Set([
1081
+ "arguments",
1082
+ "inputs",
1083
+ "hub_inputs",
1084
+ "injection_inputs",
1085
+ "position",
1086
+ "position.x",
1087
+ "position.y"
1088
+ ]);
1089
+ var hubMutablePaths = new Set([
1090
+ "position",
1091
+ "position.x",
1092
+ "position.y",
1093
+ "inputs",
1094
+ "injection_inputs"
1095
+ ]);
1096
+ function createProjectModelService(services) {
1097
+ return {
1098
+ async getProjectModel(request, context) {
1099
+ const requestContext = await authenticateProject(services, request, context);
1100
+ const [model] = await services.projectModelService.getProjectModel(requestContext, {
1101
+ includeVirtualInstances: request.includeVirtualInstances,
1102
+ includeGhostInstances: request.includeGhostInstances
1103
+ });
1104
+ return { model: toProjectModel(model) };
1105
+ },
1106
+ async createNodes(request, context) {
1107
+ const requestContext = await authenticateProject(services, request, context);
1108
+ const instances = parseValue(request.instances.map(fromInstance), "instances", projectModelInstanceSchema.array());
1109
+ const hubs = parseValue(request.hubs.map(fromHub), "hubs", hubModelSchema.array());
1110
+ await services.projectService.createNodes(requestContext, instances, hubs);
1111
+ return {};
1112
+ },
1113
+ async updateInstance(request, context) {
1114
+ const requestContext = await authenticateProject(services, request, context);
1115
+ const requestInstance = parseArgument(request, "instance", z6.custom());
1116
+ const instanceId = parseArgument(requestInstance, "id", instanceIdSchema2);
1117
+ const paths = validateUpdateMask(request.updateMask, InstanceSchema2, instanceMutablePaths);
1118
+ const patch = toInstancePatch(requestInstance, paths);
1119
+ const instance = await services.projectService.updateInstance(requestContext, instanceId, patch);
1120
+ return { instance: toInstance(instance) };
1121
+ },
1122
+ async renameInstance(request, context) {
1123
+ const requestContext = await authenticateProject(services, request, context);
1124
+ const instanceId = parseArgument(request, "instanceId", instanceIdSchema2);
1125
+ const newName = parseArgument(request, "newName", z6.string().min(1));
1126
+ const instance = await services.projectService.renameInstance(requestContext, instanceId, newName);
1127
+ return { instance: toInstance(instance) };
1128
+ },
1129
+ async deleteInstance(request, context) {
1130
+ const requestContext = await authenticateProject(services, request, context);
1131
+ const instanceId = parseArgument(request, "instanceId", instanceIdSchema2);
1132
+ await services.projectService.deleteInstance(requestContext, instanceId);
1133
+ return {};
1134
+ },
1135
+ async updateHub(request, context) {
1136
+ const requestContext = await authenticateProject(services, request, context);
1137
+ const requestHub = parseArgument(request, "hub", z6.custom());
1138
+ const hubId = parseArgument(requestHub, "id", z6.cuid2());
1139
+ const paths = validateUpdateMask(request.updateMask, HubSchema2, hubMutablePaths);
1140
+ const patch = toHubPatch(requestHub, paths);
1141
+ const hub = await services.projectService.updateHub(requestContext, hubId, patch);
1142
+ return { hub: toHub(hub) };
1143
+ },
1144
+ async deleteHub(request, context) {
1145
+ const requestContext = await authenticateProject(services, request, context);
1146
+ const hubId = parseArgument(request, "hubId", z6.cuid2());
1147
+ await services.projectService.deleteHub(requestContext, hubId);
1148
+ return {};
1149
+ }
1150
+ };
1151
+ }
1152
+
94
1153
  // src/handlers/secret.ts
95
1154
  function createSecretService(services) {
96
1155
  return {
97
- async getSecretContent(_request, context) {
98
- const [_projectId] = await authenticate(services, context);
1156
+ async getSecretContent(request, context) {
1157
+ await authenticateProject(services, request, context);
99
1158
  throw new Error("Not implemented");
100
1159
  }
101
1160
  };
102
1161
  }
103
1162
 
104
1163
  // src/handlers/worker.ts
105
- import { commonObjectMetaSchema, serviceAccountMetaSchema, z as z3 } from "@highstate/contract";
106
- var workerMetaUpdatePayloadSchema = z3.union([
107
- commonObjectMetaSchema,
108
- z3.object({
109
- workerMeta: commonObjectMetaSchema,
110
- serviceAccountMeta: serviceAccountMetaSchema
111
- })
112
- ]);
1164
+ import { create as create4 } from "@bufbuild/protobuf";
1165
+ import { ConnectResponseSchema } from "@highstate/api/worker.v1";
1166
+ import { WorkerOwnershipError } from "@highstate/backend/shared";
1167
+ import { commonObjectMetaSchema, serviceAccountMetaSchema, z as z7 } from "@highstate/contract";
113
1168
  function createWorkerService(services) {
114
1169
  return {
115
1170
  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));
1171
+ const requestContext = await authenticateProject(services, request, context);
1172
+ const { projectId } = requestContext;
1173
+ const workerVersionId = parseArgument(request, "workerVersionId", z7.cuid2());
1174
+ const workerInstanceId = parseArgument(request, "workerInstanceId", z7.cuid2());
1175
+ const dataEndpoint = parseArgument(request, "dataEndpoint", z7.string().min(1));
120
1176
  const database = await services.database.forProject(projectId);
121
1177
  const workerVersion = await database.workerVersion.findFirst({
122
- where: { id: workerVersionId, apiKeyId: apiKey.id },
1178
+ where: { id: workerVersionId, apiKeyId: requestContext.subject.apiKeyId },
123
1179
  select: { id: true }
124
1180
  });
125
1181
  if (!workerVersion) {
126
- throw new Error(`Worker version "${workerVersionId}" is not owned by the API key`);
1182
+ throw new WorkerOwnershipError(projectId, workerVersionId);
127
1183
  }
128
1184
  services.workerManager.assertWorkerInstance(projectId, workerVersionId, workerInstanceId);
129
1185
  services.panelEndpointManager.connect(projectId, workerVersionId, workerInstanceId, dataEndpoint);
@@ -134,15 +1190,15 @@ function createWorkerService(services) {
134
1190
  select: { stateId: true, params: true }
135
1191
  });
136
1192
  for (const registration of existingRegistrations) {
137
- yield {
1193
+ yield create4(ConnectResponseSchema, {
138
1194
  event: {
139
- $case: "unitRegistration",
1195
+ case: "unitRegistration",
140
1196
  value: {
141
1197
  stateId: registration.stateId,
142
- params: registration.params
1198
+ params: toJsonObject(registration.params)
143
1199
  }
144
1200
  }
145
- };
1201
+ });
146
1202
  }
147
1203
  const registrationStream = await services.pubsubManager.subscribe([
148
1204
  "worker-unit-registration",
@@ -151,19 +1207,19 @@ function createWorkerService(services) {
151
1207
  ]);
152
1208
  for await (const event of registrationStream) {
153
1209
  if (event.type === "registered") {
154
- yield {
1210
+ yield create4(ConnectResponseSchema, {
155
1211
  event: {
156
- $case: "unitRegistration",
157
- value: { stateId: event.stateId, params: event.params }
1212
+ case: "unitRegistration",
1213
+ value: { stateId: event.stateId, params: toJsonObject(event.params) }
158
1214
  }
159
- };
1215
+ });
160
1216
  } else if (event.type === "deregistered") {
161
- yield {
1217
+ yield create4(ConnectResponseSchema, {
162
1218
  event: {
163
- $case: "unitDeregistration",
1219
+ case: "unitDeregistration",
164
1220
  value: { stateId: event.stateId }
165
1221
  }
166
- };
1222
+ });
167
1223
  }
168
1224
  }
169
1225
  } finally {
@@ -171,38 +1227,93 @@ function createWorkerService(services) {
171
1227
  }
172
1228
  },
173
1229
  async updateWorkerVersionMeta(request, context) {
174
- const [projectId] = await authenticate(services, context);
175
- const workerVersionId = parseArgument(request, "workerVersionId", z3.string());
176
- const payload = parseArgument(request, "meta", workerMetaUpdatePayloadSchema);
177
- const workerMeta = "workerMeta" in payload ? payload.workerMeta : payload;
178
- const serviceAccountMeta = "serviceAccountMeta" in payload ? payload.serviceAccountMeta : undefined;
179
- await services.workerService.updateWorkerVersionMeta(projectId, workerVersionId, workerMeta, serviceAccountMeta);
1230
+ const requestContext = await authenticateProject(services, request, context);
1231
+ const workerVersionId = parseArgument(request, "workerVersionId", z7.string());
1232
+ const workerMeta = parseArgument(request, "workerMeta", commonObjectMetaSchema);
1233
+ const serviceAccountMeta = parseArgument(request, "serviceAccountMeta", serviceAccountMetaSchema.optional());
1234
+ await services.workerService.updateWorkerVersionMeta(requestContext, workerVersionId, workerMeta, serviceAccountMeta);
180
1235
  return {};
181
1236
  }
182
1237
  };
183
1238
  }
184
1239
 
185
1240
  // src/index.ts
186
- async function startBackedApi(services) {
187
- const server = createServer();
188
- server.use(createErrorHandlingMiddleware(services));
189
- server.add(InstanceServiceDefinition, createInstanceService(services));
190
- server.add(PanelServiceDefinition, createPanelService(services));
191
- server.add(SecretServiceDefinition, createSecretService(services));
192
- server.add(WorkerServiceDefinition, createWorkerService(services));
1241
+ function createBackendApiHandler(services, options = {}) {
1242
+ return connectNodeAdapter({
1243
+ interceptors: [createErrorHandlingInterceptor(services)],
1244
+ requestPathPrefix: options.requestPathPrefix,
1245
+ routes(router) {
1246
+ router.service(InstanceStateService, createInstanceStateService(services));
1247
+ router.service(LibraryService, createLibraryService(services));
1248
+ router.service(OperationService, createOperationService(services));
1249
+ router.service(PanelService, createPanelService(services));
1250
+ router.service(ProjectModelService, createProjectModelService(services));
1251
+ router.service(ProjectService, createProjectService(services));
1252
+ router.service(SecretService, createSecretService(services));
1253
+ router.service(WorkerService, createWorkerService(services));
1254
+ }
1255
+ });
1256
+ }
1257
+ async function startBackendApi(services, options = {}) {
193
1258
  const uid = process.geteuid?.();
194
- const sockPath = `/run/user/${uid}/highstate.sock`;
1259
+ const workerSocketPath = options.workerSocketPath ?? `/run/user/${uid}/highstate.sock`;
1260
+ const workerAddress = `unix:${workerSocketPath}`;
1261
+ const address = options.address ?? workerAddress;
1262
+ const addresses = address === workerAddress ? [workerAddress] : [workerAddress, address];
1263
+ const handler = createBackendApiHandler(services);
1264
+ const servers = addresses.map(() => createServer(handler));
195
1265
  try {
196
- await rm(sockPath, { force: true });
1266
+ await rm(workerSocketPath, { force: true });
197
1267
  } catch (error) {
198
1268
  if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT")) {
199
1269
  services.logger.error({ error }, "failed to remove existing socket file");
200
1270
  }
201
1271
  }
202
- await server.listen(`unix:${sockPath}`);
203
- services.workerManager.config.HIGHSTATE_WORKER_API_PATH = sockPath;
204
- services.logger.info(`api listening at %s`, sockPath);
1272
+ try {
1273
+ for (const [index, server] of servers.entries()) {
1274
+ const serverAddress = addresses[index];
1275
+ await listen(server, serverAddress);
1276
+ services.logger.info(`api listening at "%s"`, serverAddress);
1277
+ }
1278
+ } catch (error) {
1279
+ await Promise.allSettled(servers.map(close));
1280
+ await rm(workerSocketPath, { force: true });
1281
+ throw new Error("Failed to start backend api", { cause: error });
1282
+ }
1283
+ services.workerManager.config.HIGHSTATE_WORKER_API_PATH = workerSocketPath;
1284
+ return {
1285
+ address,
1286
+ async shutdown() {
1287
+ await Promise.all(servers.map(close));
1288
+ await rm(workerSocketPath, { force: true });
1289
+ }
1290
+ };
1291
+ }
1292
+ async function listen(server, address) {
1293
+ await new Promise((resolve, reject) => {
1294
+ server.once("error", reject);
1295
+ server.once("listening", resolve);
1296
+ if (address.startsWith("unix:")) {
1297
+ server.listen(address.slice("unix:".length));
1298
+ return;
1299
+ }
1300
+ const url = new URL(address.includes("://") ? address : `http://${address}`);
1301
+ server.listen(Number(url.port), url.hostname);
1302
+ });
1303
+ }
1304
+ async function close(server) {
1305
+ await new Promise((resolve, reject) => {
1306
+ server.close((error) => {
1307
+ if (error) {
1308
+ reject(error);
1309
+ return;
1310
+ }
1311
+ resolve();
1312
+ });
1313
+ server.closeAllConnections();
1314
+ });
205
1315
  }
206
1316
  export {
207
- startBackedApi
1317
+ startBackendApi,
1318
+ createBackendApiHandler
208
1319
  };