@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.
- package/gen/v1/plugin_pb.ts +231 -13
- package/gen/v1/workflow_pb.ts +274 -7
- package/package.json +1 -1
- package/src/authorization.ts +138 -0
- package/src/cache.ts +69 -9
- package/src/http-subject.ts +113 -0
- package/src/index.ts +55 -0
- package/src/indexeddb.ts +59 -6
- package/src/invoker.ts +87 -9
- package/src/manifest-metadata.ts +106 -0
- package/src/plugin.ts +202 -0
- package/src/runtime.ts +94 -3
- package/src/s3.ts +11 -4
- package/src/workflow-manager.ts +95 -0
|
@@ -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
|
-
|
|
495
|
+
constructor(name?: string) {
|
|
451
496
|
const envName = indexedDBSocketEnv(name);
|
|
452
|
-
const
|
|
453
|
-
if (!
|
|
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
|
-
|
|
458
|
-
|
|
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
|
|
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
|
-
|
|
38
|
-
|
|
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
|
+
}
|