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

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,113 @@
1
+ import type { Access, Credential, MaybePromise, Subject } from "./api.ts";
2
+
3
+ /**
4
+ * Verified hosted HTTP request metadata passed into optional plugin-local
5
+ * subject resolution hooks before normal operation dispatch.
6
+ */
7
+ export interface HTTPSubjectRequest {
8
+ binding: string;
9
+ method: string;
10
+ path: string;
11
+ contentType: string;
12
+ headers: Record<string, string[]>;
13
+ query: Record<string, string[]>;
14
+ params: Record<string, unknown>;
15
+ rawBody: Uint8Array;
16
+ securityScheme: string;
17
+ verifiedSubject: string;
18
+ verifiedClaims: Record<string, string>;
19
+ }
20
+
21
+ /**
22
+ * Request-scoped caller context available while resolving the concrete subject
23
+ * for a hosted HTTP request.
24
+ */
25
+ export interface HTTPSubjectResolutionContext {
26
+ subject: Subject;
27
+ credential: Credential;
28
+ access: Access;
29
+ workflow: Record<string, unknown>;
30
+ }
31
+
32
+ /**
33
+ * Explicit HTTP rejection surfaced from a hosted HTTP subject resolver.
34
+ */
35
+ export class HTTPSubjectResolutionError extends Error {
36
+ readonly status: number;
37
+
38
+ constructor(status: number, message: string) {
39
+ super(message);
40
+ this.name = "HTTPSubjectResolutionError";
41
+ this.status = status;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Creates an explicit hosted HTTP subject-resolution rejection.
47
+ */
48
+ export function httpSubjectError(
49
+ status: number,
50
+ message: string,
51
+ ): HTTPSubjectResolutionError {
52
+ return new HTTPSubjectResolutionError(status, message);
53
+ }
54
+
55
+ /**
56
+ * Optional hook that maps a verified hosted HTTP request to a concrete Gestalt
57
+ * subject before the target operation is authorized and executed.
58
+ */
59
+ export type HTTPSubjectResolver = (
60
+ request: HTTPSubjectRequest,
61
+ context: HTTPSubjectResolutionContext,
62
+ ) => MaybePromise<Subject | null | undefined>;
63
+
64
+ export function cloneHTTPSubjectRequest(
65
+ input: HTTPSubjectRequest,
66
+ ): HTTPSubjectRequest {
67
+ return {
68
+ binding: input.binding,
69
+ method: input.method,
70
+ path: input.path,
71
+ contentType: input.contentType,
72
+ headers: cloneStringLists(input.headers),
73
+ query: cloneStringLists(input.query),
74
+ params: {
75
+ ...input.params,
76
+ },
77
+ rawBody: new Uint8Array(input.rawBody),
78
+ securityScheme: input.securityScheme,
79
+ verifiedSubject: input.verifiedSubject,
80
+ verifiedClaims: {
81
+ ...input.verifiedClaims,
82
+ },
83
+ };
84
+ }
85
+
86
+ export function cloneHTTPSubjectResolutionContext(
87
+ input: HTTPSubjectResolutionContext,
88
+ ): HTTPSubjectResolutionContext {
89
+ return {
90
+ subject: {
91
+ ...input.subject,
92
+ },
93
+ credential: {
94
+ ...input.credential,
95
+ },
96
+ access: {
97
+ ...input.access,
98
+ },
99
+ workflow: {
100
+ ...input.workflow,
101
+ },
102
+ };
103
+ }
104
+
105
+ function cloneStringLists(
106
+ input: Record<string, string[]>,
107
+ ): Record<string, string[]> {
108
+ const output: Record<string, string[]> = {};
109
+ for (const [key, value] of Object.entries(input)) {
110
+ output[key] = [...value];
111
+ }
112
+ return output;
113
+ }
package/src/index.ts CHANGED
@@ -28,6 +28,22 @@
28
28
  * import { parseRuntimeArgs, serve } from "@valon-technologies/gestalt/runtime";
29
29
  * ```
30
30
  */
31
+ export {
32
+ Authorization,
33
+ AuthorizationClient,
34
+ ENV_AUTHORIZATION_SOCKET,
35
+ type AuthorizationActionSearchMessage,
36
+ type AuthorizationDecisionMessage,
37
+ type AuthorizationEvaluateInput,
38
+ type AuthorizationMetadataMessage,
39
+ type AuthorizationReadRelationshipsInput,
40
+ type AuthorizationReadRelationshipsMessage,
41
+ type AuthorizationResourceSearchMessage,
42
+ type AuthorizationSearchActionsInput,
43
+ type AuthorizationSearchResourcesInput,
44
+ type AuthorizationSearchSubjectsInput,
45
+ type AuthorizationSubjectSearchMessage,
46
+ } from "./authorization.ts";
31
47
  export {
32
48
  connectionParam,
33
49
  ok,
@@ -42,6 +58,13 @@ export {
42
58
  type Response,
43
59
  type Subject,
44
60
  } from "./api.ts";
61
+ export {
62
+ type HTTPSubjectRequest,
63
+ type HTTPSubjectResolutionContext,
64
+ HTTPSubjectResolutionError,
65
+ type HTTPSubjectResolver,
66
+ httpSubjectError,
67
+ } from "./http-subject.ts";
45
68
  export {
46
69
  catalogToJson,
47
70
  catalogToYaml,
@@ -53,6 +76,21 @@ export {
53
76
  type CatalogParameter,
54
77
  type CatalogSchema,
55
78
  } from "./catalog.ts";
79
+ export {
80
+ hasPluginManifestMetadata,
81
+ manifestMetadataToYaml,
82
+ writeManifestMetadataYaml,
83
+ type HTTPAck,
84
+ type HTTPAuthScheme,
85
+ type HTTPBinding,
86
+ type HTTPIn,
87
+ type HTTPMediaType,
88
+ type HTTPRequestBody,
89
+ type HTTPSecretRef,
90
+ type HTTPSecurityScheme,
91
+ type HTTPSecuritySchemeType,
92
+ type PluginManifestMetadata,
93
+ } from "./manifest-metadata.ts";
56
94
  export {
57
95
  buildProviderBinary,
58
96
  bunBuildCommand,
@@ -61,18 +99,30 @@ export {
61
99
  } from "./build.ts";
62
100
  export {
63
101
  ENV_PLUGIN_INVOKER_SOCKET,
102
+ ENV_PLUGIN_INVOKER_SOCKET_TOKEN,
64
103
  PluginInvoker,
104
+ type PluginGraphQLInvokeOptions,
105
+ type PluginInvocationGrant,
65
106
  type PluginInvokeOptions,
66
107
  } from "./invoker.ts";
67
108
  export {
68
109
  ENV_WORKFLOW_MANAGER_SOCKET,
69
110
  WorkflowManager,
111
+ type ManagedWorkflowEventTriggerMessage,
70
112
  type ManagedWorkflowScheduleMessage,
113
+ type WorkflowEventMessage,
114
+ type WorkflowManagerCreateTriggerInput,
71
115
  type WorkflowManagerCreateScheduleInput,
116
+ type WorkflowManagerDeleteTriggerInput,
72
117
  type WorkflowManagerDeleteScheduleInput,
118
+ type WorkflowManagerGetTriggerInput,
73
119
  type WorkflowManagerGetScheduleInput,
120
+ type WorkflowManagerPauseTriggerInput,
74
121
  type WorkflowManagerPauseScheduleInput,
122
+ type WorkflowManagerPublishEventInput,
123
+ type WorkflowManagerResumeTriggerInput,
75
124
  type WorkflowManagerResumeScheduleInput,
125
+ type WorkflowManagerUpdateTriggerInput,
76
126
  type WorkflowManagerUpdateScheduleInput,
77
127
  } from "./workflow-manager.ts";
78
128
  export {
@@ -90,8 +140,11 @@ export {
90
140
  Cache,
91
141
  CacheProvider,
92
142
  cacheSocketEnv,
143
+ cacheSocketTokenEnv,
93
144
  defineCacheProvider,
94
145
  isCacheProvider,
146
+ ENV_CACHE_SOCKET,
147
+ ENV_CACHE_SOCKET_TOKEN,
95
148
  type CacheEntry,
96
149
  type CacheProviderOptions,
97
150
  type CacheSetOptions,
@@ -146,6 +199,7 @@ export {
146
199
  ENV_PROVIDER_PARENT_PID,
147
200
  ENV_PROVIDER_SOCKET,
148
201
  ENV_WRITE_CATALOG,
202
+ ENV_WRITE_MANIFEST_METADATA,
149
203
  createAuthenticationService,
150
204
  createCacheService,
151
205
  createSecretsService,
@@ -182,6 +236,7 @@ export {
182
236
  AlreadyExistsError,
183
237
  ColumnType,
184
238
  indexedDBSocketEnv,
239
+ indexedDBSocketTokenEnv,
185
240
  type Record,
186
241
  type KeyRange,
187
242
  type ColumnSchema,
package/src/indexeddb.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { createClient, type Client } from "@connectrpc/connect";
1
+ import { createClient, type Client, type Interceptor } from "@connectrpc/connect";
2
2
  import { createGrpcTransport } from "@connectrpc/connect-node";
3
3
  import {
4
4
  IndexedDB as IndexedDBService,
@@ -6,6 +6,8 @@ import {
6
6
  } from "../gen/v1/datastore_pb";
7
7
 
8
8
  const ENV_INDEXEDDB_SOCKET = "GESTALT_INDEXEDDB_SOCKET";
9
+ const INDEXEDDB_SOCKET_TOKEN_SUFFIX = "_TOKEN";
10
+ const INDEXEDDB_RELAY_TOKEN_HEADER = "x-gestalt-host-service-relay-token";
9
11
 
10
12
  /**
11
13
  * Returns the environment variable name used to discover an IndexedDB socket.
@@ -16,6 +18,49 @@ export function indexedDBSocketEnv(name?: string): string {
16
18
  return `${ENV_INDEXEDDB_SOCKET}_${trimmed.replace(/[^A-Za-z0-9]/g, "_").toUpperCase()}`;
17
19
  }
18
20
 
21
+ /**
22
+ * Returns the environment variable name used to discover an IndexedDB relay token.
23
+ */
24
+ export function indexedDBSocketTokenEnv(name?: string): string {
25
+ return `${indexedDBSocketEnv(name)}${INDEXEDDB_SOCKET_TOKEN_SUFFIX}`;
26
+ }
27
+
28
+ function indexedDBTransportOptions(rawTarget: string): {
29
+ baseUrl: string;
30
+ nodeOptions?: { path: string };
31
+ } {
32
+ const target = rawTarget.trim();
33
+ if (!target) {
34
+ throw new Error("IndexedDB transport target is required");
35
+ }
36
+ if (target.startsWith("tcp://")) {
37
+ const address = target.slice("tcp://".length).trim();
38
+ if (!address) {
39
+ throw new Error(`IndexedDB tcp target ${JSON.stringify(rawTarget)} is missing host:port`);
40
+ }
41
+ return { baseUrl: `http://${address}` };
42
+ }
43
+ if (target.startsWith("tls://")) {
44
+ const address = target.slice("tls://".length).trim();
45
+ if (!address) {
46
+ throw new Error(`IndexedDB tls target ${JSON.stringify(rawTarget)} is missing host:port`);
47
+ }
48
+ return { baseUrl: `https://${address}` };
49
+ }
50
+ if (target.startsWith("unix://")) {
51
+ const socketPath = target.slice("unix://".length).trim();
52
+ if (!socketPath) {
53
+ throw new Error(`IndexedDB unix target ${JSON.stringify(rawTarget)} is missing a socket path`);
54
+ }
55
+ return { baseUrl: "http://localhost", nodeOptions: { path: socketPath } };
56
+ }
57
+ if (target.includes("://")) {
58
+ const parsed = new URL(target);
59
+ throw new Error(`Unsupported IndexedDB target scheme ${JSON.stringify(parsed.protocol.replace(/:$/, ""))}`);
60
+ }
61
+ return { baseUrl: "http://localhost", nodeOptions: { path: target } };
62
+ }
63
+
19
64
  class AsyncQueue<T> implements AsyncIterable<T> {
20
65
  private queue: T[] = [];
21
66
  private waiting: ((result: IteratorResult<T>) => void) | null = null;
@@ -447,15 +492,16 @@ export interface ObjectStoreSchema {
447
492
  export class IndexedDB {
448
493
  private client: Client<typeof IndexedDBService>;
449
494
 
450
- constructor(name?: string) {
495
+ constructor(name?: string) {
451
496
  const envName = indexedDBSocketEnv(name);
452
- const socketPath = process.env[envName];
453
- if (!socketPath) {
497
+ const target = process.env[envName];
498
+ if (!target) {
454
499
  throw new Error(`${envName} is not set`);
455
500
  }
501
+ const token = process.env[indexedDBSocketTokenEnv(name)]?.trim() ?? "";
456
502
  const transport = createGrpcTransport({
457
- baseUrl: `http://localhost`,
458
- nodeOptions: { path: socketPath },
503
+ ...indexedDBTransportOptions(target),
504
+ interceptors: token ? [indexedDBRelayTokenInterceptor(token)] : [],
459
505
  });
460
506
  this.client = createClient(IndexedDBService, transport);
461
507
  }
@@ -498,6 +544,13 @@ export class IndexedDB {
498
544
  }
499
545
  }
500
546
 
547
+ function indexedDBRelayTokenInterceptor(token: string): Interceptor {
548
+ return (next) => async (req) => {
549
+ req.header.set(INDEXEDDB_RELAY_TOKEN_HEADER, token);
550
+ return next(req);
551
+ };
552
+ }
553
+
501
554
  /**
502
555
  * Object store client used for primary-key operations.
503
556
  */
package/src/invoker.ts CHANGED
@@ -1,13 +1,13 @@
1
- import { connect } from "node:net";
2
-
3
1
  import type { JsonObject, JsonValue } from "@bufbuild/protobuf";
4
- import { createClient, type Client } from "@connectrpc/connect";
2
+ import { createClient, type Client, type Interceptor } from "@connectrpc/connect";
5
3
  import { createGrpcTransport } from "@connectrpc/connect-node";
6
4
 
7
5
  import { PluginInvoker as PluginInvokerService } from "../gen/v1/plugin_pb.ts";
8
6
  import type { OperationResult, Request } from "./api.ts";
9
7
 
10
8
  export const ENV_PLUGIN_INVOKER_SOCKET = "GESTALT_PLUGIN_INVOKER_SOCKET";
9
+ export const ENV_PLUGIN_INVOKER_SOCKET_TOKEN = `${ENV_PLUGIN_INVOKER_SOCKET}_TOKEN`;
10
+ const PLUGIN_INVOKER_RELAY_TOKEN_HEADER = "x-gestalt-host-service-relay-token";
11
11
 
12
12
  export interface PluginInvokeOptions {
13
13
  connection?: string;
@@ -16,7 +16,13 @@ export interface PluginInvokeOptions {
16
16
 
17
17
  export interface PluginInvocationGrant {
18
18
  plugin: string;
19
- operations: string[];
19
+ operations?: string[];
20
+ surfaces?: string[];
21
+ allOperations?: boolean;
22
+ }
23
+
24
+ export interface PluginGraphQLInvokeOptions extends PluginInvokeOptions {
25
+ variables?: Record<string, unknown>;
20
26
  }
21
27
 
22
28
  export class PluginInvoker {
@@ -32,12 +38,11 @@ export class PluginInvoker {
32
38
  if (!socketPath) {
33
39
  throw new Error(`plugin invoker: ${ENV_PLUGIN_INVOKER_SOCKET} is not set`);
34
40
  }
41
+ const relayToken = process.env[ENV_PLUGIN_INVOKER_SOCKET_TOKEN]?.trim() ?? "";
35
42
 
36
43
  const transport = createGrpcTransport({
37
- baseUrl: "http://localhost",
38
- nodeOptions: {
39
- createConnection: () => connect(socketPath),
40
- },
44
+ ...pluginInvokerTransportOptions(socketPath),
45
+ interceptors: relayToken ? [pluginInvokerRelayTokenInterceptor(relayToken)] : [],
41
46
  });
42
47
  this.client = createClient(PluginInvokerService, transport);
43
48
  }
@@ -62,6 +67,32 @@ export class PluginInvoker {
62
67
  };
63
68
  }
64
69
 
70
+ async invokeGraphQL(
71
+ plugin: string,
72
+ document: string,
73
+ options?: PluginGraphQLInvokeOptions,
74
+ ): Promise<OperationResult> {
75
+ const trimmedDocument = document.trim();
76
+ if (!trimmedDocument) {
77
+ throw new Error("plugin invoker: graphql document is required");
78
+ }
79
+
80
+ const response = await this.client.invokeGraphQL({
81
+ invocationToken: this.invocationToken,
82
+ plugin,
83
+ document: trimmedDocument,
84
+ ...(options?.variables
85
+ ? { variables: toJsonObject(options.variables) }
86
+ : {}),
87
+ connection: options?.connection ?? "",
88
+ instance: options?.instance ?? "",
89
+ });
90
+ return {
91
+ status: response.status,
92
+ body: response.body,
93
+ };
94
+ }
95
+
65
96
  async exchangeInvocationToken(options?: {
66
97
  grants?: PluginInvocationGrant[];
67
98
  ttlSeconds?: number;
@@ -71,9 +102,13 @@ export class PluginInvoker {
71
102
  grants: (options?.grants ?? [])
72
103
  .map((grant) => ({
73
104
  plugin: grant.plugin.trim(),
74
- operations: grant.operations
105
+ operations: (grant.operations ?? [])
75
106
  .map((operation) => operation.trim())
76
107
  .filter(Boolean),
108
+ surfaces: (grant.surfaces ?? [])
109
+ .map((surface) => surface.trim().toLowerCase())
110
+ .filter(Boolean),
111
+ allOperations: grant.allOperations ?? false,
77
112
  }))
78
113
  .filter((grant) => grant.plugin.length > 0),
79
114
  ttlSeconds: BigInt(Math.max(0, options?.ttlSeconds ?? 0)),
@@ -82,6 +117,49 @@ export class PluginInvoker {
82
117
  }
83
118
  }
84
119
 
120
+ function pluginInvokerTransportOptions(rawTarget: string): {
121
+ baseUrl: string;
122
+ nodeOptions?: { path: string };
123
+ } {
124
+ const target = rawTarget.trim();
125
+ if (!target) {
126
+ throw new Error("plugin invoker: transport target is required");
127
+ }
128
+ if (target.startsWith("tcp://")) {
129
+ const address = target.slice("tcp://".length).trim();
130
+ if (!address) {
131
+ throw new Error(`plugin invoker: tcp target ${JSON.stringify(rawTarget)} is missing host:port`);
132
+ }
133
+ return { baseUrl: `http://${address}` };
134
+ }
135
+ if (target.startsWith("tls://")) {
136
+ const address = target.slice("tls://".length).trim();
137
+ if (!address) {
138
+ throw new Error(`plugin invoker: tls target ${JSON.stringify(rawTarget)} is missing host:port`);
139
+ }
140
+ return { baseUrl: `https://${address}` };
141
+ }
142
+ if (target.startsWith("unix://")) {
143
+ const socketPath = target.slice("unix://".length).trim();
144
+ if (!socketPath) {
145
+ throw new Error(`plugin invoker: unix target ${JSON.stringify(rawTarget)} is missing a socket path`);
146
+ }
147
+ return { baseUrl: "http://localhost", nodeOptions: { path: socketPath } };
148
+ }
149
+ if (target.includes("://")) {
150
+ const parsed = new URL(target);
151
+ throw new Error(`plugin invoker: unsupported target scheme ${JSON.stringify(parsed.protocol.replace(/:$/, ""))}`);
152
+ }
153
+ return { baseUrl: "http://localhost", nodeOptions: { path: target } };
154
+ }
155
+
156
+ function pluginInvokerRelayTokenInterceptor(token: string): Interceptor {
157
+ return (next) => async (req) => {
158
+ req.header.set(PLUGIN_INVOKER_RELAY_TOKEN_HEADER, token);
159
+ return next(req);
160
+ };
161
+ }
162
+
85
163
  function normalizeInvocationToken(requestOrToken: Request | string): string {
86
164
  const invocationToken =
87
165
  typeof requestOrToken === "string"
@@ -0,0 +1,106 @@
1
+ import { writeFileSync } from "node:fs";
2
+
3
+ import YAML from "yaml";
4
+
5
+ export type HTTPSecuritySchemeType =
6
+ | "hmac"
7
+ | "apiKey"
8
+ | "http"
9
+ | "none";
10
+
11
+ export type HTTPIn = "header" | "query";
12
+
13
+ export type HTTPAuthScheme = "basic" | "bearer";
14
+
15
+ export interface HTTPSecretRef {
16
+ env?: string;
17
+ secret?: string;
18
+ }
19
+
20
+ export interface HTTPSecurityScheme {
21
+ type?: HTTPSecuritySchemeType;
22
+ description?: string;
23
+ signatureHeader?: string;
24
+ signaturePrefix?: string;
25
+ payloadTemplate?: string;
26
+ timestampHeader?: string;
27
+ maxAgeSeconds?: number;
28
+ name?: string;
29
+ in?: HTTPIn;
30
+ scheme?: HTTPAuthScheme;
31
+ secret?: HTTPSecretRef;
32
+ }
33
+
34
+ export interface HTTPMediaType {}
35
+
36
+ export interface HTTPRequestBody {
37
+ required?: boolean;
38
+ content?: Record<string, HTTPMediaType>;
39
+ }
40
+
41
+ export interface HTTPAck {
42
+ status?: number;
43
+ headers?: Record<string, string>;
44
+ body?: any;
45
+ }
46
+
47
+ export interface HTTPBinding {
48
+ path: string;
49
+ method: string;
50
+ requestBody?: HTTPRequestBody;
51
+ security: string;
52
+ target: string;
53
+ ack?: HTTPAck;
54
+ }
55
+
56
+ export interface PluginManifestMetadata {
57
+ securitySchemes?: Record<string, HTTPSecurityScheme>;
58
+ http?: Record<string, HTTPBinding>;
59
+ }
60
+
61
+ export function hasPluginManifestMetadata(
62
+ metadata: PluginManifestMetadata | null | undefined,
63
+ ): boolean {
64
+ return !!(
65
+ metadata &&
66
+ ((metadata.securitySchemes &&
67
+ Object.keys(metadata.securitySchemes).length > 0) ||
68
+ (metadata.http && Object.keys(metadata.http).length > 0))
69
+ );
70
+ }
71
+
72
+ export function manifestMetadataToYaml(
73
+ metadata: PluginManifestMetadata | Record<string, unknown>,
74
+ ): string {
75
+ return YAML.stringify(toManifestMetadataJsonObject(metadata));
76
+ }
77
+
78
+ export function writeManifestMetadataYaml(
79
+ path: string,
80
+ metadata: PluginManifestMetadata | Record<string, unknown>,
81
+ ): void {
82
+ writeFileSync(path, manifestMetadataToYaml(metadata), "utf8");
83
+ }
84
+
85
+ function toManifestMetadataJsonObject(
86
+ metadata: PluginManifestMetadata | Record<string, unknown>,
87
+ ): Record<string, unknown> {
88
+ if (!("securitySchemes" in metadata) && !("http" in metadata)) {
89
+ return {
90
+ ...metadata,
91
+ };
92
+ }
93
+
94
+ const typedMetadata = metadata as PluginManifestMetadata;
95
+ const output: Record<string, unknown> = {};
96
+ if (
97
+ typedMetadata.securitySchemes &&
98
+ Object.keys(typedMetadata.securitySchemes).length > 0
99
+ ) {
100
+ output.securitySchemes = typedMetadata.securitySchemes;
101
+ }
102
+ if (typedMetadata.http && Object.keys(typedMetadata.http).length > 0) {
103
+ output.http = typedMetadata.http;
104
+ }
105
+ return output;
106
+ }