@valon-technologies/gestalt 0.0.1-alpha.11 → 0.0.1-alpha.13

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/src/s3.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  ConnectError,
8
8
  createClient,
9
9
  type Client,
10
+ type Interceptor,
10
11
  type ServiceImpl,
11
12
  } from "@connectrpc/connect";
12
13
  import { createGrpcTransport } from "@connectrpc/connect-node";
@@ -19,12 +20,19 @@ import {
19
20
  PresignObjectResponseSchema,
20
21
  ReadObjectChunkSchema,
21
22
  S3 as S3Service,
23
+ type ByteRange as ProtoByteRange,
24
+ type ReadObjectRequest as ProtoReadObjectRequest,
25
+ type S3ObjectMeta as ProtoS3ObjectMeta,
26
+ type S3ObjectRef as ProtoS3ObjectRef,
22
27
  WriteObjectResponseSchema,
23
28
  } from "../gen/v1/s3_pb.ts";
24
29
  import { errorMessage, type MaybePromise } from "./api.ts";
25
30
  import { RuntimeProvider, type RuntimeProviderOptions } from "./provider.ts";
26
31
 
27
- const ENV_S3_SOCKET = "GESTALT_S3_SOCKET";
32
+ export const ENV_S3_SOCKET = "GESTALT_S3_SOCKET";
33
+ const S3_SOCKET_TOKEN_SUFFIX = "_TOKEN";
34
+ const S3_RELAY_TOKEN_HEADER = "x-gestalt-host-service-relay-token";
35
+ export const ENV_S3_SOCKET_TOKEN = `${ENV_S3_SOCKET}${S3_SOCKET_TOKEN_SUFFIX}`;
28
36
  const WRITE_CHUNK_SIZE = 64 * 1024;
29
37
  const textEncoder = new TextEncoder();
30
38
 
@@ -37,6 +45,13 @@ export function s3SocketEnv(name?: string): string {
37
45
  return `${ENV_S3_SOCKET}_${trimmed.replace(/[^A-Za-z0-9]/g, "_").toUpperCase()}`;
38
46
  }
39
47
 
48
+ /**
49
+ * Returns the environment variable name used to discover an S3 relay token.
50
+ */
51
+ export function s3SocketTokenEnv(name?: string): string {
52
+ return `${s3SocketEnv(name)}${S3_SOCKET_TOKEN_SUFFIX}`;
53
+ }
54
+
40
55
  /**
41
56
  * Error returned when an object reference does not exist.
42
57
  */
@@ -498,15 +513,30 @@ export class S3 {
498
513
 
499
514
  constructor(name?: string) {
500
515
  const envName = s3SocketEnv(name);
501
- const socketPath = process.env[envName];
502
- if (!socketPath) {
516
+ const target = process.env[envName];
517
+ if (!target) {
503
518
  throw new Error(`${envName} is not set`);
504
519
  }
520
+ const relayToken = process.env[s3SocketTokenEnv(name)]?.trim() ?? "";
521
+ const transportOptions = s3TransportOptions(target);
522
+ const interceptors: Interceptor[] = relayToken
523
+ ? [
524
+ (next) => async (req) => {
525
+ req.header.set(S3_RELAY_TOKEN_HEADER, relayToken);
526
+ return await next(req);
527
+ },
528
+ ]
529
+ : [];
505
530
  const transport = createGrpcTransport({
506
- baseUrl: "http://localhost",
507
- nodeOptions: {
508
- createConnection: () => connect(socketPath),
509
- },
531
+ ...transportOptions,
532
+ ...(transportOptions.nodeOptions
533
+ ? {
534
+ nodeOptions: {
535
+ createConnection: () => connect(transportOptions.nodeOptions!.path),
536
+ },
537
+ }
538
+ : {}),
539
+ interceptors,
510
540
  });
511
541
  this.client = createClient(S3Service, transport);
512
542
  }
@@ -740,6 +770,48 @@ export class S3Object {
740
770
  }
741
771
  }
742
772
 
773
+ function s3TransportOptions(rawTarget: string): {
774
+ baseUrl: string;
775
+ nodeOptions?: { path: string };
776
+ } {
777
+ const target = rawTarget.trim();
778
+ if (!target) {
779
+ throw new Error("s3 transport target is required");
780
+ }
781
+ if (target.startsWith("tcp://")) {
782
+ const address = target.slice("tcp://".length).trim();
783
+ if (!address) {
784
+ throw new Error(`s3 tcp target ${JSON.stringify(rawTarget)} is missing host:port`);
785
+ }
786
+ return { baseUrl: `http://${address}` };
787
+ }
788
+ if (target.startsWith("tls://")) {
789
+ const address = target.slice("tls://".length).trim();
790
+ if (!address) {
791
+ throw new Error(`s3 tls target ${JSON.stringify(rawTarget)} is missing host:port`);
792
+ }
793
+ return { baseUrl: `https://${address}` };
794
+ }
795
+ if (target.startsWith("unix://")) {
796
+ const socketPath = target.slice("unix://".length).trim();
797
+ if (!socketPath) {
798
+ throw new Error(`s3 unix target ${JSON.stringify(rawTarget)} is missing a socket path`);
799
+ }
800
+ return {
801
+ baseUrl: "http://localhost",
802
+ nodeOptions: { path: socketPath },
803
+ };
804
+ }
805
+ if (target.includes("://")) {
806
+ const parsed = new URL(target);
807
+ throw new Error(`Unsupported s3 target scheme ${JSON.stringify(parsed.protocol.replace(/:$/, ""))}`);
808
+ }
809
+ return {
810
+ baseUrl: "http://localhost",
811
+ nodeOptions: { path: target },
812
+ };
813
+ }
814
+
743
815
  async function invokeS3Provider<T>(label: string, fn: () => Promise<T>): Promise<T> {
744
816
  try {
745
817
  return await fn();
@@ -967,7 +1039,7 @@ function toProtoObjectRef(ref: ObjectRef) {
967
1039
  };
968
1040
  }
969
1041
 
970
- function fromProtoObjectRef(ref: { bucket?: string; key?: string; versionId?: string } | undefined): ObjectRef {
1042
+ function fromProtoObjectRef(ref: ProtoS3ObjectRef | undefined): ObjectRef {
971
1043
  const value: ObjectRef = {
972
1044
  bucket: ref?.bucket ?? "",
973
1045
  key: ref?.key ?? "",
@@ -1001,15 +1073,7 @@ function toProtoObjectMeta(meta: ObjectMeta) {
1001
1073
  return value;
1002
1074
  }
1003
1075
 
1004
- function fromProtoObjectMeta(meta: {
1005
- ref?: { bucket?: string; key?: string; versionId?: string };
1006
- etag?: string;
1007
- size?: bigint;
1008
- contentType?: string;
1009
- lastModified?: { seconds?: bigint; nanos?: number };
1010
- metadata?: Record<string, string>;
1011
- storageClass?: string;
1012
- } | undefined): ObjectMeta {
1076
+ function fromProtoObjectMeta(meta: ProtoS3ObjectMeta | undefined): ObjectMeta {
1013
1077
  const value: ObjectMeta = {
1014
1078
  ref: fromProtoObjectRef(meta?.ref),
1015
1079
  etag: meta?.etag ?? "",
@@ -1041,27 +1105,21 @@ function toProtoReadOptions(options?: ReadOptions) {
1041
1105
  return proto;
1042
1106
  }
1043
1107
 
1044
- function fromProtoReadOptions(request: {
1045
- range?: { start?: bigint; end?: bigint };
1046
- ifMatch?: string;
1047
- ifNoneMatch?: string;
1048
- ifModifiedSince?: { seconds?: bigint; nanos?: number };
1049
- ifUnmodifiedSince?: { seconds?: bigint; nanos?: number };
1050
- }): ReadOptions {
1108
+ function fromProtoReadOptions(request: ProtoReadObjectRequest | undefined): ReadOptions {
1051
1109
  const options: ReadOptions = {};
1052
- if (request.range) {
1110
+ if (request?.range) {
1053
1111
  options.range = fromProtoByteRange(request.range);
1054
1112
  }
1055
- if (request.ifMatch) {
1113
+ if (request?.ifMatch) {
1056
1114
  options.ifMatch = request.ifMatch;
1057
1115
  }
1058
- if (request.ifNoneMatch) {
1116
+ if (request?.ifNoneMatch) {
1059
1117
  options.ifNoneMatch = request.ifNoneMatch;
1060
1118
  }
1061
- if (request.ifModifiedSince) {
1119
+ if (request?.ifModifiedSince) {
1062
1120
  options.ifModifiedSince = fromProtoTimestamp(request.ifModifiedSince);
1063
1121
  }
1064
- if (request.ifUnmodifiedSince) {
1122
+ if (request?.ifUnmodifiedSince) {
1065
1123
  options.ifUnmodifiedSince = fromProtoTimestamp(request.ifUnmodifiedSince);
1066
1124
  }
1067
1125
  return options;
@@ -1116,12 +1174,12 @@ function toProtoByteRange(range: ByteRange) {
1116
1174
  return proto;
1117
1175
  }
1118
1176
 
1119
- function fromProtoByteRange(range: { start?: bigint; end?: bigint }): ByteRange {
1177
+ function fromProtoByteRange(range: ProtoByteRange | undefined): ByteRange {
1120
1178
  const value: ByteRange = {};
1121
- if (range.start !== undefined) {
1179
+ if (range?.start !== undefined) {
1122
1180
  value.start = range.start;
1123
1181
  }
1124
- if (range.end !== undefined) {
1182
+ if (range?.end !== undefined) {
1125
1183
  value.end = range.end;
1126
1184
  }
1127
1185
  return value;
@@ -1,42 +1,79 @@
1
- import { connect } from "node:net";
2
-
3
1
  import type { MessageInitShape } from "@bufbuild/protobuf";
4
- import { createClient, type Client } from "@connectrpc/connect";
2
+ import {
3
+ createClient,
4
+ type Client,
5
+ type Interceptor,
6
+ } from "@connectrpc/connect";
5
7
  import { createGrpcTransport } from "@connectrpc/connect-node";
6
8
 
7
9
  import {
8
10
  WorkflowManagerCreateScheduleRequestSchema,
11
+ WorkflowManagerCreateEventTriggerRequestSchema,
9
12
  WorkflowManagerDeleteScheduleRequestSchema,
13
+ WorkflowManagerDeleteEventTriggerRequestSchema,
10
14
  WorkflowManagerGetScheduleRequestSchema,
15
+ WorkflowManagerGetEventTriggerRequestSchema,
11
16
  WorkflowManagerHost as WorkflowManagerHostService,
12
17
  WorkflowManagerPauseScheduleRequestSchema,
18
+ WorkflowManagerPauseEventTriggerRequestSchema,
19
+ WorkflowManagerPublishEventRequestSchema,
13
20
  WorkflowManagerResumeScheduleRequestSchema,
21
+ WorkflowManagerResumeEventTriggerRequestSchema,
14
22
  WorkflowManagerUpdateScheduleRequestSchema,
23
+ WorkflowManagerUpdateEventTriggerRequestSchema,
15
24
  type ManagedWorkflowSchedule,
25
+ type ManagedWorkflowEventTrigger,
26
+ type WorkflowEvent,
16
27
  } from "../gen/v1/workflow_pb.ts";
17
28
  import type { Request } from "./api.ts";
18
29
 
19
30
  export const ENV_WORKFLOW_MANAGER_SOCKET = "GESTALT_WORKFLOW_MANAGER_SOCKET";
31
+ export const ENV_WORKFLOW_MANAGER_SOCKET_TOKEN = `${ENV_WORKFLOW_MANAGER_SOCKET}_TOKEN`;
32
+ const WORKFLOW_MANAGER_RELAY_TOKEN_HEADER =
33
+ "x-gestalt-host-service-relay-token";
20
34
 
21
35
  export type ManagedWorkflowScheduleMessage = ManagedWorkflowSchedule;
36
+ export type ManagedWorkflowEventTriggerMessage = ManagedWorkflowEventTrigger;
37
+ export type WorkflowEventMessage = WorkflowEvent;
22
38
  export type WorkflowManagerCreateScheduleInput = MessageInitShape<
23
39
  typeof WorkflowManagerCreateScheduleRequestSchema
24
40
  >;
41
+ export type WorkflowManagerCreateTriggerInput = MessageInitShape<
42
+ typeof WorkflowManagerCreateEventTriggerRequestSchema
43
+ >;
25
44
  export type WorkflowManagerGetScheduleInput = MessageInitShape<
26
45
  typeof WorkflowManagerGetScheduleRequestSchema
27
46
  >;
47
+ export type WorkflowManagerGetTriggerInput = MessageInitShape<
48
+ typeof WorkflowManagerGetEventTriggerRequestSchema
49
+ >;
28
50
  export type WorkflowManagerUpdateScheduleInput = MessageInitShape<
29
51
  typeof WorkflowManagerUpdateScheduleRequestSchema
30
52
  >;
53
+ export type WorkflowManagerUpdateTriggerInput = MessageInitShape<
54
+ typeof WorkflowManagerUpdateEventTriggerRequestSchema
55
+ >;
31
56
  export type WorkflowManagerDeleteScheduleInput = MessageInitShape<
32
57
  typeof WorkflowManagerDeleteScheduleRequestSchema
33
58
  >;
59
+ export type WorkflowManagerDeleteTriggerInput = MessageInitShape<
60
+ typeof WorkflowManagerDeleteEventTriggerRequestSchema
61
+ >;
34
62
  export type WorkflowManagerPauseScheduleInput = MessageInitShape<
35
63
  typeof WorkflowManagerPauseScheduleRequestSchema
36
64
  >;
65
+ export type WorkflowManagerPauseTriggerInput = MessageInitShape<
66
+ typeof WorkflowManagerPauseEventTriggerRequestSchema
67
+ >;
37
68
  export type WorkflowManagerResumeScheduleInput = MessageInitShape<
38
69
  typeof WorkflowManagerResumeScheduleRequestSchema
39
70
  >;
71
+ export type WorkflowManagerResumeTriggerInput = MessageInitShape<
72
+ typeof WorkflowManagerResumeEventTriggerRequestSchema
73
+ >;
74
+ export type WorkflowManagerPublishEventInput = MessageInitShape<
75
+ typeof WorkflowManagerPublishEventRequestSchema
76
+ >;
40
77
 
41
78
  export class WorkflowManager {
42
79
  private readonly client: Client<typeof WorkflowManagerHostService>;
@@ -47,18 +84,20 @@ export class WorkflowManager {
47
84
  constructor(requestOrToken: Request | string) {
48
85
  this.invocationToken = normalizeInvocationToken(requestOrToken);
49
86
 
50
- const socketPath = process.env[ENV_WORKFLOW_MANAGER_SOCKET];
51
- if (!socketPath) {
87
+ const target = process.env[ENV_WORKFLOW_MANAGER_SOCKET];
88
+ if (!target) {
52
89
  throw new Error(
53
90
  `workflow manager: ${ENV_WORKFLOW_MANAGER_SOCKET} is not set`,
54
91
  );
55
92
  }
93
+ const relayToken =
94
+ process.env[ENV_WORKFLOW_MANAGER_SOCKET_TOKEN]?.trim() ?? "";
56
95
 
57
96
  const transport = createGrpcTransport({
58
- baseUrl: "http://localhost",
59
- nodeOptions: {
60
- createConnection: () => connect(socketPath),
61
- },
97
+ ...workflowManagerTransportOptions(target),
98
+ interceptors: relayToken
99
+ ? [workflowManagerRelayTokenInterceptor(relayToken)]
100
+ : [],
62
101
  });
63
102
  this.client = createClient(WorkflowManagerHostService, transport);
64
103
  }
@@ -116,6 +155,69 @@ export class WorkflowManager {
116
155
  invocationToken: this.invocationToken,
117
156
  });
118
157
  }
158
+
159
+ async createTrigger(
160
+ request: WorkflowManagerCreateTriggerInput,
161
+ ): Promise<ManagedWorkflowEventTriggerMessage> {
162
+ return await this.client.createEventTrigger({
163
+ ...request,
164
+ invocationToken: this.invocationToken,
165
+ });
166
+ }
167
+
168
+ async getTrigger(
169
+ request: WorkflowManagerGetTriggerInput,
170
+ ): Promise<ManagedWorkflowEventTriggerMessage> {
171
+ return await this.client.getEventTrigger({
172
+ ...request,
173
+ invocationToken: this.invocationToken,
174
+ });
175
+ }
176
+
177
+ async updateTrigger(
178
+ request: WorkflowManagerUpdateTriggerInput,
179
+ ): Promise<ManagedWorkflowEventTriggerMessage> {
180
+ return await this.client.updateEventTrigger({
181
+ ...request,
182
+ invocationToken: this.invocationToken,
183
+ });
184
+ }
185
+
186
+ async deleteTrigger(
187
+ request: WorkflowManagerDeleteTriggerInput,
188
+ ): Promise<void> {
189
+ await this.client.deleteEventTrigger({
190
+ ...request,
191
+ invocationToken: this.invocationToken,
192
+ });
193
+ }
194
+
195
+ async pauseTrigger(
196
+ request: WorkflowManagerPauseTriggerInput,
197
+ ): Promise<ManagedWorkflowEventTriggerMessage> {
198
+ return await this.client.pauseEventTrigger({
199
+ ...request,
200
+ invocationToken: this.invocationToken,
201
+ });
202
+ }
203
+
204
+ async resumeTrigger(
205
+ request: WorkflowManagerResumeTriggerInput,
206
+ ): Promise<ManagedWorkflowEventTriggerMessage> {
207
+ return await this.client.resumeEventTrigger({
208
+ ...request,
209
+ invocationToken: this.invocationToken,
210
+ });
211
+ }
212
+
213
+ async publishEvent(
214
+ request: WorkflowManagerPublishEventInput,
215
+ ): Promise<WorkflowEventMessage> {
216
+ return await this.client.publishEvent({
217
+ ...request,
218
+ invocationToken: this.invocationToken,
219
+ });
220
+ }
119
221
  }
120
222
 
121
223
  function normalizeInvocationToken(requestOrToken: Request | string): string {
@@ -129,3 +231,54 @@ function normalizeInvocationToken(requestOrToken: Request | string): string {
129
231
  }
130
232
  return trimmed;
131
233
  }
234
+
235
+ function workflowManagerTransportOptions(rawTarget: string): {
236
+ baseUrl: string;
237
+ nodeOptions?: { path: string };
238
+ } {
239
+ const target = rawTarget.trim();
240
+ if (!target) {
241
+ throw new Error("workflow manager: transport target is required");
242
+ }
243
+ if (target.startsWith("tcp://")) {
244
+ const address = target.slice("tcp://".length).trim();
245
+ if (!address) {
246
+ throw new Error(
247
+ `workflow manager: tcp target ${JSON.stringify(rawTarget)} is missing host:port`,
248
+ );
249
+ }
250
+ return { baseUrl: `http://${address}` };
251
+ }
252
+ if (target.startsWith("tls://")) {
253
+ const address = target.slice("tls://".length).trim();
254
+ if (!address) {
255
+ throw new Error(
256
+ `workflow manager: tls target ${JSON.stringify(rawTarget)} is missing host:port`,
257
+ );
258
+ }
259
+ return { baseUrl: `https://${address}` };
260
+ }
261
+ if (target.startsWith("unix://")) {
262
+ const socketPath = target.slice("unix://".length).trim();
263
+ if (!socketPath) {
264
+ throw new Error(
265
+ `workflow manager: unix target ${JSON.stringify(rawTarget)} is missing a socket path`,
266
+ );
267
+ }
268
+ return { baseUrl: "http://localhost", nodeOptions: { path: socketPath } };
269
+ }
270
+ if (target.includes("://")) {
271
+ const parsed = new URL(target);
272
+ throw new Error(
273
+ `workflow manager: unsupported target scheme ${JSON.stringify(parsed.protocol.replace(/:$/, ""))}`,
274
+ );
275
+ }
276
+ return { baseUrl: "http://localhost", nodeOptions: { path: target } };
277
+ }
278
+
279
+ function workflowManagerRelayTokenInterceptor(token: string): Interceptor {
280
+ return (next) => async (req) => {
281
+ req.header.set(WORKFLOW_MANAGER_RELAY_TOKEN_HEADER, token);
282
+ return next(req);
283
+ };
284
+ }