@valon-technologies/gestalt 0.0.1-alpha.8 → 0.0.1-alpha.9

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/invoker.ts ADDED
@@ -0,0 +1,124 @@
1
+ import { connect } from "node:net";
2
+
3
+ import type { JsonObject, JsonValue } from "@bufbuild/protobuf";
4
+ import { createClient, type Client } from "@connectrpc/connect";
5
+ import { createGrpcTransport } from "@connectrpc/connect-node";
6
+
7
+ import { PluginInvoker as PluginInvokerService } from "../gen/v1/plugin_pb.ts";
8
+ import type { OperationResult, Request } from "./api.ts";
9
+
10
+ export const ENV_PLUGIN_INVOKER_SOCKET = "GESTALT_PLUGIN_INVOKER_SOCKET";
11
+
12
+ export interface PluginInvokeOptions {
13
+ connection?: string;
14
+ instance?: string;
15
+ }
16
+
17
+ export interface PluginInvocationGrant {
18
+ plugin: string;
19
+ operations: string[];
20
+ }
21
+
22
+ export class PluginInvoker {
23
+ private readonly client: Client<typeof PluginInvokerService>;
24
+ private readonly invocationToken: string;
25
+
26
+ constructor(request: Request);
27
+ constructor(invocationToken: string);
28
+ constructor(requestOrToken: Request | string) {
29
+ this.invocationToken = normalizeInvocationToken(requestOrToken);
30
+
31
+ const socketPath = process.env[ENV_PLUGIN_INVOKER_SOCKET];
32
+ if (!socketPath) {
33
+ throw new Error(`plugin invoker: ${ENV_PLUGIN_INVOKER_SOCKET} is not set`);
34
+ }
35
+
36
+ const transport = createGrpcTransport({
37
+ baseUrl: "http://localhost",
38
+ nodeOptions: {
39
+ createConnection: () => connect(socketPath),
40
+ },
41
+ });
42
+ this.client = createClient(PluginInvokerService, transport);
43
+ }
44
+
45
+ async invoke(
46
+ plugin: string,
47
+ operation: string,
48
+ params: Record<string, unknown> = {},
49
+ options?: PluginInvokeOptions,
50
+ ): Promise<OperationResult> {
51
+ const response = await this.client.invoke({
52
+ invocationToken: this.invocationToken,
53
+ plugin,
54
+ operation,
55
+ params: toJsonObject(params),
56
+ connection: options?.connection ?? "",
57
+ instance: options?.instance ?? "",
58
+ });
59
+ return {
60
+ status: response.status,
61
+ body: response.body,
62
+ };
63
+ }
64
+
65
+ async exchangeInvocationToken(options?: {
66
+ grants?: PluginInvocationGrant[];
67
+ ttlSeconds?: number;
68
+ }): Promise<string> {
69
+ const response = await this.client.exchangeInvocationToken({
70
+ parentInvocationToken: this.invocationToken,
71
+ grants: (options?.grants ?? [])
72
+ .map((grant) => ({
73
+ plugin: grant.plugin.trim(),
74
+ operations: grant.operations
75
+ .map((operation) => operation.trim())
76
+ .filter(Boolean),
77
+ }))
78
+ .filter((grant) => grant.plugin.length > 0),
79
+ ttlSeconds: BigInt(Math.max(0, options?.ttlSeconds ?? 0)),
80
+ });
81
+ return response.invocationToken;
82
+ }
83
+ }
84
+
85
+ function normalizeInvocationToken(requestOrToken: Request | string): string {
86
+ const invocationToken =
87
+ typeof requestOrToken === "string"
88
+ ? requestOrToken
89
+ : requestOrToken.invocationToken;
90
+ const trimmed = invocationToken.trim();
91
+ if (!trimmed) {
92
+ throw new Error("plugin invoker: invocation token is not available");
93
+ }
94
+ return trimmed;
95
+ }
96
+
97
+ function toJsonObject(params: Record<string, unknown>): JsonObject {
98
+ const output: JsonObject = {};
99
+ for (const [key, value] of Object.entries(params ?? {})) {
100
+ if (value === undefined) {
101
+ continue;
102
+ }
103
+ output[key] = toJsonValue(value);
104
+ }
105
+ return output;
106
+ }
107
+
108
+ function toJsonValue(value: unknown): JsonValue {
109
+ if (
110
+ value === null ||
111
+ typeof value === "string" ||
112
+ typeof value === "number" ||
113
+ typeof value === "boolean"
114
+ ) {
115
+ return value;
116
+ }
117
+ if (Array.isArray(value)) {
118
+ return value.map((entry) => toJsonValue(entry));
119
+ }
120
+ if (typeof value === "object") {
121
+ return toJsonObject(value as Record<string, unknown>);
122
+ }
123
+ throw new Error("plugin invoker: params must be JSON-serializable");
124
+ }
package/src/plugin.ts CHANGED
@@ -18,13 +18,18 @@ import {
18
18
  import { RuntimeProvider, type RuntimeProviderOptions } from "./provider.ts";
19
19
  import type { Schema } from "./schema.ts";
20
20
 
21
+ /**
22
+ * How a plugin provider expects to authenticate or connect.
23
+ */
21
24
  export type ConnectionMode =
22
25
  | "unspecified"
23
26
  | "none"
24
27
  | "user"
25
- | "identity"
26
- | "either";
28
+ | "identity";
27
29
 
30
+ /**
31
+ * Metadata for a single connection parameter exposed by a provider.
32
+ */
28
33
  export interface ConnectionParamDefinition {
29
34
  required?: boolean;
30
35
  description?: string;
@@ -33,6 +38,9 @@ export interface ConnectionParamDefinition {
33
38
  field?: string;
34
39
  }
35
40
 
41
+ /**
42
+ * Operation definition accepted by {@link operation} and {@link definePlugin}.
43
+ */
36
44
  export interface OperationOptions<In, Out> {
37
45
  id: string;
38
46
  method?: string;
@@ -47,16 +55,29 @@ export interface OperationOptions<In, Out> {
47
55
  handler: (input: In, request: Request) => MaybePromise<Out | Response<Out>>;
48
56
  }
49
57
 
58
+ /**
59
+ * Normalized plugin operation definition.
60
+ */
50
61
  export interface OperationDefinition<In, Out> extends OperationOptions<
51
62
  In,
52
63
  Out
53
64
  > {}
54
65
 
66
+ /**
67
+ * Session-specific catalog payload returned by a provider at runtime.
68
+ */
55
69
  export type SessionCatalog = Catalog | Record<string, unknown>;
70
+
71
+ /**
72
+ * Callback used to resolve a catalog for an authenticated request context.
73
+ */
56
74
  export type SessionCatalogHandler = (
57
75
  request: Request,
58
76
  ) => MaybePromise<SessionCatalog | null | undefined>;
59
77
 
78
+ /**
79
+ * Runtime hooks required to implement a plugin provider.
80
+ */
60
81
  export interface PluginDefinitionOptions extends RuntimeProviderOptions {
61
82
  connectionMode?: ConnectionMode;
62
83
  authTypes?: string[];
@@ -66,8 +87,9 @@ export interface PluginDefinitionOptions extends RuntimeProviderOptions {
66
87
  sessionCatalog?: SessionCatalogHandler;
67
88
  }
68
89
 
69
- export type IntegrationProviderOptions = PluginDefinitionOptions;
70
-
90
+ /**
91
+ * Normalizes a plugin operation definition.
92
+ */
71
93
  export function operation<In, Out>(
72
94
  options: OperationOptions<In, Out>,
73
95
  ): OperationDefinition<In, Out> {
@@ -82,7 +104,31 @@ export function operation<In, Out>(
82
104
  };
83
105
  }
84
106
 
85
- export class IntegrationProvider extends RuntimeProvider {
107
+ /**
108
+ * Plugin provider implementation consumed by the Gestalt runtime.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * import { definePlugin, ok, operation, s } from "@valon-technologies/gestalt";
113
+ *
114
+ * export const plugin = definePlugin({
115
+ * displayName: "Example Provider",
116
+ * operations: [
117
+ * operation({
118
+ * id: "ping",
119
+ * method: "GET",
120
+ * readOnly: true,
121
+ * input: s.object({ name: s.string({ default: "World" }) }),
122
+ * output: s.object({ message: s.string() }),
123
+ * async handler(input) {
124
+ * return ok({ message: `Hello, ${input.name}` });
125
+ * },
126
+ * }),
127
+ * ],
128
+ * });
129
+ * ```
130
+ */
131
+ export class PluginProvider extends RuntimeProvider {
86
132
  readonly kind = "integration" as const;
87
133
  readonly iconSvg: string;
88
134
  readonly connectionMode: ConnectionMode;
@@ -90,10 +136,7 @@ export class IntegrationProvider extends RuntimeProvider {
90
136
  readonly connectionParams: Record<string, ConnectionParamDefinition>;
91
137
 
92
138
  private readonly sessionCatalogHandler: SessionCatalogHandler | undefined;
93
- private readonly operations = new Map<
94
- string,
95
- OperationDefinition<any, any>
96
- >();
139
+ private readonly operations = new Map<string, OperationDefinition<any, any>>();
97
140
 
98
141
  constructor(options: PluginDefinitionOptions) {
99
142
  super(options);
@@ -103,36 +146,37 @@ export class IntegrationProvider extends RuntimeProvider {
103
146
  this.connectionParams = normalizeConnectionParams(options.connectionParams);
104
147
  this.sessionCatalogHandler = options.sessionCatalog;
105
148
 
106
- for (const entry of options.operations) {
107
- const id = entry.id.trim();
108
- if (!id) {
149
+ for (const rawEntry of options.operations) {
150
+ const entry = operation(rawEntry);
151
+ if (!entry.id) {
109
152
  throw new Error("operation id is required");
110
153
  }
111
- if (this.operations.has(id)) {
112
- throw new Error(`duplicate operation id ${JSON.stringify(id)}`);
154
+ if (this.operations.has(entry.id)) {
155
+ throw new Error(`duplicate operation id ${JSON.stringify(entry.id)}`);
113
156
  }
114
- this.operations.set(id, {
115
- ...entry,
116
- id,
117
- method: normalizeMethod(entry.method),
118
- title: entry.title?.trim() ?? "",
119
- description: entry.description?.trim() ?? "",
120
- allowedRoles: normalizeAllowedRoles(entry.allowedRoles),
121
- tags: [...(entry.tags ?? [])],
122
- });
157
+ this.operations.set(entry.id, entry);
123
158
  }
124
159
  }
125
160
 
161
+ /**
162
+ * Reports whether the provider exposes a session-specific catalog.
163
+ */
126
164
  supportsSessionCatalog(): boolean {
127
165
  return this.sessionCatalogHandler !== undefined;
128
166
  }
129
167
 
168
+ /**
169
+ * Resolves a catalog for the current request context, if configured.
170
+ */
130
171
  async catalogForRequest(
131
172
  request: Request,
132
173
  ): Promise<SessionCatalog | null | undefined> {
133
174
  return await this.sessionCatalogHandler?.(request);
134
175
  }
135
176
 
177
+ /**
178
+ * Returns the static catalog emitted during provider startup.
179
+ */
136
180
  staticCatalog(): Catalog {
137
181
  const catalog: Catalog = {
138
182
  operations: [...this.operations.values()].map<CatalogOperation>(
@@ -197,14 +241,23 @@ export class IntegrationProvider extends RuntimeProvider {
197
241
  return catalog;
198
242
  }
199
243
 
244
+ /**
245
+ * Writes the provider's static catalog to disk as YAML.
246
+ */
200
247
  writeCatalog(path: string): void {
201
248
  writeCatalogYaml(path, this.staticCatalog());
202
249
  }
203
250
 
251
+ /**
252
+ * Returns the static catalog serialized as JSON.
253
+ */
204
254
  catalogJson(): string {
205
255
  return catalogToJson(this.staticCatalog());
206
256
  }
207
257
 
258
+ /**
259
+ * Executes an operation against validated input and request metadata.
260
+ */
208
261
  async execute(
209
262
  operationId: string,
210
263
  params: Record<string, unknown>,
@@ -244,25 +297,23 @@ export class IntegrationProvider extends RuntimeProvider {
244
297
  }
245
298
  }
246
299
 
247
- export const Plugin = IntegrationProvider;
248
-
249
- export function defineIntegrationProvider(
250
- options: PluginDefinitionOptions,
251
- ): IntegrationProvider {
252
- return new IntegrationProvider(options);
253
- }
254
-
300
+ /**
301
+ * Creates a plugin provider.
302
+ */
255
303
  export function definePlugin(
256
304
  options: PluginDefinitionOptions,
257
- ): IntegrationProvider {
258
- return new IntegrationProvider(options);
305
+ ): PluginProvider {
306
+ return new PluginProvider(options);
259
307
  }
260
308
 
261
- export function isIntegrationProvider(
309
+ /**
310
+ * Runtime type guard for plugin providers loaded from user modules.
311
+ */
312
+ export function isPluginProvider(
262
313
  value: unknown,
263
- ): value is IntegrationProvider {
314
+ ): value is PluginProvider {
264
315
  return (
265
- value instanceof IntegrationProvider ||
316
+ value instanceof PluginProvider ||
266
317
  (typeof value === "object" &&
267
318
  value !== null &&
268
319
  "kind" in value &&
@@ -353,6 +404,9 @@ function errorResult(status: number, message: string): OperationResult {
353
404
  };
354
405
  }
355
406
 
407
+ /**
408
+ * Converts a connection mode into the shared protocol enum value.
409
+ */
356
410
  export function connectionModeToProtoValue(mode: ConnectionMode): number {
357
411
  switch (mode) {
358
412
  case "none":
@@ -361,14 +415,15 @@ export function connectionModeToProtoValue(mode: ConnectionMode): number {
361
415
  return 2;
362
416
  case "identity":
363
417
  return 3;
364
- case "either":
365
- return 4;
366
418
  case "unspecified":
367
419
  default:
368
420
  return 0;
369
421
  }
370
422
  }
371
423
 
424
+ /**
425
+ * Converts a connection parameter definition into protocol wire metadata.
426
+ */
372
427
  export function connectionParamToProto(value: ConnectionParamDefinition): {
373
428
  required?: boolean;
374
429
  description?: string;
@@ -0,0 +1,107 @@
1
+ import type { ProviderKind } from "./provider.ts";
2
+
3
+ type ProviderKindDefinition = {
4
+ readonly tokens: readonly string[];
5
+ readonly formatToken: string;
6
+ readonly defaultExportNames: readonly string[];
7
+ readonly label: string;
8
+ };
9
+
10
+ const PROVIDER_KIND_DEFINITIONS = {
11
+ integration: {
12
+ tokens: ["plugin"],
13
+ formatToken: "plugin",
14
+ defaultExportNames: ["provider", "plugin"],
15
+ label: "plugin provider",
16
+ },
17
+ authentication: {
18
+ tokens: ["authentication"],
19
+ formatToken: "authentication",
20
+ defaultExportNames: ["authentication", "provider"],
21
+ label: "authentication provider",
22
+ },
23
+ cache: {
24
+ tokens: ["cache"],
25
+ formatToken: "cache",
26
+ defaultExportNames: ["cache", "provider"],
27
+ label: "cache provider",
28
+ },
29
+ secrets: {
30
+ tokens: ["secrets"],
31
+ formatToken: "secrets",
32
+ defaultExportNames: ["secrets", "provider"],
33
+ label: "secrets provider",
34
+ },
35
+ s3: {
36
+ tokens: ["s3"],
37
+ formatToken: "s3",
38
+ defaultExportNames: ["s3", "provider"],
39
+ label: "s3 provider",
40
+ },
41
+ workflow: {
42
+ tokens: ["workflow"],
43
+ formatToken: "workflow",
44
+ defaultExportNames: ["workflow", "provider"],
45
+ label: "workflow provider",
46
+ },
47
+ telemetry: {
48
+ tokens: ["telemetry"],
49
+ formatToken: "telemetry",
50
+ defaultExportNames: ["telemetry", "provider"],
51
+ label: "telemetry provider",
52
+ },
53
+ } satisfies Record<ProviderKind, ProviderKindDefinition>;
54
+
55
+ const EXTERNAL_PROVIDER_KIND_TOKEN_SET = new Set<string>(
56
+ Object.values(PROVIDER_KIND_DEFINITIONS).flatMap(
57
+ (definition) => definition.tokens,
58
+ ),
59
+ );
60
+
61
+ const EXTERNAL_PROVIDER_KIND_MAP = new Map<string, ProviderKind>(
62
+ Object.entries(PROVIDER_KIND_DEFINITIONS).flatMap(([kind, definition]) =>
63
+ definition.tokens.map(
64
+ (token) => [token, kind as ProviderKind] as const,
65
+ ),
66
+ ),
67
+ );
68
+
69
+ export function isExternalProviderKindToken(value: string): boolean {
70
+ return EXTERNAL_PROVIDER_KIND_TOKEN_SET.has(value.trim().toLowerCase());
71
+ }
72
+
73
+ export function parseExternalProviderKind(value: string): ProviderKind {
74
+ const normalized = value.trim().toLowerCase();
75
+ const kind = EXTERNAL_PROVIDER_KIND_MAP.get(normalized);
76
+ if (!kind) {
77
+ throw new Error(`unsupported provider kind ${JSON.stringify(value)}`);
78
+ }
79
+ return kind;
80
+ }
81
+
82
+ export function formatExternalProviderKind(kind: ProviderKind): string {
83
+ return PROVIDER_KIND_DEFINITIONS[kind].formatToken;
84
+ }
85
+
86
+ export function providerKindLabel(kind: ProviderKind): string {
87
+ return PROVIDER_KIND_DEFINITIONS[kind].label;
88
+ }
89
+
90
+ export function defaultProviderExportNames(
91
+ kind: ProviderKind,
92
+ ): readonly string[] {
93
+ return PROVIDER_KIND_DEFINITIONS[kind].defaultExportNames;
94
+ }
95
+
96
+ export function resolveDefaultProviderExport(
97
+ module: Record<string, unknown>,
98
+ kind: ProviderKind,
99
+ ): unknown {
100
+ for (const exportName of defaultProviderExportNames(kind)) {
101
+ const candidate = Reflect.get(module, exportName);
102
+ if (candidate !== undefined && candidate !== null) {
103
+ return candidate;
104
+ }
105
+ }
106
+ return Reflect.get(module, "default");
107
+ }
package/src/provider.ts CHANGED
@@ -1,13 +1,20 @@
1
1
  import type { MaybePromise } from "./api.ts";
2
2
 
3
+ /**
4
+ * Provider kinds supported by the TypeScript SDK runtime.
5
+ */
3
6
  export type ProviderKind =
4
7
  | "integration"
5
- | "auth"
8
+ | "authentication"
6
9
  | "cache"
7
10
  | "secrets"
8
11
  | "s3"
12
+ | "workflow"
9
13
  | "telemetry";
10
14
 
15
+ /**
16
+ * Runtime metadata reported to the Gestalt host during startup.
17
+ */
11
18
  export type ProviderMetadata = {
12
19
  kind?: ProviderKind;
13
20
  name?: string;
@@ -16,17 +23,32 @@ export type ProviderMetadata = {
16
23
  version?: string;
17
24
  };
18
25
 
26
+ /**
27
+ * Optional configuration hook invoked after the host starts the provider.
28
+ */
19
29
  export type ConfigureHandler = (
20
30
  name: string,
21
31
  config: Record<string, unknown>,
22
32
  ) => MaybePromise<void>;
23
33
 
34
+ /**
35
+ * Optional readiness probe invoked by the Gestalt host.
36
+ */
24
37
  export type HealthCheckHandler = () => MaybePromise<void>;
25
38
 
39
+ /**
40
+ * Optional callback that returns non-fatal runtime warnings.
41
+ */
26
42
  export type WarningsHandler = () => MaybePromise<string[]>;
27
43
 
44
+ /**
45
+ * Optional shutdown hook invoked when the provider process exits.
46
+ */
28
47
  export type CloseHandler = () => MaybePromise<void>;
29
48
 
49
+ /**
50
+ * Shared runtime metadata and lifecycle hooks for authored providers.
51
+ */
30
52
  export interface RuntimeProviderOptions {
31
53
  name?: string;
32
54
  displayName?: string;
@@ -38,6 +60,9 @@ export interface RuntimeProviderOptions {
38
60
  close?: CloseHandler;
39
61
  }
40
62
 
63
+ /**
64
+ * Base class shared by all TypeScript SDK provider implementations.
65
+ */
41
66
  export abstract class RuntimeProvider {
42
67
  abstract readonly kind: ProviderKind;
43
68
 
@@ -116,6 +141,9 @@ export abstract class RuntimeProvider {
116
141
  }
117
142
  }
118
143
 
144
+ /**
145
+ * Runtime type guard for values that implement the provider base contract.
146
+ */
119
147
  export function isRuntimeProvider(value: unknown): value is RuntimeProvider {
120
148
  return (
121
149
  value instanceof RuntimeProvider ||
@@ -127,6 +155,9 @@ export function isRuntimeProvider(value: unknown): value is RuntimeProvider {
127
155
  );
128
156
  }
129
157
 
158
+ /**
159
+ * Normalizes package and provider names into Gestalt's slug format.
160
+ */
130
161
  export function slugName(value: string): string {
131
162
  const normalized = value.trim().replace(/^@[^/]+\//, "");
132
163
  return normalized.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");