@valon-technologies/gestalt 0.0.1-alpha.12 → 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
@@ -1,3 +1,5 @@
1
+ import { connect } from "node:net";
2
+
1
3
  import { create } from "@bufbuild/protobuf";
2
4
  import { EmptySchema } from "@bufbuild/protobuf/wkt";
3
5
  import {
@@ -5,6 +7,7 @@ import {
5
7
  ConnectError,
6
8
  createClient,
7
9
  type Client,
10
+ type Interceptor,
8
11
  type ServiceImpl,
9
12
  } from "@connectrpc/connect";
10
13
  import { createGrpcTransport } from "@connectrpc/connect-node";
@@ -17,12 +20,19 @@ import {
17
20
  PresignObjectResponseSchema,
18
21
  ReadObjectChunkSchema,
19
22
  S3 as S3Service,
23
+ type ByteRange as ProtoByteRange,
24
+ type ReadObjectRequest as ProtoReadObjectRequest,
25
+ type S3ObjectMeta as ProtoS3ObjectMeta,
26
+ type S3ObjectRef as ProtoS3ObjectRef,
20
27
  WriteObjectResponseSchema,
21
28
  } from "../gen/v1/s3_pb.ts";
22
29
  import { errorMessage, type MaybePromise } from "./api.ts";
23
30
  import { RuntimeProvider, type RuntimeProviderOptions } from "./provider.ts";
24
31
 
25
- 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}`;
26
36
  const WRITE_CHUNK_SIZE = 64 * 1024;
27
37
  const textEncoder = new TextEncoder();
28
38
 
@@ -35,6 +45,13 @@ export function s3SocketEnv(name?: string): string {
35
45
  return `${ENV_S3_SOCKET}_${trimmed.replace(/[^A-Za-z0-9]/g, "_").toUpperCase()}`;
36
46
  }
37
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
+
38
55
  /**
39
56
  * Error returned when an object reference does not exist.
40
57
  */
@@ -496,15 +513,30 @@ export class S3 {
496
513
 
497
514
  constructor(name?: string) {
498
515
  const envName = s3SocketEnv(name);
499
- const socketPath = process.env[envName];
500
- if (!socketPath) {
516
+ const target = process.env[envName];
517
+ if (!target) {
501
518
  throw new Error(`${envName} is not set`);
502
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
+ : [];
503
530
  const transport = createGrpcTransport({
504
- baseUrl: unixSocketBaseUrl(socketPath),
505
- nodeOptions: {
506
- path: socketPath,
507
- },
531
+ ...transportOptions,
532
+ ...(transportOptions.nodeOptions
533
+ ? {
534
+ nodeOptions: {
535
+ createConnection: () => connect(transportOptions.nodeOptions!.path),
536
+ },
537
+ }
538
+ : {}),
539
+ interceptors,
508
540
  });
509
541
  this.client = createClient(S3Service, transport);
510
542
  }
@@ -738,13 +770,46 @@ export class S3Object {
738
770
  }
739
771
  }
740
772
 
741
- function unixSocketBaseUrl(socketPath: string): string {
742
- let hash = 0x811c9dc5;
743
- for (const char of socketPath) {
744
- hash ^= char.charCodeAt(0);
745
- hash = Math.imul(hash, 0x01000193);
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
+ };
746
804
  }
747
- return `http://unix-${(hash >>> 0).toString(16)}.local`;
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
+ };
748
813
  }
749
814
 
750
815
  async function invokeS3Provider<T>(label: string, fn: () => Promise<T>): Promise<T> {
@@ -974,7 +1039,7 @@ function toProtoObjectRef(ref: ObjectRef) {
974
1039
  };
975
1040
  }
976
1041
 
977
- function fromProtoObjectRef(ref: { bucket?: string; key?: string; versionId?: string } | undefined): ObjectRef {
1042
+ function fromProtoObjectRef(ref: ProtoS3ObjectRef | undefined): ObjectRef {
978
1043
  const value: ObjectRef = {
979
1044
  bucket: ref?.bucket ?? "",
980
1045
  key: ref?.key ?? "",
@@ -1008,15 +1073,7 @@ function toProtoObjectMeta(meta: ObjectMeta) {
1008
1073
  return value;
1009
1074
  }
1010
1075
 
1011
- function fromProtoObjectMeta(meta: {
1012
- ref?: { bucket?: string; key?: string; versionId?: string };
1013
- etag?: string;
1014
- size?: bigint;
1015
- contentType?: string;
1016
- lastModified?: { seconds?: bigint; nanos?: number };
1017
- metadata?: Record<string, string>;
1018
- storageClass?: string;
1019
- } | undefined): ObjectMeta {
1076
+ function fromProtoObjectMeta(meta: ProtoS3ObjectMeta | undefined): ObjectMeta {
1020
1077
  const value: ObjectMeta = {
1021
1078
  ref: fromProtoObjectRef(meta?.ref),
1022
1079
  etag: meta?.etag ?? "",
@@ -1048,27 +1105,21 @@ function toProtoReadOptions(options?: ReadOptions) {
1048
1105
  return proto;
1049
1106
  }
1050
1107
 
1051
- function fromProtoReadOptions(request: {
1052
- range?: { start?: bigint; end?: bigint };
1053
- ifMatch?: string;
1054
- ifNoneMatch?: string;
1055
- ifModifiedSince?: { seconds?: bigint; nanos?: number };
1056
- ifUnmodifiedSince?: { seconds?: bigint; nanos?: number };
1057
- }): ReadOptions {
1108
+ function fromProtoReadOptions(request: ProtoReadObjectRequest | undefined): ReadOptions {
1058
1109
  const options: ReadOptions = {};
1059
- if (request.range) {
1110
+ if (request?.range) {
1060
1111
  options.range = fromProtoByteRange(request.range);
1061
1112
  }
1062
- if (request.ifMatch) {
1113
+ if (request?.ifMatch) {
1063
1114
  options.ifMatch = request.ifMatch;
1064
1115
  }
1065
- if (request.ifNoneMatch) {
1116
+ if (request?.ifNoneMatch) {
1066
1117
  options.ifNoneMatch = request.ifNoneMatch;
1067
1118
  }
1068
- if (request.ifModifiedSince) {
1119
+ if (request?.ifModifiedSince) {
1069
1120
  options.ifModifiedSince = fromProtoTimestamp(request.ifModifiedSince);
1070
1121
  }
1071
- if (request.ifUnmodifiedSince) {
1122
+ if (request?.ifUnmodifiedSince) {
1072
1123
  options.ifUnmodifiedSince = fromProtoTimestamp(request.ifUnmodifiedSince);
1073
1124
  }
1074
1125
  return options;
@@ -1123,12 +1174,12 @@ function toProtoByteRange(range: ByteRange) {
1123
1174
  return proto;
1124
1175
  }
1125
1176
 
1126
- function fromProtoByteRange(range: { start?: bigint; end?: bigint }): ByteRange {
1177
+ function fromProtoByteRange(range: ProtoByteRange | undefined): ByteRange {
1127
1178
  const value: ByteRange = {};
1128
- if (range.start !== undefined) {
1179
+ if (range?.start !== undefined) {
1129
1180
  value.start = range.start;
1130
1181
  }
1131
- if (range.end !== undefined) {
1182
+ if (range?.end !== undefined) {
1132
1183
  value.end = range.end;
1133
1184
  }
1134
1185
  return value;
@@ -1,7 +1,9 @@
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 {
@@ -26,6 +28,9 @@ import {
26
28
  import type { Request } from "./api.ts";
27
29
 
28
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";
29
34
 
30
35
  export type ManagedWorkflowScheduleMessage = ManagedWorkflowSchedule;
31
36
  export type ManagedWorkflowEventTriggerMessage = ManagedWorkflowEventTrigger;
@@ -79,18 +84,20 @@ export class WorkflowManager {
79
84
  constructor(requestOrToken: Request | string) {
80
85
  this.invocationToken = normalizeInvocationToken(requestOrToken);
81
86
 
82
- const socketPath = process.env[ENV_WORKFLOW_MANAGER_SOCKET];
83
- if (!socketPath) {
87
+ const target = process.env[ENV_WORKFLOW_MANAGER_SOCKET];
88
+ if (!target) {
84
89
  throw new Error(
85
90
  `workflow manager: ${ENV_WORKFLOW_MANAGER_SOCKET} is not set`,
86
91
  );
87
92
  }
93
+ const relayToken =
94
+ process.env[ENV_WORKFLOW_MANAGER_SOCKET_TOKEN]?.trim() ?? "";
88
95
 
89
96
  const transport = createGrpcTransport({
90
- baseUrl: "http://localhost",
91
- nodeOptions: {
92
- createConnection: () => connect(socketPath),
93
- },
97
+ ...workflowManagerTransportOptions(target),
98
+ interceptors: relayToken
99
+ ? [workflowManagerRelayTokenInterceptor(relayToken)]
100
+ : [],
94
101
  });
95
102
  this.client = createClient(WorkflowManagerHostService, transport);
96
103
  }
@@ -224,3 +231,54 @@ function normalizeInvocationToken(requestOrToken: Request | string): string {
224
231
  }
225
232
  return trimmed;
226
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
+ }