@valon-technologies/gestalt 0.0.1-alpha.12 → 0.0.1-alpha.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,244 @@
1
+ import { connect } from "node:net";
2
+ import { Writable } from "node:stream";
3
+
4
+ import type { MessageInitShape } from "@bufbuild/protobuf";
5
+ import {
6
+ createClient,
7
+ type Client,
8
+ type Interceptor,
9
+ } from "@connectrpc/connect";
10
+ import { createGrpcTransport } from "@connectrpc/connect-node";
11
+
12
+ import {
13
+ AppendPluginRuntimeLogsRequestSchema,
14
+ type AppendPluginRuntimeLogsResponse,
15
+ PluginRuntimeLogHost as PluginRuntimeLogHostService,
16
+ PluginRuntimeLogStream,
17
+ } from "../gen/v1/pluginruntime_pb.ts";
18
+
19
+ export const ENV_RUNTIME_LOG_HOST_SOCKET = "GESTALT_RUNTIME_LOG_SOCKET";
20
+ export const ENV_RUNTIME_LOG_HOST_SOCKET_TOKEN = `${ENV_RUNTIME_LOG_HOST_SOCKET}_TOKEN`;
21
+ export const ENV_RUNTIME_SESSION_ID = "GESTALT_RUNTIME_SESSION_ID";
22
+
23
+ const RUNTIME_LOG_RELAY_TOKEN_HEADER = "x-gestalt-host-service-relay-token";
24
+
25
+ export type RuntimeLogStreamName = "stdout" | "stderr" | "runtime";
26
+ export type RuntimeLogStreamInput =
27
+ | RuntimeLogStreamName
28
+ | PluginRuntimeLogStream;
29
+ export type RuntimeLogAppendLogsInput = MessageInitShape<
30
+ typeof AppendPluginRuntimeLogsRequestSchema
31
+ >;
32
+ export type RuntimeLogAppendResponseMessage = AppendPluginRuntimeLogsResponse;
33
+
34
+ export interface RuntimeLogAppendInput {
35
+ sessionId?: string;
36
+ message: string | Uint8Array;
37
+ stream?: RuntimeLogStreamInput;
38
+ observedAt?: Date;
39
+ sourceSeq?: number | bigint;
40
+ }
41
+
42
+ export interface RuntimeLogWriterOptions {
43
+ sessionId?: string;
44
+ stream?: RuntimeLogStreamInput;
45
+ sourceSeqStart?: number | bigint;
46
+ }
47
+
48
+ export class RuntimeLogHost {
49
+ private readonly client: Client<typeof PluginRuntimeLogHostService>;
50
+ private sourceSeq = 0n;
51
+
52
+ constructor() {
53
+ const target = process.env[ENV_RUNTIME_LOG_HOST_SOCKET];
54
+ if (!target) {
55
+ throw new Error(
56
+ `runtime log host: ${ENV_RUNTIME_LOG_HOST_SOCKET} is not set`,
57
+ );
58
+ }
59
+ const relayToken =
60
+ process.env[ENV_RUNTIME_LOG_HOST_SOCKET_TOKEN]?.trim() ?? "";
61
+ const transportOptions = runtimeLogTransportOptions(target);
62
+ const transport = createGrpcTransport({
63
+ ...transportOptions,
64
+ ...(transportOptions.nodeOptions
65
+ ? {
66
+ nodeOptions: {
67
+ createConnection: () =>
68
+ connect(transportOptions.nodeOptions!.path),
69
+ },
70
+ }
71
+ : {}),
72
+ interceptors: relayToken
73
+ ? [runtimeLogRelayTokenInterceptor(relayToken)]
74
+ : [],
75
+ });
76
+ this.client = createClient(PluginRuntimeLogHostService, transport);
77
+ }
78
+
79
+ async appendLogs(
80
+ request: RuntimeLogAppendLogsInput,
81
+ ): Promise<RuntimeLogAppendResponseMessage> {
82
+ return await this.client.appendLogs(request);
83
+ }
84
+
85
+ async append(
86
+ input: RuntimeLogAppendInput,
87
+ ): Promise<RuntimeLogAppendResponseMessage> {
88
+ const sourceSeq =
89
+ input.sourceSeq === undefined
90
+ ? (this.sourceSeq += 1n)
91
+ : BigInt(input.sourceSeq);
92
+ if (sourceSeq > this.sourceSeq) {
93
+ this.sourceSeq = sourceSeq;
94
+ }
95
+ return await this.appendLogs({
96
+ sessionId: runtimeSessionId(input.sessionId),
97
+ logs: [
98
+ {
99
+ stream: runtimeLogStream(input.stream ?? "runtime"),
100
+ message: runtimeLogMessage(input.message),
101
+ observedAt: toProtoTimestamp(input.observedAt ?? new Date()),
102
+ sourceSeq,
103
+ },
104
+ ],
105
+ });
106
+ }
107
+
108
+ writer(options?: RuntimeLogWriterOptions): Writable;
109
+ writer(sessionId: string, options?: RuntimeLogWriterOptions): Writable;
110
+ writer(
111
+ sessionIdOrOptions: string | RuntimeLogWriterOptions = {},
112
+ options: RuntimeLogWriterOptions = {},
113
+ ): Writable {
114
+ const writerOptions =
115
+ typeof sessionIdOrOptions === "string"
116
+ ? options
117
+ : sessionIdOrOptions;
118
+ const sessionId = runtimeSessionId(
119
+ typeof sessionIdOrOptions === "string"
120
+ ? sessionIdOrOptions
121
+ : writerOptions.sessionId,
122
+ );
123
+ const stream = writerOptions.stream ?? "stdout";
124
+ let sourceSeq = BigInt(writerOptions.sourceSeqStart ?? 0);
125
+
126
+ return new Writable({
127
+ write: (chunk: Buffer | string, encoding, callback) => {
128
+ const actualEncoding = (
129
+ String(encoding) === "buffer" ? "utf8" : encoding
130
+ ) as BufferEncoding;
131
+ const message =
132
+ typeof chunk === "string"
133
+ ? chunk
134
+ : Buffer.from(chunk).toString(actualEncoding);
135
+ sourceSeq += 1n;
136
+ this.append({
137
+ sessionId,
138
+ stream,
139
+ message,
140
+ sourceSeq,
141
+ }).then(
142
+ () => callback(),
143
+ (error: unknown) => callback(toError(error)),
144
+ );
145
+ },
146
+ });
147
+ }
148
+ }
149
+
150
+ function runtimeSessionId(sessionId?: string): string {
151
+ const value = (sessionId ?? process.env[ENV_RUNTIME_SESSION_ID] ?? "").trim();
152
+ if (!value) {
153
+ throw new Error(`runtime session: ${ENV_RUNTIME_SESSION_ID} is not set`);
154
+ }
155
+ return value;
156
+ }
157
+
158
+ function runtimeLogTransportOptions(rawTarget: string): {
159
+ baseUrl: string;
160
+ nodeOptions?: { path: string };
161
+ } {
162
+ const target = rawTarget.trim();
163
+ if (!target) {
164
+ throw new Error("runtime log host: transport target is required");
165
+ }
166
+ if (target.startsWith("tcp://")) {
167
+ const address = target.slice("tcp://".length).trim();
168
+ if (!address) {
169
+ throw new Error(
170
+ `runtime log host: tcp target ${JSON.stringify(rawTarget)} is missing host:port`,
171
+ );
172
+ }
173
+ return { baseUrl: `http://${address}` };
174
+ }
175
+ if (target.startsWith("tls://")) {
176
+ const address = target.slice("tls://".length).trim();
177
+ if (!address) {
178
+ throw new Error(
179
+ `runtime log host: tls target ${JSON.stringify(rawTarget)} is missing host:port`,
180
+ );
181
+ }
182
+ return { baseUrl: `https://${address}` };
183
+ }
184
+ if (target.startsWith("unix://")) {
185
+ const socketPath = target.slice("unix://".length).trim();
186
+ if (!socketPath) {
187
+ throw new Error(
188
+ `runtime log host: unix target ${JSON.stringify(rawTarget)} is missing a socket path`,
189
+ );
190
+ }
191
+ return { baseUrl: "http://localhost", nodeOptions: { path: socketPath } };
192
+ }
193
+ if (target.includes("://")) {
194
+ const parsed = new URL(target);
195
+ throw new Error(
196
+ `runtime log host: unsupported target scheme ${JSON.stringify(parsed.protocol.replace(/:$/, ""))}`,
197
+ );
198
+ }
199
+ return { baseUrl: "http://localhost", nodeOptions: { path: target } };
200
+ }
201
+
202
+ function runtimeLogRelayTokenInterceptor(token: string): Interceptor {
203
+ return (next) => async (req) => {
204
+ req.header.set(RUNTIME_LOG_RELAY_TOKEN_HEADER, token);
205
+ return await next(req);
206
+ };
207
+ }
208
+
209
+ function runtimeLogStream(stream: RuntimeLogStreamInput): PluginRuntimeLogStream {
210
+ if (typeof stream === "number") {
211
+ return stream;
212
+ }
213
+ switch (stream.trim().toLowerCase()) {
214
+ case "stdout":
215
+ return PluginRuntimeLogStream.STDOUT;
216
+ case "stderr":
217
+ return PluginRuntimeLogStream.STDERR;
218
+ case "runtime":
219
+ return PluginRuntimeLogStream.RUNTIME;
220
+ default:
221
+ throw new Error(`unsupported runtime log stream ${JSON.stringify(stream)}`);
222
+ }
223
+ }
224
+
225
+ function runtimeLogMessage(message: string | Uint8Array): string {
226
+ if (typeof message === "string") {
227
+ return message;
228
+ }
229
+ return Buffer.from(message).toString("utf8");
230
+ }
231
+
232
+ function toProtoTimestamp(value: Date): { seconds: bigint; nanos: number } {
233
+ const millis = value.getTime();
234
+ const seconds = Math.floor(millis / 1000);
235
+ const nanos = Math.trunc((millis - (seconds * 1000)) * 1_000_000);
236
+ return {
237
+ seconds: BigInt(seconds),
238
+ nanos,
239
+ };
240
+ }
241
+
242
+ function toError(error: unknown): Error {
243
+ return error instanceof Error ? error : new Error(String(error));
244
+ }
package/src/runtime.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, rmSync, writeFileSync } from "node:fs";
1
+ import { rmSync, writeFileSync } from "node:fs";
2
2
  import { createServer } from "node:http2";
3
3
  import { dirname, resolve } from "node:path";
4
4
 
@@ -12,6 +12,9 @@ import {
12
12
  } from "@connectrpc/connect";
13
13
  import { connectNodeAdapter } from "@connectrpc/connect-node";
14
14
 
15
+ import {
16
+ AgentProvider as AgentProviderService,
17
+ } from "../gen/v1/agent_pb.ts";
15
18
  import {
16
19
  AuthenticationProvider as AuthenticationProviderService,
17
20
  AuthSessionSettingsSchema,
@@ -40,10 +43,12 @@ import {
40
43
  CatalogSchema as ProtoCatalogSchema,
41
44
  ConnectionMode as ProviderConnectionMode,
42
45
  GetSessionCatalogResponseSchema,
46
+ PostConnectResponseSchema,
43
47
  ResolveHTTPSubjectResponseSchema,
44
48
  OperationResultSchema,
45
49
  ProviderMetadataSchema,
46
50
  type HTTPSubjectRequest as ProtoHTTPSubjectRequest,
51
+ type PostConnectCredential as ProtoPostConnectCredential,
47
52
  type RequestContext as ProtoRequestContext,
48
53
  type ResolveHTTPSubjectRequest as ProtoResolveHTTPSubjectRequest,
49
54
  IntegrationProvider as IntegrationProviderService,
@@ -52,17 +57,26 @@ import {
52
57
  type GetSessionCatalogRequest,
53
58
  type StartProviderRequest,
54
59
  } from "../gen/v1/plugin_pb.ts";
60
+ import {
61
+ PluginRuntimeProvider as PluginRuntimeProviderService,
62
+ } from "../gen/v1/pluginruntime_pb.ts";
55
63
  import {
56
64
  ConfigureProviderResponseSchema,
57
65
  HealthCheckResponseSchema,
58
66
  ProviderIdentitySchema,
59
67
  ProviderKind as ProtoProviderKind,
60
68
  ProviderLifecycle,
69
+ StartRuntimeProviderResponseSchema,
61
70
  type ConfigureProviderRequest,
62
71
  } from "../gen/v1/runtime_pb.ts";
63
72
  import { S3 as S3Service } from "../gen/v1/s3_pb.ts";
64
73
  import { WorkflowProvider as WorkflowProviderService } from "../gen/v1/workflow_pb.ts";
65
74
  import { errorMessage, type Request } from "./api.ts";
75
+ import {
76
+ AgentProvider,
77
+ createAgentProviderService,
78
+ isAgentProvider,
79
+ } from "./agent.ts";
66
80
  import {
67
81
  AuthenticationProvider,
68
82
  isAuthenticationProvider,
@@ -77,11 +91,17 @@ import {
77
91
  type HTTPSubjectResolutionContext,
78
92
  } from "./http-subject.ts";
79
93
  import {
94
+ type ConnectedToken,
80
95
  PluginProvider,
81
96
  connectionModeToProtoValue,
82
97
  connectionParamToProto,
83
98
  isPluginProvider,
84
99
  } from "./plugin.ts";
100
+ import {
101
+ PluginRuntimeProvider,
102
+ createPluginRuntimeProviderService,
103
+ isPluginRuntimeProvider,
104
+ } from "./pluginruntime.ts";
85
105
  import {
86
106
  providerKindLabel,
87
107
  resolveDefaultProviderExport,
@@ -114,11 +134,6 @@ export const ENV_PROVIDER_PARENT_PID = "GESTALT_PLUGIN_PARENT_PID";
114
134
  * Environment variable used to request static catalog generation.
115
135
  */
116
136
  export const ENV_WRITE_CATALOG = "GESTALT_PLUGIN_WRITE_CATALOG";
117
- /**
118
- * Environment variable used to request generated manifest metadata export.
119
- */
120
- export const ENV_WRITE_MANIFEST_METADATA =
121
- "GESTALT_PLUGIN_WRITE_MANIFEST_METADATA";
122
137
  /**
123
138
  * Protocol version currently implemented by the TypeScript runtime.
124
139
  */
@@ -127,6 +142,8 @@ export const CURRENT_PROTOCOL_VERSION = 3;
127
142
  * Command-line usage for the runtime entrypoint.
128
143
  */
129
144
  export const USAGE = "usage: bun run runtime.ts ROOT PROVIDER_TARGET";
145
+ export { createAgentProviderService } from "./agent.ts";
146
+ export { createPluginRuntimeProviderService } from "./pluginruntime.ts";
130
147
  export { createWorkflowProviderService } from "./workflow.ts";
131
148
 
132
149
  /**
@@ -146,6 +163,8 @@ export type LoadedProvider =
146
163
  | CacheProvider
147
164
  | SecretsProvider
148
165
  | S3Provider
166
+ | PluginRuntimeProvider
167
+ | AgentProvider
149
168
  | WorkflowProvider;
150
169
 
151
170
  type ProviderRuntimeEntry = {
@@ -205,6 +224,27 @@ const PROVIDER_RUNTIME_ENTRIES: Partial<
205
224
  router.service(S3Service, createS3Service(provider as S3Provider));
206
225
  },
207
226
  },
227
+ runtime: {
228
+ isProvider:
229
+ isPluginRuntimeProvider as (value: unknown) => value is LoadedProvider,
230
+ protoKind: ProtoProviderKind.RUNTIME,
231
+ registerService(router, provider) {
232
+ router.service(
233
+ PluginRuntimeProviderService,
234
+ createPluginRuntimeProviderService(provider as PluginRuntimeProvider),
235
+ );
236
+ },
237
+ },
238
+ agent: {
239
+ isProvider: isAgentProvider as (value: unknown) => value is LoadedProvider,
240
+ protoKind: ProtoProviderKind.AGENT,
241
+ registerService(router, provider) {
242
+ router.service(
243
+ AgentProviderService,
244
+ createAgentProviderService(provider as AgentProvider),
245
+ );
246
+ },
247
+ },
208
248
  workflow: {
209
249
  isProvider: isWorkflowProvider as (value: unknown) => value is LoadedProvider,
210
250
  protoKind: ProtoProviderKind.WORKFLOW,
@@ -301,19 +341,11 @@ export async function runLoadedProvider(
301
341
  }
302
342
 
303
343
  const catalogPath = process.env[ENV_WRITE_CATALOG];
304
- const manifestMetadataPath = process.env[ENV_WRITE_MANIFEST_METADATA];
305
- if (catalogPath || manifestMetadataPath) {
344
+ if (catalogPath) {
306
345
  if (!isPluginProvider(provider)) {
307
- throw new Error(
308
- "static catalog and manifest metadata generation are only supported for plugin providers",
309
- );
310
- }
311
- if (catalogPath) {
312
- writeFileSync(catalogPath, catalogToYaml(provider.staticCatalog()), "utf8");
313
- }
314
- if (manifestMetadataPath && provider.supportsManifestMetadata()) {
315
- provider.writeManifestMetadata(manifestMetadataPath);
346
+ throw new Error("static catalog generation is only supported for plugin providers");
316
347
  }
348
+ writeFileSync(catalogPath, catalogToYaml(provider.staticCatalog()), "utf8");
317
349
  return;
318
350
  }
319
351
 
@@ -341,9 +373,7 @@ export async function serve(provider: LoadedProvider): Promise<void> {
341
373
  if (!socketPath) {
342
374
  throw new Error(`${ENV_PROVIDER_SOCKET} is required`);
343
375
  }
344
- if (existsSync(socketPath)) {
345
- rmSync(socketPath);
346
- }
376
+ rmSync(socketPath, { force: true });
347
377
 
348
378
  const handler = connectNodeAdapter({
349
379
  grpc: true,
@@ -370,9 +400,7 @@ export async function serve(provider: LoadedProvider): Promise<void> {
370
400
  server.close(() => resolveClose());
371
401
  });
372
402
  } finally {
373
- if (existsSync(socketPath)) {
374
- rmSync(socketPath);
375
- }
403
+ rmSync(socketPath, { force: true });
376
404
  }
377
405
  }
378
406
  })();
@@ -412,16 +440,20 @@ export function createRuntimeService(
412
440
  ): Partial<ServiceImpl<typeof ProviderLifecycle>> {
413
441
  return {
414
442
  async getProviderIdentity() {
415
- return create(ProviderIdentitySchema, {
416
- kind: providerRuntimeEntry(provider.kind).protoKind,
417
- name: provider.name,
418
- displayName: provider.displayName,
419
- description: provider.description,
420
- version: provider.version,
421
- warnings: await provider.warnings(),
422
- minProtocolVersion: CURRENT_PROTOCOL_VERSION,
423
- maxProtocolVersion: CURRENT_PROTOCOL_VERSION,
424
- });
443
+ try {
444
+ return create(ProviderIdentitySchema, {
445
+ kind: providerRuntimeEntry(provider.kind).protoKind,
446
+ name: provider.name,
447
+ displayName: provider.displayName,
448
+ description: provider.description,
449
+ version: provider.version,
450
+ warnings: await provider.warnings(),
451
+ minProtocolVersion: CURRENT_PROTOCOL_VERSION,
452
+ maxProtocolVersion: CURRENT_PROTOCOL_VERSION,
453
+ });
454
+ } catch (error) {
455
+ throw providerRuntimeError("provider identity", error);
456
+ }
425
457
  },
426
458
  async configureProvider(request: ConfigureProviderRequest) {
427
459
  assertProtocolVersion(request.protocolVersion);
@@ -431,10 +463,7 @@ export function createRuntimeService(
431
463
  objectFromUnknown(request.config),
432
464
  );
433
465
  } catch (error) {
434
- throw new ConnectError(
435
- `configure provider: ${errorMessage(error)}`,
436
- Code.Unknown,
437
- );
466
+ throw providerRuntimeError("configure provider", error);
438
467
  }
439
468
  return create(ConfigureProviderResponseSchema, {
440
469
  protocolVersion: CURRENT_PROTOCOL_VERSION,
@@ -458,6 +487,16 @@ export function createRuntimeService(
458
487
  });
459
488
  }
460
489
  },
490
+ async startProvider() {
491
+ try {
492
+ await provider.startProvider();
493
+ } catch (error) {
494
+ throw providerRuntimeError("start provider", error);
495
+ }
496
+ return create(StartRuntimeProviderResponseSchema, {
497
+ protocolVersion: CURRENT_PROTOCOL_VERSION,
498
+ });
499
+ },
461
500
  };
462
501
  }
463
502
 
@@ -473,27 +512,31 @@ export function createProviderService(
473
512
  throw new Error("provider is not a plugin provider");
474
513
  }
475
514
  return {
476
- getMetadata() {
477
- return create(ProviderMetadataSchema, {
478
- name: provider.name,
479
- displayName: provider.displayName,
480
- description: provider.description,
481
- connectionMode: connectionModeToProtoValue(
482
- provider.connectionMode,
483
- ) as ProviderConnectionMode,
484
- authTypes: [...provider.authTypes],
485
- connectionParams: Object.fromEntries(
486
- Object.entries(provider.connectionParams).map(([key, value]) => [
487
- key,
488
- connectionParamToProto(value),
489
- ]),
490
- ),
491
- staticCatalog: catalogToProto(provider.staticCatalog()),
492
- supportsSessionCatalog: provider.supportsSessionCatalog(),
493
- supportsPostConnect: false,
494
- minProtocolVersion: CURRENT_PROTOCOL_VERSION,
495
- maxProtocolVersion: CURRENT_PROTOCOL_VERSION,
496
- });
515
+ async getMetadata() {
516
+ try {
517
+ return create(ProviderMetadataSchema, {
518
+ name: provider.name,
519
+ displayName: provider.displayName,
520
+ description: provider.description,
521
+ connectionMode: connectionModeToProtoValue(
522
+ provider.connectionMode,
523
+ ) as ProviderConnectionMode,
524
+ authTypes: [...provider.authTypes],
525
+ connectionParams: Object.fromEntries(
526
+ Object.entries(provider.connectionParams).map(([key, value]) => [
527
+ key,
528
+ connectionParamToProto(value),
529
+ ]),
530
+ ),
531
+ staticCatalog: catalogToProto(provider.staticCatalog()),
532
+ supportsSessionCatalog: provider.supportsSessionCatalog(),
533
+ supportsPostConnect: provider.supportsPostConnect(),
534
+ minProtocolVersion: CURRENT_PROTOCOL_VERSION,
535
+ maxProtocolVersion: CURRENT_PROTOCOL_VERSION,
536
+ });
537
+ } catch (error) {
538
+ throw providerRuntimeError("provider metadata", error);
539
+ }
497
540
  },
498
541
  async startProvider(request: StartProviderRequest) {
499
542
  assertProtocolVersion(request.protocolVersion);
@@ -503,10 +546,7 @@ export function createProviderService(
503
546
  objectFromUnknown(request.config),
504
547
  );
505
548
  } catch (error) {
506
- throw new ConnectError(
507
- `configure provider: ${errorMessage(error)}`,
508
- Code.Unknown,
509
- );
549
+ throw providerRuntimeError("configure provider", error);
510
550
  }
511
551
  return create(StartProviderResponseSchema, {
512
552
  protocolVersion: CURRENT_PROTOCOL_VERSION,
@@ -523,6 +563,7 @@ export function createProviderService(
523
563
  request.connectionParams,
524
564
  request.context,
525
565
  request.invocationToken,
566
+ request.idempotencyKey,
526
567
  ),
527
568
  ),
528
569
  );
@@ -583,11 +624,29 @@ export function createProviderService(
583
624
  catalog: catalogToProto(catalog),
584
625
  });
585
626
  },
586
- async postConnect() {
587
- throw new ConnectError(
588
- "provider does not support post connect",
589
- Code.Unimplemented,
590
- );
627
+ async postConnect(request) {
628
+ if (!provider.supportsPostConnect()) {
629
+ throw new ConnectError(
630
+ "provider does not support post connect",
631
+ Code.Unimplemented,
632
+ );
633
+ }
634
+ let metadata: Record<string, string> | null | undefined;
635
+ try {
636
+ metadata = await provider.postConnectMetadata(
637
+ providerConnectedToken(request.token),
638
+ );
639
+ } catch (error) {
640
+ throw new ConnectError(
641
+ `post connect: ${errorMessage(error)}`,
642
+ Code.Unknown,
643
+ );
644
+ }
645
+ return create(PostConnectResponseSchema, {
646
+ metadata: {
647
+ ...(metadata ?? {}),
648
+ },
649
+ });
591
650
  },
592
651
  };
593
652
  }
@@ -758,6 +817,7 @@ function providerRequest(
758
817
  connectionParams: Record<string, string>,
759
818
  requestContext?: ProtoRequestContext,
760
819
  invocationToken = "",
820
+ idempotencyKey = "",
761
821
  ): Request {
762
822
  const subject = requestContext?.subject;
763
823
  const credential = requestContext?.credential;
@@ -787,6 +847,7 @@ function providerRequest(
787
847
  ...(requestContext?.workflow ?? {}),
788
848
  },
789
849
  invocationToken,
850
+ idempotencyKey: idempotencyKey.trim(),
790
851
  };
791
852
  }
792
853
 
@@ -822,6 +883,29 @@ function providerHTTPSubjectResolutionContext(
822
883
  };
823
884
  }
824
885
 
886
+ function providerConnectedToken(
887
+ token?: ProtoPostConnectCredential,
888
+ ): ConnectedToken {
889
+ const metadataJson = token?.metadataJson ?? "";
890
+ return {
891
+ id: token?.id ?? "",
892
+ subjectId: token?.subjectId ?? "",
893
+ integration: token?.integration ?? "",
894
+ connection: token?.connection ?? "",
895
+ instance: token?.instance ?? "",
896
+ accessToken: token?.accessToken ?? "",
897
+ refreshToken: token?.refreshToken ?? "",
898
+ scopes: token?.scopes ?? "",
899
+ expiresAt: timestampToDate(token?.expiresAt),
900
+ lastRefreshedAt: timestampToDate(token?.lastRefreshedAt),
901
+ refreshErrorCount: token?.refreshErrorCount ?? 0,
902
+ metadataJson,
903
+ metadata: stringRecordFromJSON(metadataJson),
904
+ createdAt: timestampToDate(token?.createdAt),
905
+ updatedAt: timestampToDate(token?.updatedAt),
906
+ };
907
+ }
908
+
825
909
  function providerStringLists(
826
910
  input: Record<string, { values?: string[] }> | undefined,
827
911
  ): Record<string, string[]> {
@@ -844,6 +928,10 @@ function providerRuntimeEntry(
844
928
  return entry;
845
929
  }
846
930
 
931
+ function providerRuntimeError(label: string, error: unknown): ConnectError {
932
+ return new ConnectError(`${label}: ${errorMessage(error)}`, Code.Unknown);
933
+ }
934
+
847
935
  function resolveLoadedProvider(
848
936
  candidate: unknown,
849
937
  kind: ProviderKind,
@@ -930,6 +1018,45 @@ function normalizeBigInt(value: number | bigint): bigint {
930
1018
  return BigInt(Math.max(0, Math.trunc(value)));
931
1019
  }
932
1020
 
1021
+ function timestampToDate(
1022
+ value: { seconds: bigint; nanos: number } | undefined,
1023
+ ): Date | undefined {
1024
+ if (!value) {
1025
+ return undefined;
1026
+ }
1027
+ const seconds = Number(value.seconds ?? 0n);
1028
+ const nanos = Number(value.nanos ?? 0);
1029
+ if (!Number.isFinite(seconds) || !Number.isFinite(nanos)) {
1030
+ return undefined;
1031
+ }
1032
+ const millis = (seconds * 1000) + Math.trunc(nanos / 1_000_000);
1033
+ if (!Number.isFinite(millis)) {
1034
+ return undefined;
1035
+ }
1036
+ return new Date(millis);
1037
+ }
1038
+
1039
+ function stringRecordFromJSON(value: string): Record<string, string> {
1040
+ if (!value.trim()) {
1041
+ return {};
1042
+ }
1043
+ try {
1044
+ const parsed = JSON.parse(value) as unknown;
1045
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1046
+ return {};
1047
+ }
1048
+ const output: Record<string, string> = {};
1049
+ for (const [key, entry] of Object.entries(parsed)) {
1050
+ if (typeof entry === "string") {
1051
+ output[key] = entry;
1052
+ }
1053
+ }
1054
+ return output;
1055
+ } catch {
1056
+ return {};
1057
+ }
1058
+ }
1059
+
933
1060
  function cloneUint8Array(value: Uint8Array | undefined): Uint8Array {
934
1061
  if (!value) {
935
1062
  return new Uint8Array();