@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/plugin.ts CHANGED
@@ -23,8 +23,20 @@ import {
23
23
  type Request,
24
24
  responseBrand,
25
25
  type Response,
26
+ type Subject,
26
27
  } from "./api.ts";
27
- import { RuntimeProvider, type RuntimeProviderOptions } from "./provider.ts";
28
+ import {
29
+ cloneHTTPSubjectRequest,
30
+ cloneHTTPSubjectResolutionContext,
31
+ type HTTPSubjectRequest,
32
+ type HTTPSubjectResolutionContext,
33
+ type HTTPSubjectResolver,
34
+ } from "./http-subject.ts";
35
+ import {
36
+ isRuntimeProvider,
37
+ RuntimeProvider,
38
+ type RuntimeProviderOptions,
39
+ } from "./provider.ts";
28
40
  import type { Schema } from "./schema.ts";
29
41
 
30
42
  /**
@@ -84,6 +96,34 @@ export type SessionCatalogHandler = (
84
96
  request: Request,
85
97
  ) => MaybePromise<SessionCatalog | null | undefined>;
86
98
 
99
+ /**
100
+ * Host-managed connection payload passed into a provider post-connect hook.
101
+ */
102
+ export interface ConnectedToken {
103
+ id: string;
104
+ subjectId: string;
105
+ integration: string;
106
+ connection: string;
107
+ instance: string;
108
+ accessToken: string;
109
+ refreshToken: string;
110
+ scopes: string;
111
+ expiresAt?: Date | undefined;
112
+ lastRefreshedAt?: Date | undefined;
113
+ refreshErrorCount: number;
114
+ metadataJson: string;
115
+ metadata: Record<string, string>;
116
+ createdAt?: Date | undefined;
117
+ updatedAt?: Date | undefined;
118
+ }
119
+
120
+ /**
121
+ * Callback used to add derived metadata after a connection is established.
122
+ */
123
+ export type PostConnectHandler = (
124
+ token: ConnectedToken,
125
+ ) => MaybePromise<Record<string, string> | null | undefined>;
126
+
87
127
  /**
88
128
  * Runtime hooks required to implement a plugin provider.
89
129
  */
@@ -93,6 +133,8 @@ export interface PluginDefinitionOptions extends RuntimeProviderOptions {
93
133
  connectionParams?: Record<string, ConnectionParamDefinition>;
94
134
  securitySchemes?: Record<string, HTTPSecurityScheme>;
95
135
  http?: Record<string, HTTPBinding>;
136
+ resolveHTTPSubject?: HTTPSubjectResolver;
137
+ postConnect?: PostConnectHandler;
96
138
  iconSvg?: string;
97
139
  operations: Array<OperationDefinition<any, any>>;
98
140
  sessionCatalog?: SessionCatalogHandler;
@@ -149,6 +191,8 @@ export class PluginProvider extends RuntimeProvider {
149
191
  readonly http: Record<string, HTTPBinding>;
150
192
 
151
193
  private readonly sessionCatalogHandler: SessionCatalogHandler | undefined;
194
+ private readonly httpSubjectResolver: HTTPSubjectResolver | undefined;
195
+ private readonly postConnectHandler: PostConnectHandler | undefined;
152
196
  private readonly operations = new Map<string, OperationDefinition<any, any>>();
153
197
 
154
198
  constructor(options: PluginDefinitionOptions) {
@@ -159,6 +203,8 @@ export class PluginProvider extends RuntimeProvider {
159
203
  this.connectionParams = normalizeConnectionParams(options.connectionParams);
160
204
  this.securitySchemes = normalizeHTTPSecuritySchemes(options.securitySchemes);
161
205
  this.http = normalizeHTTPBindings(options.http);
206
+ this.httpSubjectResolver = options.resolveHTTPSubject;
207
+ this.postConnectHandler = options.postConnect;
162
208
  this.sessionCatalogHandler = options.sessionCatalog;
163
209
 
164
210
  for (const rawEntry of options.operations) {
@@ -189,6 +235,36 @@ export class PluginProvider extends RuntimeProvider {
189
235
  return await this.sessionCatalogHandler?.(request);
190
236
  }
191
237
 
238
+ /**
239
+ * Reports whether the provider exposes a connect-time metadata hook.
240
+ */
241
+ supportsPostConnect(): boolean {
242
+ return this.postConnectHandler !== undefined;
243
+ }
244
+
245
+ /**
246
+ * Computes additional connection metadata after a successful connect flow.
247
+ */
248
+ async postConnectMetadata(
249
+ token: ConnectedToken,
250
+ ): Promise<Record<string, string> | null | undefined> {
251
+ return await this.postConnectHandler?.(cloneConnectedToken(token));
252
+ }
253
+
254
+ /**
255
+ * Resolves the concrete Gestalt subject for a verified hosted HTTP request,
256
+ * if the plugin opts into subject resolution.
257
+ */
258
+ async resolveHTTPSubject(
259
+ request: HTTPSubjectRequest,
260
+ context: HTTPSubjectResolutionContext,
261
+ ): Promise<Subject | null | undefined> {
262
+ return await this.httpSubjectResolver?.(
263
+ cloneHTTPSubjectRequest(request),
264
+ cloneHTTPSubjectResolutionContext(context),
265
+ );
266
+ }
267
+
192
268
  /**
193
269
  * Returns the static catalog emitted during provider startup.
194
270
  */
@@ -361,12 +437,27 @@ export function isPluginProvider(
361
437
  ): value is PluginProvider {
362
438
  return (
363
439
  value instanceof PluginProvider ||
364
- (typeof value === "object" &&
365
- value !== null &&
440
+ (isRuntimeProvider(value) &&
366
441
  "kind" in value &&
367
442
  (value as { kind?: unknown }).kind === "integration" &&
368
443
  "staticCatalog" in value &&
369
- "execute" in value)
444
+ typeof (value as { staticCatalog?: unknown }).staticCatalog === "function" &&
445
+ "execute" in value &&
446
+ typeof (value as { execute?: unknown }).execute === "function" &&
447
+ "supportsSessionCatalog" in value &&
448
+ typeof (value as { supportsSessionCatalog?: unknown }).supportsSessionCatalog === "function" &&
449
+ "catalogForRequest" in value &&
450
+ typeof (value as { catalogForRequest?: unknown }).catalogForRequest === "function" &&
451
+ "supportsManifestMetadata" in value &&
452
+ typeof (value as { supportsManifestMetadata?: unknown }).supportsManifestMetadata === "function" &&
453
+ "writeManifestMetadata" in value &&
454
+ typeof (value as { writeManifestMetadata?: unknown }).writeManifestMetadata === "function" &&
455
+ "supportsPostConnect" in value &&
456
+ typeof (value as { supportsPostConnect?: unknown }).supportsPostConnect === "function" &&
457
+ "postConnectMetadata" in value &&
458
+ typeof (value as { postConnectMetadata?: unknown }).postConnectMetadata === "function" &&
459
+ "resolveHTTPSubject" in value &&
460
+ typeof (value as { resolveHTTPSubject?: unknown }).resolveHTTPSubject === "function")
370
461
  );
371
462
  }
372
463
 
@@ -396,6 +487,21 @@ function normalizeConnectionParams(
396
487
  return output;
397
488
  }
398
489
 
490
+ function cloneConnectedToken(token: ConnectedToken): ConnectedToken {
491
+ return {
492
+ ...token,
493
+ metadata: {
494
+ ...(token.metadata ?? {}),
495
+ },
496
+ expiresAt: token.expiresAt ? new Date(token.expiresAt) : undefined,
497
+ lastRefreshedAt: token.lastRefreshedAt
498
+ ? new Date(token.lastRefreshedAt)
499
+ : undefined,
500
+ createdAt: token.createdAt ? new Date(token.createdAt) : undefined,
501
+ updatedAt: token.updatedAt ? new Date(token.updatedAt) : undefined,
502
+ };
503
+ }
504
+
399
505
  function normalizeHTTPSecuritySchemes(
400
506
  input: Record<string, HTTPSecurityScheme> | undefined,
401
507
  ): Record<string, HTTPSecurityScheme> {
@@ -424,6 +530,21 @@ function cloneHTTPSecurityScheme(value: HTTPSecurityScheme): HTTPSecurityScheme
424
530
  if (value.description !== undefined) {
425
531
  output.description = value.description;
426
532
  }
533
+ if (value.signatureHeader !== undefined) {
534
+ output.signatureHeader = value.signatureHeader;
535
+ }
536
+ if (value.signaturePrefix !== undefined) {
537
+ output.signaturePrefix = value.signaturePrefix;
538
+ }
539
+ if (value.payloadTemplate !== undefined) {
540
+ output.payloadTemplate = value.payloadTemplate;
541
+ }
542
+ if (value.timestampHeader !== undefined) {
543
+ output.timestampHeader = value.timestampHeader;
544
+ }
545
+ if (value.maxAgeSeconds !== undefined) {
546
+ output.maxAgeSeconds = value.maxAgeSeconds;
547
+ }
427
548
  if (value.name !== undefined) {
428
549
  output.name = value.name;
429
550
  }
@@ -44,6 +44,12 @@ const PROVIDER_KIND_DEFINITIONS = {
44
44
  defaultExportNames: ["workflow", "provider"],
45
45
  label: "workflow provider",
46
46
  },
47
+ agent: {
48
+ tokens: ["agent"],
49
+ formatToken: "agent",
50
+ defaultExportNames: ["agent", "provider"],
51
+ label: "agent provider",
52
+ },
47
53
  telemetry: {
48
54
  tokens: ["telemetry"],
49
55
  formatToken: "telemetry",
package/src/provider.ts CHANGED
@@ -10,6 +10,7 @@ export type ProviderKind =
10
10
  | "secrets"
11
11
  | "s3"
12
12
  | "workflow"
13
+ | "agent"
13
14
  | "telemetry";
14
15
 
15
16
  /**
@@ -151,7 +152,17 @@ export function isRuntimeProvider(value: unknown): value is RuntimeProvider {
151
152
  value !== null &&
152
153
  "kind" in value &&
153
154
  "resolveName" in value &&
154
- "configureProvider" in value)
155
+ typeof (value as { resolveName?: unknown }).resolveName === "function" &&
156
+ "configureProvider" in value &&
157
+ typeof (value as { configureProvider?: unknown }).configureProvider === "function" &&
158
+ "supportsHealthCheck" in value &&
159
+ typeof (value as { supportsHealthCheck?: unknown }).supportsHealthCheck === "function" &&
160
+ "healthCheck" in value &&
161
+ typeof (value as { healthCheck?: unknown }).healthCheck === "function" &&
162
+ "warnings" in value &&
163
+ typeof (value as { warnings?: unknown }).warnings === "function" &&
164
+ "closeProvider" in value &&
165
+ typeof (value as { closeProvider?: unknown }).closeProvider === "function")
155
166
  );
156
167
  }
157
168
 
package/src/runtime.ts CHANGED
@@ -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,9 +43,14 @@ import {
40
43
  CatalogSchema as ProtoCatalogSchema,
41
44
  ConnectionMode as ProviderConnectionMode,
42
45
  GetSessionCatalogResponseSchema,
46
+ PostConnectResponseSchema,
47
+ ResolveHTTPSubjectResponseSchema,
43
48
  OperationResultSchema,
44
49
  ProviderMetadataSchema,
50
+ type HTTPSubjectRequest as ProtoHTTPSubjectRequest,
51
+ type IntegrationToken as ProtoIntegrationToken,
45
52
  type RequestContext as ProtoRequestContext,
53
+ type ResolveHTTPSubjectRequest as ProtoResolveHTTPSubjectRequest,
46
54
  IntegrationProvider as IntegrationProviderService,
47
55
  StartProviderResponseSchema,
48
56
  type ExecuteRequest,
@@ -60,6 +68,11 @@ import {
60
68
  import { S3 as S3Service } from "../gen/v1/s3_pb.ts";
61
69
  import { WorkflowProvider as WorkflowProviderService } from "../gen/v1/workflow_pb.ts";
62
70
  import { errorMessage, type Request } from "./api.ts";
71
+ import {
72
+ AgentProvider,
73
+ createAgentProviderService,
74
+ isAgentProvider,
75
+ } from "./agent.ts";
63
76
  import {
64
77
  AuthenticationProvider,
65
78
  isAuthenticationProvider,
@@ -69,6 +82,12 @@ import { CacheProvider, isCacheProvider } from "./cache.ts";
69
82
  import { SecretsProvider, isSecretsProvider } from "./secrets.ts";
70
83
  import { catalogToYaml, type Catalog } from "./catalog.ts";
71
84
  import {
85
+ HTTPSubjectResolutionError,
86
+ type HTTPSubjectRequest,
87
+ type HTTPSubjectResolutionContext,
88
+ } from "./http-subject.ts";
89
+ import {
90
+ type ConnectedToken,
72
91
  PluginProvider,
73
92
  connectionModeToProtoValue,
74
93
  connectionParamToProto,
@@ -119,6 +138,7 @@ export const CURRENT_PROTOCOL_VERSION = 3;
119
138
  * Command-line usage for the runtime entrypoint.
120
139
  */
121
140
  export const USAGE = "usage: bun run runtime.ts ROOT PROVIDER_TARGET";
141
+ export { createAgentProviderService } from "./agent.ts";
122
142
  export { createWorkflowProviderService } from "./workflow.ts";
123
143
 
124
144
  /**
@@ -138,6 +158,7 @@ export type LoadedProvider =
138
158
  | CacheProvider
139
159
  | SecretsProvider
140
160
  | S3Provider
161
+ | AgentProvider
141
162
  | WorkflowProvider;
142
163
 
143
164
  type ProviderRuntimeEntry = {
@@ -197,6 +218,16 @@ const PROVIDER_RUNTIME_ENTRIES: Partial<
197
218
  router.service(S3Service, createS3Service(provider as S3Provider));
198
219
  },
199
220
  },
221
+ agent: {
222
+ isProvider: isAgentProvider as (value: unknown) => value is LoadedProvider,
223
+ protoKind: ProtoProviderKind.AGENT,
224
+ registerService(router, provider) {
225
+ router.service(
226
+ AgentProviderService,
227
+ createAgentProviderService(provider as AgentProvider),
228
+ );
229
+ },
230
+ },
200
231
  workflow: {
201
232
  isProvider: isWorkflowProvider as (value: unknown) => value is LoadedProvider,
202
233
  protoKind: ProtoProviderKind.WORKFLOW,
@@ -404,16 +435,20 @@ export function createRuntimeService(
404
435
  ): Partial<ServiceImpl<typeof ProviderLifecycle>> {
405
436
  return {
406
437
  async getProviderIdentity() {
407
- return create(ProviderIdentitySchema, {
408
- kind: providerRuntimeEntry(provider.kind).protoKind,
409
- name: provider.name,
410
- displayName: provider.displayName,
411
- description: provider.description,
412
- version: provider.version,
413
- warnings: await provider.warnings(),
414
- minProtocolVersion: CURRENT_PROTOCOL_VERSION,
415
- maxProtocolVersion: CURRENT_PROTOCOL_VERSION,
416
- });
438
+ try {
439
+ return create(ProviderIdentitySchema, {
440
+ kind: providerRuntimeEntry(provider.kind).protoKind,
441
+ name: provider.name,
442
+ displayName: provider.displayName,
443
+ description: provider.description,
444
+ version: provider.version,
445
+ warnings: await provider.warnings(),
446
+ minProtocolVersion: CURRENT_PROTOCOL_VERSION,
447
+ maxProtocolVersion: CURRENT_PROTOCOL_VERSION,
448
+ });
449
+ } catch (error) {
450
+ throw providerRuntimeError("provider identity", error);
451
+ }
417
452
  },
418
453
  async configureProvider(request: ConfigureProviderRequest) {
419
454
  assertProtocolVersion(request.protocolVersion);
@@ -423,10 +458,7 @@ export function createRuntimeService(
423
458
  objectFromUnknown(request.config),
424
459
  );
425
460
  } catch (error) {
426
- throw new ConnectError(
427
- `configure provider: ${errorMessage(error)}`,
428
- Code.Unknown,
429
- );
461
+ throw providerRuntimeError("configure provider", error);
430
462
  }
431
463
  return create(ConfigureProviderResponseSchema, {
432
464
  protocolVersion: CURRENT_PROTOCOL_VERSION,
@@ -465,27 +497,31 @@ export function createProviderService(
465
497
  throw new Error("provider is not a plugin provider");
466
498
  }
467
499
  return {
468
- getMetadata() {
469
- return create(ProviderMetadataSchema, {
470
- name: provider.name,
471
- displayName: provider.displayName,
472
- description: provider.description,
473
- connectionMode: connectionModeToProtoValue(
474
- provider.connectionMode,
475
- ) as ProviderConnectionMode,
476
- authTypes: [...provider.authTypes],
477
- connectionParams: Object.fromEntries(
478
- Object.entries(provider.connectionParams).map(([key, value]) => [
479
- key,
480
- connectionParamToProto(value),
481
- ]),
482
- ),
483
- staticCatalog: catalogToProto(provider.staticCatalog()),
484
- supportsSessionCatalog: provider.supportsSessionCatalog(),
485
- supportsPostConnect: false,
486
- minProtocolVersion: CURRENT_PROTOCOL_VERSION,
487
- maxProtocolVersion: CURRENT_PROTOCOL_VERSION,
488
- });
500
+ async getMetadata() {
501
+ try {
502
+ return create(ProviderMetadataSchema, {
503
+ name: provider.name,
504
+ displayName: provider.displayName,
505
+ description: provider.description,
506
+ connectionMode: connectionModeToProtoValue(
507
+ provider.connectionMode,
508
+ ) as ProviderConnectionMode,
509
+ authTypes: [...provider.authTypes],
510
+ connectionParams: Object.fromEntries(
511
+ Object.entries(provider.connectionParams).map(([key, value]) => [
512
+ key,
513
+ connectionParamToProto(value),
514
+ ]),
515
+ ),
516
+ staticCatalog: catalogToProto(provider.staticCatalog()),
517
+ supportsSessionCatalog: provider.supportsSessionCatalog(),
518
+ supportsPostConnect: provider.supportsPostConnect(),
519
+ minProtocolVersion: CURRENT_PROTOCOL_VERSION,
520
+ maxProtocolVersion: CURRENT_PROTOCOL_VERSION,
521
+ });
522
+ } catch (error) {
523
+ throw providerRuntimeError("provider metadata", error);
524
+ }
489
525
  },
490
526
  async startProvider(request: StartProviderRequest) {
491
527
  assertProtocolVersion(request.protocolVersion);
@@ -495,10 +531,7 @@ export function createProviderService(
495
531
  objectFromUnknown(request.config),
496
532
  );
497
533
  } catch (error) {
498
- throw new ConnectError(
499
- `configure provider: ${errorMessage(error)}`,
500
- Code.Unknown,
501
- );
534
+ throw providerRuntimeError("configure provider", error);
502
535
  }
503
536
  return create(StartProviderResponseSchema, {
504
537
  protocolVersion: CURRENT_PROTOCOL_VERSION,
@@ -519,6 +552,36 @@ export function createProviderService(
519
552
  ),
520
553
  );
521
554
  },
555
+ async resolveHTTPSubject(request: ProtoResolveHTTPSubjectRequest) {
556
+ let subject;
557
+ try {
558
+ subject = await provider.resolveHTTPSubject(
559
+ providerHTTPSubjectRequest(request.request),
560
+ providerHTTPSubjectResolutionContext(request.context),
561
+ );
562
+ } catch (error) {
563
+ if (error instanceof HTTPSubjectResolutionError) {
564
+ return create(ResolveHTTPSubjectResponseSchema, {
565
+ rejectStatus: error.status,
566
+ rejectMessage: error.message,
567
+ });
568
+ }
569
+ throw new ConnectError(
570
+ `resolve http subject: ${errorMessage(error)}`,
571
+ Code.Unknown,
572
+ );
573
+ }
574
+ return create(ResolveHTTPSubjectResponseSchema, subject
575
+ ? {
576
+ subject: {
577
+ id: subject.id,
578
+ kind: subject.kind,
579
+ displayName: subject.displayName,
580
+ authSource: subject.authSource,
581
+ },
582
+ }
583
+ : {});
584
+ },
522
585
  async getSessionCatalog(request: GetSessionCatalogRequest) {
523
586
  let catalog: Catalog | Record<string, unknown> | null | undefined;
524
587
  try {
@@ -545,11 +608,29 @@ export function createProviderService(
545
608
  catalog: catalogToProto(catalog),
546
609
  });
547
610
  },
548
- async postConnect() {
549
- throw new ConnectError(
550
- "provider does not support post connect",
551
- Code.Unimplemented,
552
- );
611
+ async postConnect(request) {
612
+ if (!provider.supportsPostConnect()) {
613
+ throw new ConnectError(
614
+ "provider does not support post connect",
615
+ Code.Unimplemented,
616
+ );
617
+ }
618
+ let metadata: Record<string, string> | null | undefined;
619
+ try {
620
+ metadata = await provider.postConnectMetadata(
621
+ providerConnectedToken(request.token),
622
+ );
623
+ } catch (error) {
624
+ throw new ConnectError(
625
+ `post connect: ${errorMessage(error)}`,
626
+ Code.Unknown,
627
+ );
628
+ }
629
+ return create(PostConnectResponseSchema, {
630
+ metadata: {
631
+ ...(metadata ?? {}),
632
+ },
633
+ });
553
634
  },
554
635
  };
555
636
  }
@@ -752,6 +833,71 @@ function providerRequest(
752
833
  };
753
834
  }
754
835
 
836
+ function providerHTTPSubjectRequest(
837
+ request?: ProtoHTTPSubjectRequest,
838
+ ): HTTPSubjectRequest {
839
+ return {
840
+ binding: request?.binding ?? "",
841
+ method: request?.method ?? "",
842
+ path: request?.path ?? "",
843
+ contentType: request?.contentType ?? "",
844
+ headers: providerStringLists(request?.headers),
845
+ query: providerStringLists(request?.query),
846
+ params: objectFromUnknown(request?.params),
847
+ rawBody: new Uint8Array(request?.rawBody ?? new Uint8Array()),
848
+ securityScheme: request?.securityScheme ?? "",
849
+ verifiedSubject: request?.verifiedSubject ?? "",
850
+ verifiedClaims: {
851
+ ...(request?.verifiedClaims ?? {}),
852
+ },
853
+ };
854
+ }
855
+
856
+ function providerHTTPSubjectResolutionContext(
857
+ requestContext?: ProtoRequestContext,
858
+ ): HTTPSubjectResolutionContext {
859
+ const request = providerRequest("", {}, requestContext);
860
+ return {
861
+ subject: request.subject,
862
+ credential: request.credential,
863
+ access: request.access,
864
+ workflow: request.workflow,
865
+ };
866
+ }
867
+
868
+ function providerConnectedToken(
869
+ token?: ProtoIntegrationToken,
870
+ ): ConnectedToken {
871
+ const metadataJson = token?.metadataJson ?? "";
872
+ return {
873
+ id: token?.id ?? "",
874
+ subjectId: token?.userId ?? "",
875
+ integration: token?.integration ?? "",
876
+ connection: token?.connection ?? "",
877
+ instance: token?.instance ?? "",
878
+ accessToken: token?.accessToken ?? "",
879
+ refreshToken: token?.refreshToken ?? "",
880
+ scopes: token?.scopes ?? "",
881
+ expiresAt: timestampToDate(token?.expiresAt),
882
+ lastRefreshedAt: timestampToDate(token?.lastRefreshedAt),
883
+ refreshErrorCount: token?.refreshErrorCount ?? 0,
884
+ metadataJson,
885
+ metadata: stringRecordFromJSON(metadataJson),
886
+ createdAt: timestampToDate(token?.createdAt),
887
+ updatedAt: timestampToDate(token?.updatedAt),
888
+ };
889
+ }
890
+
891
+ function providerStringLists(
892
+ input: Record<string, { values?: string[] }> | undefined,
893
+ ): Record<string, string[]> {
894
+ const output: Record<string, string[]> = {};
895
+ for (const [key, value] of Object.entries(input ?? {})) {
896
+ output[key] = [...(value.values ?? [])];
897
+ }
898
+ return output;
899
+ }
900
+
755
901
  function providerRuntimeEntry(
756
902
  kind: ProviderKind,
757
903
  ): ProviderRuntimeEntry {
@@ -764,6 +910,10 @@ function providerRuntimeEntry(
764
910
  return entry;
765
911
  }
766
912
 
913
+ function providerRuntimeError(label: string, error: unknown): ConnectError {
914
+ return new ConnectError(`${label}: ${errorMessage(error)}`, Code.Unknown);
915
+ }
916
+
767
917
  function resolveLoadedProvider(
768
918
  candidate: unknown,
769
919
  kind: ProviderKind,
@@ -850,6 +1000,45 @@ function normalizeBigInt(value: number | bigint): bigint {
850
1000
  return BigInt(Math.max(0, Math.trunc(value)));
851
1001
  }
852
1002
 
1003
+ function timestampToDate(
1004
+ value: { seconds: bigint; nanos: number } | undefined,
1005
+ ): Date | undefined {
1006
+ if (!value) {
1007
+ return undefined;
1008
+ }
1009
+ const seconds = Number(value.seconds ?? 0n);
1010
+ const nanos = Number(value.nanos ?? 0);
1011
+ if (!Number.isFinite(seconds) || !Number.isFinite(nanos)) {
1012
+ return undefined;
1013
+ }
1014
+ const millis = (seconds * 1000) + Math.trunc(nanos / 1_000_000);
1015
+ if (!Number.isFinite(millis)) {
1016
+ return undefined;
1017
+ }
1018
+ return new Date(millis);
1019
+ }
1020
+
1021
+ function stringRecordFromJSON(value: string): Record<string, string> {
1022
+ if (!value.trim()) {
1023
+ return {};
1024
+ }
1025
+ try {
1026
+ const parsed = JSON.parse(value) as unknown;
1027
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1028
+ return {};
1029
+ }
1030
+ const output: Record<string, string> = {};
1031
+ for (const [key, entry] of Object.entries(parsed)) {
1032
+ if (typeof entry === "string") {
1033
+ output[key] = entry;
1034
+ }
1035
+ }
1036
+ return output;
1037
+ } catch {
1038
+ return {};
1039
+ }
1040
+ }
1041
+
853
1042
  function cloneUint8Array(value: Uint8Array | undefined): Uint8Array {
854
1043
  if (!value) {
855
1044
  return new Uint8Array();