@valon-technologies/gestalt 0.0.1-alpha.11 → 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 +166 -11
- 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 +37 -0
- package/src/indexeddb.ts +59 -6
- package/src/invoker.ts +49 -7
- package/src/manifest-metadata.ts +6 -1
- package/src/plugin.ts +40 -0
- package/src/runtime.ts +80 -0
- 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,
|
|
@@ -76,6 +99,7 @@ export {
|
|
|
76
99
|
} from "./build.ts";
|
|
77
100
|
export {
|
|
78
101
|
ENV_PLUGIN_INVOKER_SOCKET,
|
|
102
|
+
ENV_PLUGIN_INVOKER_SOCKET_TOKEN,
|
|
79
103
|
PluginInvoker,
|
|
80
104
|
type PluginGraphQLInvokeOptions,
|
|
81
105
|
type PluginInvocationGrant,
|
|
@@ -84,12 +108,21 @@ export {
|
|
|
84
108
|
export {
|
|
85
109
|
ENV_WORKFLOW_MANAGER_SOCKET,
|
|
86
110
|
WorkflowManager,
|
|
111
|
+
type ManagedWorkflowEventTriggerMessage,
|
|
87
112
|
type ManagedWorkflowScheduleMessage,
|
|
113
|
+
type WorkflowEventMessage,
|
|
114
|
+
type WorkflowManagerCreateTriggerInput,
|
|
88
115
|
type WorkflowManagerCreateScheduleInput,
|
|
116
|
+
type WorkflowManagerDeleteTriggerInput,
|
|
89
117
|
type WorkflowManagerDeleteScheduleInput,
|
|
118
|
+
type WorkflowManagerGetTriggerInput,
|
|
90
119
|
type WorkflowManagerGetScheduleInput,
|
|
120
|
+
type WorkflowManagerPauseTriggerInput,
|
|
91
121
|
type WorkflowManagerPauseScheduleInput,
|
|
122
|
+
type WorkflowManagerPublishEventInput,
|
|
123
|
+
type WorkflowManagerResumeTriggerInput,
|
|
92
124
|
type WorkflowManagerResumeScheduleInput,
|
|
125
|
+
type WorkflowManagerUpdateTriggerInput,
|
|
93
126
|
type WorkflowManagerUpdateScheduleInput,
|
|
94
127
|
} from "./workflow-manager.ts";
|
|
95
128
|
export {
|
|
@@ -107,8 +140,11 @@ export {
|
|
|
107
140
|
Cache,
|
|
108
141
|
CacheProvider,
|
|
109
142
|
cacheSocketEnv,
|
|
143
|
+
cacheSocketTokenEnv,
|
|
110
144
|
defineCacheProvider,
|
|
111
145
|
isCacheProvider,
|
|
146
|
+
ENV_CACHE_SOCKET,
|
|
147
|
+
ENV_CACHE_SOCKET_TOKEN,
|
|
112
148
|
type CacheEntry,
|
|
113
149
|
type CacheProviderOptions,
|
|
114
150
|
type CacheSetOptions,
|
|
@@ -200,6 +236,7 @@ export {
|
|
|
200
236
|
AlreadyExistsError,
|
|
201
237
|
ColumnType,
|
|
202
238
|
indexedDBSocketEnv,
|
|
239
|
+
indexedDBSocketTokenEnv,
|
|
203
240
|
type Record,
|
|
204
241
|
type KeyRange,
|
|
205
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;
|
|
@@ -38,12 +38,11 @@ export class PluginInvoker {
|
|
|
38
38
|
if (!socketPath) {
|
|
39
39
|
throw new Error(`plugin invoker: ${ENV_PLUGIN_INVOKER_SOCKET} is not set`);
|
|
40
40
|
}
|
|
41
|
+
const relayToken = process.env[ENV_PLUGIN_INVOKER_SOCKET_TOKEN]?.trim() ?? "";
|
|
41
42
|
|
|
42
43
|
const transport = createGrpcTransport({
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
createConnection: () => connect(socketPath),
|
|
46
|
-
},
|
|
44
|
+
...pluginInvokerTransportOptions(socketPath),
|
|
45
|
+
interceptors: relayToken ? [pluginInvokerRelayTokenInterceptor(relayToken)] : [],
|
|
47
46
|
});
|
|
48
47
|
this.client = createClient(PluginInvokerService, transport);
|
|
49
48
|
}
|
|
@@ -118,6 +117,49 @@ export class PluginInvoker {
|
|
|
118
117
|
}
|
|
119
118
|
}
|
|
120
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
|
+
|
|
121
163
|
function normalizeInvocationToken(requestOrToken: Request | string): string {
|
|
122
164
|
const invocationToken =
|
|
123
165
|
typeof requestOrToken === "string"
|
package/src/manifest-metadata.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { writeFileSync } from "node:fs";
|
|
|
3
3
|
import YAML from "yaml";
|
|
4
4
|
|
|
5
5
|
export type HTTPSecuritySchemeType =
|
|
6
|
-
| "
|
|
6
|
+
| "hmac"
|
|
7
7
|
| "apiKey"
|
|
8
8
|
| "http"
|
|
9
9
|
| "none";
|
|
@@ -20,6 +20,11 @@ export interface HTTPSecretRef {
|
|
|
20
20
|
export interface HTTPSecurityScheme {
|
|
21
21
|
type?: HTTPSecuritySchemeType;
|
|
22
22
|
description?: string;
|
|
23
|
+
signatureHeader?: string;
|
|
24
|
+
signaturePrefix?: string;
|
|
25
|
+
payloadTemplate?: string;
|
|
26
|
+
timestampHeader?: string;
|
|
27
|
+
maxAgeSeconds?: number;
|
|
23
28
|
name?: string;
|
|
24
29
|
in?: HTTPIn;
|
|
25
30
|
scheme?: HTTPAuthScheme;
|
package/src/plugin.ts
CHANGED
|
@@ -23,7 +23,15 @@ import {
|
|
|
23
23
|
type Request,
|
|
24
24
|
responseBrand,
|
|
25
25
|
type Response,
|
|
26
|
+
type Subject,
|
|
26
27
|
} from "./api.ts";
|
|
28
|
+
import {
|
|
29
|
+
cloneHTTPSubjectRequest,
|
|
30
|
+
cloneHTTPSubjectResolutionContext,
|
|
31
|
+
type HTTPSubjectRequest,
|
|
32
|
+
type HTTPSubjectResolutionContext,
|
|
33
|
+
type HTTPSubjectResolver,
|
|
34
|
+
} from "./http-subject.ts";
|
|
27
35
|
import { RuntimeProvider, type RuntimeProviderOptions } from "./provider.ts";
|
|
28
36
|
import type { Schema } from "./schema.ts";
|
|
29
37
|
|
|
@@ -93,6 +101,7 @@ export interface PluginDefinitionOptions extends RuntimeProviderOptions {
|
|
|
93
101
|
connectionParams?: Record<string, ConnectionParamDefinition>;
|
|
94
102
|
securitySchemes?: Record<string, HTTPSecurityScheme>;
|
|
95
103
|
http?: Record<string, HTTPBinding>;
|
|
104
|
+
resolveHTTPSubject?: HTTPSubjectResolver;
|
|
96
105
|
iconSvg?: string;
|
|
97
106
|
operations: Array<OperationDefinition<any, any>>;
|
|
98
107
|
sessionCatalog?: SessionCatalogHandler;
|
|
@@ -149,6 +158,7 @@ export class PluginProvider extends RuntimeProvider {
|
|
|
149
158
|
readonly http: Record<string, HTTPBinding>;
|
|
150
159
|
|
|
151
160
|
private readonly sessionCatalogHandler: SessionCatalogHandler | undefined;
|
|
161
|
+
private readonly httpSubjectResolver: HTTPSubjectResolver | undefined;
|
|
152
162
|
private readonly operations = new Map<string, OperationDefinition<any, any>>();
|
|
153
163
|
|
|
154
164
|
constructor(options: PluginDefinitionOptions) {
|
|
@@ -159,6 +169,7 @@ export class PluginProvider extends RuntimeProvider {
|
|
|
159
169
|
this.connectionParams = normalizeConnectionParams(options.connectionParams);
|
|
160
170
|
this.securitySchemes = normalizeHTTPSecuritySchemes(options.securitySchemes);
|
|
161
171
|
this.http = normalizeHTTPBindings(options.http);
|
|
172
|
+
this.httpSubjectResolver = options.resolveHTTPSubject;
|
|
162
173
|
this.sessionCatalogHandler = options.sessionCatalog;
|
|
163
174
|
|
|
164
175
|
for (const rawEntry of options.operations) {
|
|
@@ -189,6 +200,20 @@ export class PluginProvider extends RuntimeProvider {
|
|
|
189
200
|
return await this.sessionCatalogHandler?.(request);
|
|
190
201
|
}
|
|
191
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Resolves the concrete Gestalt subject for a verified hosted HTTP request,
|
|
205
|
+
* if the plugin opts into subject resolution.
|
|
206
|
+
*/
|
|
207
|
+
async resolveHTTPSubject(
|
|
208
|
+
request: HTTPSubjectRequest,
|
|
209
|
+
context: HTTPSubjectResolutionContext,
|
|
210
|
+
): Promise<Subject | null | undefined> {
|
|
211
|
+
return await this.httpSubjectResolver?.(
|
|
212
|
+
cloneHTTPSubjectRequest(request),
|
|
213
|
+
cloneHTTPSubjectResolutionContext(context),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
192
217
|
/**
|
|
193
218
|
* Returns the static catalog emitted during provider startup.
|
|
194
219
|
*/
|
|
@@ -424,6 +449,21 @@ function cloneHTTPSecurityScheme(value: HTTPSecurityScheme): HTTPSecurityScheme
|
|
|
424
449
|
if (value.description !== undefined) {
|
|
425
450
|
output.description = value.description;
|
|
426
451
|
}
|
|
452
|
+
if (value.signatureHeader !== undefined) {
|
|
453
|
+
output.signatureHeader = value.signatureHeader;
|
|
454
|
+
}
|
|
455
|
+
if (value.signaturePrefix !== undefined) {
|
|
456
|
+
output.signaturePrefix = value.signaturePrefix;
|
|
457
|
+
}
|
|
458
|
+
if (value.payloadTemplate !== undefined) {
|
|
459
|
+
output.payloadTemplate = value.payloadTemplate;
|
|
460
|
+
}
|
|
461
|
+
if (value.timestampHeader !== undefined) {
|
|
462
|
+
output.timestampHeader = value.timestampHeader;
|
|
463
|
+
}
|
|
464
|
+
if (value.maxAgeSeconds !== undefined) {
|
|
465
|
+
output.maxAgeSeconds = value.maxAgeSeconds;
|
|
466
|
+
}
|
|
427
467
|
if (value.name !== undefined) {
|
|
428
468
|
output.name = value.name;
|
|
429
469
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -40,9 +40,12 @@ import {
|
|
|
40
40
|
CatalogSchema as ProtoCatalogSchema,
|
|
41
41
|
ConnectionMode as ProviderConnectionMode,
|
|
42
42
|
GetSessionCatalogResponseSchema,
|
|
43
|
+
ResolveHTTPSubjectResponseSchema,
|
|
43
44
|
OperationResultSchema,
|
|
44
45
|
ProviderMetadataSchema,
|
|
46
|
+
type HTTPSubjectRequest as ProtoHTTPSubjectRequest,
|
|
45
47
|
type RequestContext as ProtoRequestContext,
|
|
48
|
+
type ResolveHTTPSubjectRequest as ProtoResolveHTTPSubjectRequest,
|
|
46
49
|
IntegrationProvider as IntegrationProviderService,
|
|
47
50
|
StartProviderResponseSchema,
|
|
48
51
|
type ExecuteRequest,
|
|
@@ -68,6 +71,11 @@ import {
|
|
|
68
71
|
import { CacheProvider, isCacheProvider } from "./cache.ts";
|
|
69
72
|
import { SecretsProvider, isSecretsProvider } from "./secrets.ts";
|
|
70
73
|
import { catalogToYaml, type Catalog } from "./catalog.ts";
|
|
74
|
+
import {
|
|
75
|
+
HTTPSubjectResolutionError,
|
|
76
|
+
type HTTPSubjectRequest,
|
|
77
|
+
type HTTPSubjectResolutionContext,
|
|
78
|
+
} from "./http-subject.ts";
|
|
71
79
|
import {
|
|
72
80
|
PluginProvider,
|
|
73
81
|
connectionModeToProtoValue,
|
|
@@ -519,6 +527,36 @@ export function createProviderService(
|
|
|
519
527
|
),
|
|
520
528
|
);
|
|
521
529
|
},
|
|
530
|
+
async resolveHTTPSubject(request: ProtoResolveHTTPSubjectRequest) {
|
|
531
|
+
let subject;
|
|
532
|
+
try {
|
|
533
|
+
subject = await provider.resolveHTTPSubject(
|
|
534
|
+
providerHTTPSubjectRequest(request.request),
|
|
535
|
+
providerHTTPSubjectResolutionContext(request.context),
|
|
536
|
+
);
|
|
537
|
+
} catch (error) {
|
|
538
|
+
if (error instanceof HTTPSubjectResolutionError) {
|
|
539
|
+
return create(ResolveHTTPSubjectResponseSchema, {
|
|
540
|
+
rejectStatus: error.status,
|
|
541
|
+
rejectMessage: error.message,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
throw new ConnectError(
|
|
545
|
+
`resolve http subject: ${errorMessage(error)}`,
|
|
546
|
+
Code.Unknown,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
return create(ResolveHTTPSubjectResponseSchema, subject
|
|
550
|
+
? {
|
|
551
|
+
subject: {
|
|
552
|
+
id: subject.id,
|
|
553
|
+
kind: subject.kind,
|
|
554
|
+
displayName: subject.displayName,
|
|
555
|
+
authSource: subject.authSource,
|
|
556
|
+
},
|
|
557
|
+
}
|
|
558
|
+
: {});
|
|
559
|
+
},
|
|
522
560
|
async getSessionCatalog(request: GetSessionCatalogRequest) {
|
|
523
561
|
let catalog: Catalog | Record<string, unknown> | null | undefined;
|
|
524
562
|
try {
|
|
@@ -752,6 +790,48 @@ function providerRequest(
|
|
|
752
790
|
};
|
|
753
791
|
}
|
|
754
792
|
|
|
793
|
+
function providerHTTPSubjectRequest(
|
|
794
|
+
request?: ProtoHTTPSubjectRequest,
|
|
795
|
+
): HTTPSubjectRequest {
|
|
796
|
+
return {
|
|
797
|
+
binding: request?.binding ?? "",
|
|
798
|
+
method: request?.method ?? "",
|
|
799
|
+
path: request?.path ?? "",
|
|
800
|
+
contentType: request?.contentType ?? "",
|
|
801
|
+
headers: providerStringLists(request?.headers),
|
|
802
|
+
query: providerStringLists(request?.query),
|
|
803
|
+
params: objectFromUnknown(request?.params),
|
|
804
|
+
rawBody: new Uint8Array(request?.rawBody ?? new Uint8Array()),
|
|
805
|
+
securityScheme: request?.securityScheme ?? "",
|
|
806
|
+
verifiedSubject: request?.verifiedSubject ?? "",
|
|
807
|
+
verifiedClaims: {
|
|
808
|
+
...(request?.verifiedClaims ?? {}),
|
|
809
|
+
},
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function providerHTTPSubjectResolutionContext(
|
|
814
|
+
requestContext?: ProtoRequestContext,
|
|
815
|
+
): HTTPSubjectResolutionContext {
|
|
816
|
+
const request = providerRequest("", {}, requestContext);
|
|
817
|
+
return {
|
|
818
|
+
subject: request.subject,
|
|
819
|
+
credential: request.credential,
|
|
820
|
+
access: request.access,
|
|
821
|
+
workflow: request.workflow,
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function providerStringLists(
|
|
826
|
+
input: Record<string, { values?: string[] }> | undefined,
|
|
827
|
+
): Record<string, string[]> {
|
|
828
|
+
const output: Record<string, string[]> = {};
|
|
829
|
+
for (const [key, value] of Object.entries(input ?? {})) {
|
|
830
|
+
output[key] = [...(value.values ?? [])];
|
|
831
|
+
}
|
|
832
|
+
return output;
|
|
833
|
+
}
|
|
834
|
+
|
|
755
835
|
function providerRuntimeEntry(
|
|
756
836
|
kind: ProviderKind,
|
|
757
837
|
): ProviderRuntimeEntry {
|
package/src/s3.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { connect } from "node:net";
|
|
2
|
-
|
|
3
1
|
import { create } from "@bufbuild/protobuf";
|
|
4
2
|
import { EmptySchema } from "@bufbuild/protobuf/wkt";
|
|
5
3
|
import {
|
|
@@ -503,9 +501,9 @@ export class S3 {
|
|
|
503
501
|
throw new Error(`${envName} is not set`);
|
|
504
502
|
}
|
|
505
503
|
const transport = createGrpcTransport({
|
|
506
|
-
baseUrl:
|
|
504
|
+
baseUrl: unixSocketBaseUrl(socketPath),
|
|
507
505
|
nodeOptions: {
|
|
508
|
-
|
|
506
|
+
path: socketPath,
|
|
509
507
|
},
|
|
510
508
|
});
|
|
511
509
|
this.client = createClient(S3Service, transport);
|
|
@@ -740,6 +738,15 @@ export class S3Object {
|
|
|
740
738
|
}
|
|
741
739
|
}
|
|
742
740
|
|
|
741
|
+
function unixSocketBaseUrl(socketPath: string): string {
|
|
742
|
+
let hash = 0x811c9dc5;
|
|
743
|
+
for (const char of socketPath) {
|
|
744
|
+
hash ^= char.charCodeAt(0);
|
|
745
|
+
hash = Math.imul(hash, 0x01000193);
|
|
746
|
+
}
|
|
747
|
+
return `http://unix-${(hash >>> 0).toString(16)}.local`;
|
|
748
|
+
}
|
|
749
|
+
|
|
743
750
|
async function invokeS3Provider<T>(label: string, fn: () => Promise<T>): Promise<T> {
|
|
744
751
|
try {
|
|
745
752
|
return await fn();
|