@callsitehq/runtime 0.1.0 → 0.2.0
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/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/aws-lambda.d.ts +9 -0
- package/dist/aws-lambda.js +280 -0
- package/dist/aws-lambda.js.map +1 -0
- package/dist/chunk-BT7K4T26.js +196 -0
- package/dist/chunk-BT7K4T26.js.map +1 -0
- package/dist/express.d.ts +10 -0
- package/dist/express.js +121 -0
- package/dist/express.js.map +1 -0
- package/dist/index.d.ts +30 -3
- package/dist/index.js +7 -87
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +15 -0
- package/dist/mcp.js +158 -0
- package/dist/mcp.js.map +1 -0
- package/dist/node.d.ts +7 -0
- package/dist/node.js +81 -0
- package/dist/node.js.map +1 -0
- package/package.json +54 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Callsite contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @callsitehq/runtime
|
|
2
|
+
|
|
3
|
+
Runtime dispatch engine for Callsite capabilities.
|
|
4
|
+
|
|
5
|
+
Use this package to execute Callsite capabilities through one transport-neutral
|
|
6
|
+
validation and dispatch path. HTTP and other surfaces should adapt into this
|
|
7
|
+
runtime instead of reimplementing validation, error mapping, and handler lookup.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createRuntimeManifest, execute } from "@callsitehq/runtime";
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`createFetchHandler()` is also exported as a thin web-standard
|
|
14
|
+
`Request -> Response` adapter over `execute()`.
|
|
15
|
+
|
|
16
|
+
Node-specific hosting code lives behind a subpath so the default runtime export
|
|
17
|
+
stays fetch-native:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { createNodeHandler } from "@callsitehq/runtime/node";
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Express hosting code is also a shallow adapter over the same fetch handler:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { createFetchHandler } from "@callsitehq/runtime";
|
|
27
|
+
import { createExpressHandler } from "@callsitehq/runtime/express";
|
|
28
|
+
|
|
29
|
+
const callsiteHandler = createFetchHandler(capabilities, {
|
|
30
|
+
context(request) {
|
|
31
|
+
return {
|
|
32
|
+
subject: request.headers.get("x-subject"),
|
|
33
|
+
log(event, data) {
|
|
34
|
+
console.log({ event, data });
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
app.use("/capabilities", createExpressHandler(callsiteHandler));
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
AWS Lambda hosting for API Gateway HTTP API v2 and Lambda Function URLs lives
|
|
44
|
+
behind its own subpath:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { createFetchHandler } from "@callsitehq/runtime";
|
|
48
|
+
import { createLambdaHandler } from "@callsitehq/runtime/aws-lambda";
|
|
49
|
+
|
|
50
|
+
const callsiteHandler = createFetchHandler(capabilities, {
|
|
51
|
+
context(request) {
|
|
52
|
+
return {
|
|
53
|
+
subject: request.headers.get("x-subject"),
|
|
54
|
+
log(event, data) {
|
|
55
|
+
console.log({ event, data });
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export const handler = createLambdaHandler(callsiteHandler);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The Lambda adapter intentionally targets payload format v2 first. API Gateway
|
|
65
|
+
v1, ALB events, and streaming responses are not normalized by this adapter.
|
|
66
|
+
For custom domains with API mappings, configure
|
|
67
|
+
`createFetchHandler(capabilities, { basePath })` to match the Lambda event route
|
|
68
|
+
path; API Gateway v2 `rawPath` does not include the public custom-domain mapping
|
|
69
|
+
prefix.
|
|
70
|
+
|
|
71
|
+
MCP tool registration lives behind `@callsitehq/runtime/mcp`. It registers
|
|
72
|
+
Callsite capabilities on an MCP SDK server; the SDK owns protocol handling and
|
|
73
|
+
transports. Install `@modelcontextprotocol/sdk` alongside this package when you
|
|
74
|
+
use the MCP adapter:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { toJsonSchema } from "@callsitehq/zod";
|
|
78
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
79
|
+
import { registerCallsiteTools } from "@callsitehq/runtime/mcp";
|
|
80
|
+
|
|
81
|
+
import { capabilities } from "./src/app.js";
|
|
82
|
+
|
|
83
|
+
const server = new McpServer({ name: "orders", version: "0.1.0" });
|
|
84
|
+
|
|
85
|
+
registerCallsiteTools(server, capabilities, {
|
|
86
|
+
toJsonSchema,
|
|
87
|
+
context(extra) {
|
|
88
|
+
return {
|
|
89
|
+
subject: extra.authInfo?.clientId,
|
|
90
|
+
log(event, data) {
|
|
91
|
+
console.log({ event, data });
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Connect the SDK server to whatever MCP transport your host uses, such as stdio
|
|
99
|
+
or Streamable HTTP. Callsite does not start a server, mount HTTP, read
|
|
100
|
+
`mcp.json`, or own auth; it maps capabilities onto SDK tools and routes
|
|
101
|
+
`tools/call` through the same runtime validation path. Host-owned SDK tools can
|
|
102
|
+
be registered on the same server before or after Callsite tools.
|
|
103
|
+
|
|
104
|
+
## Status
|
|
105
|
+
|
|
106
|
+
Early `0.x` package. The transport-neutral runtime path is implemented first;
|
|
107
|
+
surface-specific adapters are intentionally thin.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Handler, APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from 'aws-lambda';
|
|
2
|
+
|
|
3
|
+
type FetchHandler = (request: Request) => Promise<Response> | Response;
|
|
4
|
+
type AwsLambdaHttpApiV2Event = APIGatewayProxyEventV2;
|
|
5
|
+
type AwsLambdaHttpApiV2Result = APIGatewayProxyStructuredResultV2;
|
|
6
|
+
type AwsLambdaHandler = Handler<AwsLambdaHttpApiV2Event, AwsLambdaHttpApiV2Result>;
|
|
7
|
+
declare function createLambdaHandler(fetchHandler: FetchHandler): AwsLambdaHandler;
|
|
8
|
+
|
|
9
|
+
export { type AwsLambdaHandler, type AwsLambdaHttpApiV2Event, type AwsLambdaHttpApiV2Result, createLambdaHandler };
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// src/aws-lambda.ts
|
|
2
|
+
function createLambdaHandler(fetchHandler) {
|
|
3
|
+
return async (event) => {
|
|
4
|
+
try {
|
|
5
|
+
const unsupportedReason = unsupportedEventReason(event);
|
|
6
|
+
if (unsupportedReason !== void 0) {
|
|
7
|
+
return unsupportedEvent(unsupportedReason);
|
|
8
|
+
}
|
|
9
|
+
const response = await fetchHandler(requestFromLambdaEvent(event));
|
|
10
|
+
return resultFromResponse(response);
|
|
11
|
+
} catch {
|
|
12
|
+
return internalError();
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function requestFromLambdaEvent(event) {
|
|
17
|
+
const headers = headersFromLambdaEvent(event);
|
|
18
|
+
const method = event.requestContext?.http?.method ?? "GET";
|
|
19
|
+
const init = {
|
|
20
|
+
headers,
|
|
21
|
+
method
|
|
22
|
+
};
|
|
23
|
+
if (method !== "GET" && method !== "HEAD" && event.body !== void 0) {
|
|
24
|
+
init.body = event.isBase64Encoded === true ? arrayBufferFromBytes(Buffer.from(event.body, "base64")) : event.body;
|
|
25
|
+
}
|
|
26
|
+
return new Request(urlFromLambdaEvent(event, headers), init);
|
|
27
|
+
}
|
|
28
|
+
function headersFromLambdaEvent(event) {
|
|
29
|
+
const headers = new Headers();
|
|
30
|
+
for (const [name, value] of Object.entries(event.headers ?? {})) {
|
|
31
|
+
if (value !== void 0) {
|
|
32
|
+
headers.set(name, value);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (event.cookies !== void 0 && event.cookies.length > 0) {
|
|
36
|
+
const existingCookie = headers.get("cookie");
|
|
37
|
+
const eventCookie = event.cookies.join("; ");
|
|
38
|
+
headers.set(
|
|
39
|
+
"cookie",
|
|
40
|
+
existingCookie === null ? eventCookie : `${existingCookie}; ${eventCookie}`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return headers;
|
|
44
|
+
}
|
|
45
|
+
function urlFromLambdaEvent(event, headers) {
|
|
46
|
+
const protocol = headers.get("x-forwarded-proto") ?? "https";
|
|
47
|
+
const host = headers.get("host") ?? event.requestContext?.domainName ?? "localhost";
|
|
48
|
+
const path = pathWithLeadingSlash(event.rawPath ?? event.requestContext?.http?.path ?? "/");
|
|
49
|
+
const query = event.rawQueryString === void 0 || event.rawQueryString.length === 0 ? "" : `?${event.rawQueryString}`;
|
|
50
|
+
return new URL(`${path}${query}`, `${protocol}://${host}`).toString();
|
|
51
|
+
}
|
|
52
|
+
function pathWithLeadingSlash(path) {
|
|
53
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
54
|
+
}
|
|
55
|
+
async function resultFromResponse(response) {
|
|
56
|
+
const headers = headersFromResponse(response.headers);
|
|
57
|
+
const cookies = getSetCookie(response.headers);
|
|
58
|
+
const body = await bodyFromResponse(response);
|
|
59
|
+
return {
|
|
60
|
+
statusCode: response.status,
|
|
61
|
+
...headers === void 0 ? {} : { headers },
|
|
62
|
+
...cookies.length === 0 ? {} : { cookies: [...cookies] },
|
|
63
|
+
...body === void 0 ? {} : body
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function headersFromResponse(headers) {
|
|
67
|
+
const result = {};
|
|
68
|
+
const setCookie = getSetCookie(headers);
|
|
69
|
+
headers.forEach((value, name) => {
|
|
70
|
+
if (name.toLowerCase() === "set-cookie" && setCookie.length > 0) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
result[name] = value;
|
|
74
|
+
});
|
|
75
|
+
return Object.keys(result).length === 0 ? void 0 : result;
|
|
76
|
+
}
|
|
77
|
+
async function bodyFromResponse(response) {
|
|
78
|
+
if (response.body === null) {
|
|
79
|
+
return void 0;
|
|
80
|
+
}
|
|
81
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
82
|
+
if (isTextResponse(response.headers)) {
|
|
83
|
+
return {
|
|
84
|
+
body: bytes.toString("utf8"),
|
|
85
|
+
isBase64Encoded: false
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
body: bytes.toString("base64"),
|
|
90
|
+
isBase64Encoded: true
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function isTextResponse(headers) {
|
|
94
|
+
if (headers.has("content-encoding")) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
const contentType = headers.get("content-type")?.toLowerCase();
|
|
98
|
+
if (contentType === void 0) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return contentType.startsWith("text/") || contentType.includes("json") || contentType.includes("xml") || contentType.includes("javascript") || contentType.includes("x-www-form-urlencoded");
|
|
102
|
+
}
|
|
103
|
+
function getSetCookie(headers) {
|
|
104
|
+
const candidate = headers;
|
|
105
|
+
return candidate.getSetCookie?.() ?? [];
|
|
106
|
+
}
|
|
107
|
+
function arrayBufferFromBytes(bytes) {
|
|
108
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
109
|
+
}
|
|
110
|
+
function unsupportedEventReason(event) {
|
|
111
|
+
if (!isRecord(event)) {
|
|
112
|
+
return "Expected an event object.";
|
|
113
|
+
}
|
|
114
|
+
if (event.version !== "2.0") {
|
|
115
|
+
return 'Expected payload format version "2.0".';
|
|
116
|
+
}
|
|
117
|
+
if (!isRecord(event.headers)) {
|
|
118
|
+
return "Expected headers object.";
|
|
119
|
+
}
|
|
120
|
+
if (!recordValuesAreStrings(event.headers)) {
|
|
121
|
+
return "Expected headers to contain only string values.";
|
|
122
|
+
}
|
|
123
|
+
if (!recordKeysAreHeaderNames(event.headers)) {
|
|
124
|
+
return "Expected headers to contain only valid HTTP header names.";
|
|
125
|
+
}
|
|
126
|
+
if (!recordValuesAreHeaderValues(event.headers)) {
|
|
127
|
+
return "Expected headers to contain only valid HTTP header values.";
|
|
128
|
+
}
|
|
129
|
+
const forwardedProto = headerValue(event.headers, "x-forwarded-proto");
|
|
130
|
+
if (forwardedProto !== void 0 && forwardedProto !== "http" && forwardedProto !== "https") {
|
|
131
|
+
return 'Expected x-forwarded-proto to be "http" or "https".';
|
|
132
|
+
}
|
|
133
|
+
if (event.cookies !== void 0 && !isStringArray(event.cookies)) {
|
|
134
|
+
return "Expected cookies to be an array of strings.";
|
|
135
|
+
}
|
|
136
|
+
if (event.cookies !== void 0 && !event.cookies.every(isHeaderValue)) {
|
|
137
|
+
return "Expected cookies to contain only valid HTTP header values.";
|
|
138
|
+
}
|
|
139
|
+
if (event.rawQueryString !== void 0 && typeof event.rawQueryString !== "string") {
|
|
140
|
+
return "Expected rawQueryString to be a string.";
|
|
141
|
+
}
|
|
142
|
+
if (event.rawPath !== void 0 && !isNonEmptyString(event.rawPath)) {
|
|
143
|
+
return "Expected rawPath to be a non-empty string when provided.";
|
|
144
|
+
}
|
|
145
|
+
if (event.rawPath !== void 0 && !isValidRequestPath(event.rawPath)) {
|
|
146
|
+
return "Expected rawPath to be a valid request path when provided.";
|
|
147
|
+
}
|
|
148
|
+
if (event.body !== void 0 && typeof event.body !== "string") {
|
|
149
|
+
return "Expected body to be a string when provided.";
|
|
150
|
+
}
|
|
151
|
+
if (event.isBase64Encoded !== void 0 && typeof event.isBase64Encoded !== "boolean") {
|
|
152
|
+
return "Expected isBase64Encoded to be a boolean.";
|
|
153
|
+
}
|
|
154
|
+
if (!isRecord(event.requestContext) || !isRecord(event.requestContext.http)) {
|
|
155
|
+
return "Expected requestContext.http.";
|
|
156
|
+
}
|
|
157
|
+
if (event.requestContext.domainName !== void 0 && typeof event.requestContext.domainName !== "string") {
|
|
158
|
+
return "Expected requestContext.domainName to be a string when provided.";
|
|
159
|
+
}
|
|
160
|
+
const host = headerValue(event.headers, "host") ?? event.requestContext.domainName;
|
|
161
|
+
if (host !== void 0 && !isValidUrlHost(host)) {
|
|
162
|
+
return "Expected host or requestContext.domainName to be a valid URL host.";
|
|
163
|
+
}
|
|
164
|
+
if (!isNonEmptyString(event.requestContext.http.method)) {
|
|
165
|
+
return "Expected requestContext.http.method.";
|
|
166
|
+
}
|
|
167
|
+
if (!isFetchMethod(event.requestContext.http.method)) {
|
|
168
|
+
return "Expected requestContext.http.method to be a valid Fetch method.";
|
|
169
|
+
}
|
|
170
|
+
if (event.requestContext.http.path !== void 0 && typeof event.requestContext.http.path !== "string") {
|
|
171
|
+
return "Expected requestContext.http.path to be a string when provided.";
|
|
172
|
+
}
|
|
173
|
+
if (event.rawPath === void 0) {
|
|
174
|
+
const fallbackPath = event.requestContext.http.path;
|
|
175
|
+
if (!isNonEmptyString(fallbackPath)) {
|
|
176
|
+
return "Expected rawPath or requestContext.http.path.";
|
|
177
|
+
}
|
|
178
|
+
if (!isValidRequestPath(fallbackPath)) {
|
|
179
|
+
return "Expected requestContext.http.path to be a valid request path when provided.";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return void 0;
|
|
183
|
+
}
|
|
184
|
+
function unsupportedEvent(reason) {
|
|
185
|
+
return {
|
|
186
|
+
statusCode: 500,
|
|
187
|
+
headers: {
|
|
188
|
+
"content-type": "application/json"
|
|
189
|
+
},
|
|
190
|
+
body: JSON.stringify({
|
|
191
|
+
error: {
|
|
192
|
+
code: "internal",
|
|
193
|
+
message: "Unsupported Lambda event payload.",
|
|
194
|
+
details: {
|
|
195
|
+
reason
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}),
|
|
199
|
+
isBase64Encoded: false
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function isRecord(value) {
|
|
203
|
+
return typeof value === "object" && value !== null;
|
|
204
|
+
}
|
|
205
|
+
function recordValuesAreStrings(value) {
|
|
206
|
+
return Object.values(value).every((item) => typeof item === "string");
|
|
207
|
+
}
|
|
208
|
+
function recordKeysAreHeaderNames(value) {
|
|
209
|
+
return Object.keys(value).every(isHeaderName);
|
|
210
|
+
}
|
|
211
|
+
function recordValuesAreHeaderValues(value) {
|
|
212
|
+
return Object.values(value).every((item) => typeof item === "string" && isHeaderValue(item));
|
|
213
|
+
}
|
|
214
|
+
function isHeaderName(value) {
|
|
215
|
+
return /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(value);
|
|
216
|
+
}
|
|
217
|
+
function isHeaderValue(value) {
|
|
218
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
219
|
+
const code = value.charCodeAt(index);
|
|
220
|
+
if (code === 127 || code <= 8 || code >= 10 && code <= 31) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
function isFetchMethod(value) {
|
|
227
|
+
return isHeaderName(value) && !["CONNECT", "TRACE", "TRACK"].includes(value.toUpperCase());
|
|
228
|
+
}
|
|
229
|
+
function headerValue(headers, name) {
|
|
230
|
+
const match = Object.entries(headers).find(([candidate]) => candidate.toLowerCase() === name);
|
|
231
|
+
return match === void 0 ? void 0 : match[1];
|
|
232
|
+
}
|
|
233
|
+
function isValidUrlHost(host) {
|
|
234
|
+
try {
|
|
235
|
+
return new URL(`https://${host}`).host === host.toLowerCase();
|
|
236
|
+
} catch {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function isValidRequestPath(path) {
|
|
241
|
+
const normalizedPath = pathWithLeadingSlash(path);
|
|
242
|
+
return !hasControlCharacter(path) && !normalizedPath.startsWith("//") && !isAbsoluteUrlReference(path);
|
|
243
|
+
}
|
|
244
|
+
function hasControlCharacter(value) {
|
|
245
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
246
|
+
const code = value.charCodeAt(index);
|
|
247
|
+
if (code <= 31 || code === 127) {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
function isAbsoluteUrlReference(value) {
|
|
254
|
+
return /^[A-Za-z][A-Za-z0-9+.-]*:/.test(value);
|
|
255
|
+
}
|
|
256
|
+
function isStringArray(value) {
|
|
257
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
258
|
+
}
|
|
259
|
+
function isNonEmptyString(value) {
|
|
260
|
+
return typeof value === "string" && value.length > 0;
|
|
261
|
+
}
|
|
262
|
+
function internalError() {
|
|
263
|
+
return {
|
|
264
|
+
statusCode: 500,
|
|
265
|
+
headers: {
|
|
266
|
+
"content-type": "application/json"
|
|
267
|
+
},
|
|
268
|
+
body: JSON.stringify({
|
|
269
|
+
error: {
|
|
270
|
+
code: "internal",
|
|
271
|
+
message: "Internal server error."
|
|
272
|
+
}
|
|
273
|
+
}),
|
|
274
|
+
isBase64Encoded: false
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
export {
|
|
278
|
+
createLambdaHandler
|
|
279
|
+
};
|
|
280
|
+
//# sourceMappingURL=aws-lambda.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/aws-lambda.ts"],"sourcesContent":["import type {\n APIGatewayProxyEventV2,\n APIGatewayProxyStructuredResultV2,\n Handler\n} from \"aws-lambda\";\n\ntype FetchHandler = (request: Request) => Promise<Response> | Response;\n\nexport type AwsLambdaHttpApiV2Event = APIGatewayProxyEventV2;\nexport type AwsLambdaHttpApiV2Result = APIGatewayProxyStructuredResultV2;\nexport type AwsLambdaHandler = Handler<AwsLambdaHttpApiV2Event, AwsLambdaHttpApiV2Result>;\n\nexport function createLambdaHandler(fetchHandler: FetchHandler): AwsLambdaHandler {\n return async (event) => {\n try {\n const unsupportedReason = unsupportedEventReason(event);\n if (unsupportedReason !== undefined) {\n return unsupportedEvent(unsupportedReason);\n }\n\n const response = await fetchHandler(requestFromLambdaEvent(event));\n return resultFromResponse(response);\n } catch {\n return internalError();\n }\n };\n}\n\nfunction requestFromLambdaEvent(event: AwsLambdaHttpApiV2Event): Request {\n const headers = headersFromLambdaEvent(event);\n const method = event.requestContext?.http?.method ?? \"GET\";\n const init: RequestInit = {\n headers,\n method\n };\n\n if (method !== \"GET\" && method !== \"HEAD\" && event.body !== undefined) {\n init.body =\n event.isBase64Encoded === true\n ? arrayBufferFromBytes(Buffer.from(event.body, \"base64\"))\n : event.body;\n }\n\n return new Request(urlFromLambdaEvent(event, headers), init);\n}\n\nfunction headersFromLambdaEvent(event: AwsLambdaHttpApiV2Event): Headers {\n const headers = new Headers();\n\n for (const [name, value] of Object.entries(event.headers ?? {})) {\n if (value !== undefined) {\n headers.set(name, value);\n }\n }\n\n if (event.cookies !== undefined && event.cookies.length > 0) {\n const existingCookie = headers.get(\"cookie\");\n const eventCookie = event.cookies.join(\"; \");\n headers.set(\n \"cookie\",\n existingCookie === null ? eventCookie : `${existingCookie}; ${eventCookie}`\n );\n }\n\n return headers;\n}\n\nfunction urlFromLambdaEvent(event: AwsLambdaHttpApiV2Event, headers: Headers): string {\n const protocol = headers.get(\"x-forwarded-proto\") ?? \"https\";\n const host = headers.get(\"host\") ?? event.requestContext?.domainName ?? \"localhost\";\n const path = pathWithLeadingSlash(event.rawPath ?? event.requestContext?.http?.path ?? \"/\");\n const query =\n event.rawQueryString === undefined || event.rawQueryString.length === 0\n ? \"\"\n : `?${event.rawQueryString}`;\n\n return new URL(`${path}${query}`, `${protocol}://${host}`).toString();\n}\n\nfunction pathWithLeadingSlash(path: string): string {\n return path.startsWith(\"/\") ? path : `/${path}`;\n}\n\nasync function resultFromResponse(response: Response): Promise<AwsLambdaHttpApiV2Result> {\n const headers = headersFromResponse(response.headers);\n const cookies = getSetCookie(response.headers);\n const body = await bodyFromResponse(response);\n\n return {\n statusCode: response.status,\n ...(headers === undefined ? {} : { headers }),\n ...(cookies.length === 0 ? {} : { cookies: [...cookies] }),\n ...(body === undefined ? {} : body)\n };\n}\n\nfunction headersFromResponse(headers: Headers): Readonly<Record<string, string>> | undefined {\n const result: Record<string, string> = {};\n const setCookie = getSetCookie(headers);\n\n headers.forEach((value, name) => {\n if (name.toLowerCase() === \"set-cookie\" && setCookie.length > 0) {\n return;\n }\n\n result[name] = value;\n });\n\n return Object.keys(result).length === 0 ? undefined : result;\n}\n\nasync function bodyFromResponse(\n response: Response\n): Promise<Pick<AwsLambdaHttpApiV2Result, \"body\" | \"isBase64Encoded\"> | undefined> {\n if (response.body === null) {\n return undefined;\n }\n\n const bytes = Buffer.from(await response.arrayBuffer());\n if (isTextResponse(response.headers)) {\n return {\n body: bytes.toString(\"utf8\"),\n isBase64Encoded: false\n };\n }\n\n return {\n body: bytes.toString(\"base64\"),\n isBase64Encoded: true\n };\n}\n\nfunction isTextResponse(headers: Headers): boolean {\n if (headers.has(\"content-encoding\")) {\n return false;\n }\n\n const contentType = headers.get(\"content-type\")?.toLowerCase();\n if (contentType === undefined) {\n return false;\n }\n\n return (\n contentType.startsWith(\"text/\") ||\n contentType.includes(\"json\") ||\n contentType.includes(\"xml\") ||\n contentType.includes(\"javascript\") ||\n contentType.includes(\"x-www-form-urlencoded\")\n );\n}\n\nfunction getSetCookie(headers: Headers): readonly string[] {\n const candidate = headers as Headers & {\n getSetCookie?: () => string[];\n };\n\n return candidate.getSetCookie?.() ?? [];\n}\n\nfunction arrayBufferFromBytes(bytes: Uint8Array): ArrayBuffer {\n return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;\n}\n\nfunction unsupportedEventReason(event: unknown): string | undefined {\n if (!isRecord(event)) {\n return \"Expected an event object.\";\n }\n\n if (event.version !== \"2.0\") {\n return 'Expected payload format version \"2.0\".';\n }\n\n if (!isRecord(event.headers)) {\n return \"Expected headers object.\";\n }\n\n if (!recordValuesAreStrings(event.headers)) {\n return \"Expected headers to contain only string values.\";\n }\n\n if (!recordKeysAreHeaderNames(event.headers)) {\n return \"Expected headers to contain only valid HTTP header names.\";\n }\n\n if (!recordValuesAreHeaderValues(event.headers)) {\n return \"Expected headers to contain only valid HTTP header values.\";\n }\n\n const forwardedProto = headerValue(event.headers, \"x-forwarded-proto\");\n if (forwardedProto !== undefined && forwardedProto !== \"http\" && forwardedProto !== \"https\") {\n return 'Expected x-forwarded-proto to be \"http\" or \"https\".';\n }\n\n if (event.cookies !== undefined && !isStringArray(event.cookies)) {\n return \"Expected cookies to be an array of strings.\";\n }\n\n if (event.cookies !== undefined && !event.cookies.every(isHeaderValue)) {\n return \"Expected cookies to contain only valid HTTP header values.\";\n }\n\n if (event.rawQueryString !== undefined && typeof event.rawQueryString !== \"string\") {\n return \"Expected rawQueryString to be a string.\";\n }\n\n if (event.rawPath !== undefined && !isNonEmptyString(event.rawPath)) {\n return \"Expected rawPath to be a non-empty string when provided.\";\n }\n\n if (event.rawPath !== undefined && !isValidRequestPath(event.rawPath)) {\n return \"Expected rawPath to be a valid request path when provided.\";\n }\n\n if (event.body !== undefined && typeof event.body !== \"string\") {\n return \"Expected body to be a string when provided.\";\n }\n\n if (event.isBase64Encoded !== undefined && typeof event.isBase64Encoded !== \"boolean\") {\n return \"Expected isBase64Encoded to be a boolean.\";\n }\n\n if (!isRecord(event.requestContext) || !isRecord(event.requestContext.http)) {\n return \"Expected requestContext.http.\";\n }\n\n if (\n event.requestContext.domainName !== undefined &&\n typeof event.requestContext.domainName !== \"string\"\n ) {\n return \"Expected requestContext.domainName to be a string when provided.\";\n }\n\n const host = headerValue(event.headers, \"host\") ?? event.requestContext.domainName;\n if (host !== undefined && !isValidUrlHost(host)) {\n return \"Expected host or requestContext.domainName to be a valid URL host.\";\n }\n\n if (!isNonEmptyString(event.requestContext.http.method)) {\n return \"Expected requestContext.http.method.\";\n }\n\n if (!isFetchMethod(event.requestContext.http.method)) {\n return \"Expected requestContext.http.method to be a valid Fetch method.\";\n }\n\n if (\n event.requestContext.http.path !== undefined &&\n typeof event.requestContext.http.path !== \"string\"\n ) {\n return \"Expected requestContext.http.path to be a string when provided.\";\n }\n\n if (event.rawPath === undefined) {\n const fallbackPath = event.requestContext.http.path;\n\n if (!isNonEmptyString(fallbackPath)) {\n return \"Expected rawPath or requestContext.http.path.\";\n }\n\n if (!isValidRequestPath(fallbackPath)) {\n return \"Expected requestContext.http.path to be a valid request path when provided.\";\n }\n }\n\n return undefined;\n}\n\nfunction unsupportedEvent(reason: string): AwsLambdaHttpApiV2Result {\n return {\n statusCode: 500,\n headers: {\n \"content-type\": \"application/json\"\n },\n body: JSON.stringify({\n error: {\n code: \"internal\",\n message: \"Unsupported Lambda event payload.\",\n details: {\n reason\n }\n }\n }),\n isBase64Encoded: false\n };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction recordValuesAreStrings(value: Record<string, unknown>): boolean {\n return Object.values(value).every((item) => typeof item === \"string\");\n}\n\nfunction recordKeysAreHeaderNames(value: Record<string, unknown>): boolean {\n return Object.keys(value).every(isHeaderName);\n}\n\nfunction recordValuesAreHeaderValues(value: Record<string, unknown>): boolean {\n return Object.values(value).every((item) => typeof item === \"string\" && isHeaderValue(item));\n}\n\nfunction isHeaderName(value: string): boolean {\n return /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(value);\n}\n\nfunction isHeaderValue(value: string): boolean {\n for (let index = 0; index < value.length; index += 1) {\n const code = value.charCodeAt(index);\n\n if (code === 0x7f || code <= 0x08 || (code >= 0x0a && code <= 0x1f)) {\n return false;\n }\n }\n\n return true;\n}\n\nfunction isFetchMethod(value: string): boolean {\n return isHeaderName(value) && ![\"CONNECT\", \"TRACE\", \"TRACK\"].includes(value.toUpperCase());\n}\n\nfunction headerValue(headers: Record<string, unknown>, name: string): string | undefined {\n const match = Object.entries(headers).find(([candidate]) => candidate.toLowerCase() === name);\n\n return match === undefined ? undefined : (match[1] as string);\n}\n\nfunction isValidUrlHost(host: string): boolean {\n try {\n return new URL(`https://${host}`).host === host.toLowerCase();\n } catch {\n return false;\n }\n}\n\nfunction isValidRequestPath(path: string): boolean {\n const normalizedPath = pathWithLeadingSlash(path);\n\n return (\n !hasControlCharacter(path) && !normalizedPath.startsWith(\"//\") && !isAbsoluteUrlReference(path)\n );\n}\n\nfunction hasControlCharacter(value: string): boolean {\n for (let index = 0; index < value.length; index += 1) {\n const code = value.charCodeAt(index);\n\n if (code <= 0x1f || code === 0x7f) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction isAbsoluteUrlReference(value: string): boolean {\n return /^[A-Za-z][A-Za-z0-9+.-]*:/.test(value);\n}\n\nfunction isStringArray(value: unknown): value is string[] {\n return Array.isArray(value) && value.every((item) => typeof item === \"string\");\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value.length > 0;\n}\n\nfunction internalError(): AwsLambdaHttpApiV2Result {\n return {\n statusCode: 500,\n headers: {\n \"content-type\": \"application/json\"\n },\n body: JSON.stringify({\n error: {\n code: \"internal\",\n message: \"Internal server error.\"\n }\n }),\n isBase64Encoded: false\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
CapabilityError
|
|
4
|
+
} from "@callsitehq/core";
|
|
5
|
+
function createRuntimeManifest(capabilities) {
|
|
6
|
+
return {
|
|
7
|
+
capabilities: Object.assign(
|
|
8
|
+
/* @__PURE__ */ Object.create(null),
|
|
9
|
+
Object.fromEntries(capabilities.map((capability) => [capability.id, capability]))
|
|
10
|
+
)
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
async function execute(manifest, request, context = {}) {
|
|
14
|
+
const capability = capabilityFromManifest(manifest, request.capabilityId);
|
|
15
|
+
if (capability === void 0) {
|
|
16
|
+
return failure("not_found", `Capability "${request.capabilityId}" not found.`);
|
|
17
|
+
}
|
|
18
|
+
const inputResult = await validate(capability.input, request.input);
|
|
19
|
+
if (!inputResult.ok) {
|
|
20
|
+
return failure("invalid_input", "Invalid input.", validationDetails(inputResult.issues));
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const runResult = await capability.run(inputResult.value, capabilityContext(context));
|
|
24
|
+
const outputResult = await validate(capability.output, runResult);
|
|
25
|
+
if (!outputResult.ok) {
|
|
26
|
+
return failure(
|
|
27
|
+
"internal",
|
|
28
|
+
"Capability returned invalid output.",
|
|
29
|
+
validationDetails(outputResult.issues)
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
ok: true,
|
|
34
|
+
value: outputResult.value
|
|
35
|
+
};
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return failureFromThrown(error);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function capabilityFromManifest(manifest, capabilityId) {
|
|
41
|
+
if (!Object.hasOwn(manifest.capabilities, capabilityId)) {
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
return manifest.capabilities[capabilityId];
|
|
45
|
+
}
|
|
46
|
+
function createFetchHandler(manifestOrCapabilities, options = {}) {
|
|
47
|
+
const basePath = normalizeBasePath(options.basePath ?? "/capabilities");
|
|
48
|
+
const manifest = isRuntimeManifest(manifestOrCapabilities) ? manifestOrCapabilities : createRuntimeManifest(manifestOrCapabilities);
|
|
49
|
+
return async function fetchHandler(request) {
|
|
50
|
+
if (request.method !== "POST") {
|
|
51
|
+
return json({ error: runtimeError("invalid_input", "Use POST.") }, 405);
|
|
52
|
+
}
|
|
53
|
+
const capabilityId = capabilityIdFromRequest(request, basePath);
|
|
54
|
+
if (capabilityId === void 0) {
|
|
55
|
+
return json({ error: runtimeError("not_found", "Capability route not found.") }, 404);
|
|
56
|
+
}
|
|
57
|
+
const input = await requestJson(request);
|
|
58
|
+
if (!input.ok) {
|
|
59
|
+
return json({ error: input.error }, statusForErrorCode(input.error.code));
|
|
60
|
+
}
|
|
61
|
+
const result = await execute(
|
|
62
|
+
manifest,
|
|
63
|
+
{ capabilityId, input: input.value },
|
|
64
|
+
await contextFor(request, options.context)
|
|
65
|
+
);
|
|
66
|
+
if (!result.ok) {
|
|
67
|
+
return json({ error: result.error }, statusForErrorCode(result.error.code));
|
|
68
|
+
}
|
|
69
|
+
return json(result.value, 200);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function isRuntimeManifest(value) {
|
|
73
|
+
return !Array.isArray(value) && "capabilities" in value;
|
|
74
|
+
}
|
|
75
|
+
async function validate(schema, value) {
|
|
76
|
+
const result = await schema["~standard"].validate(value);
|
|
77
|
+
if (result.issues !== void 0) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
issues: result.issues
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
value: result.value
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function capabilityContext(context) {
|
|
89
|
+
return {
|
|
90
|
+
...context.subject === void 0 ? {} : { subject: context.subject },
|
|
91
|
+
log: context.log ?? noopLog
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function failure(code, message, details) {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
error: runtimeError(code, message, details)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function failureFromThrown(error) {
|
|
101
|
+
if (error instanceof CapabilityError) {
|
|
102
|
+
return failure(error.code, error.message, error.details);
|
|
103
|
+
}
|
|
104
|
+
return failure("internal", "Internal capability error.");
|
|
105
|
+
}
|
|
106
|
+
function runtimeError(code, message, details) {
|
|
107
|
+
return {
|
|
108
|
+
code,
|
|
109
|
+
message,
|
|
110
|
+
...details === void 0 ? {} : { details }
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function validationDetails(issues) {
|
|
114
|
+
return {
|
|
115
|
+
issues: issues.map((issue) => ({
|
|
116
|
+
message: issue.message,
|
|
117
|
+
...issue.path === void 0 ? {} : { path: issue.path.map(pathSegmentToJson) }
|
|
118
|
+
}))
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function pathSegmentToJson(segment) {
|
|
122
|
+
const key = typeof segment === "object" && segment !== null && "key" in segment ? segment.key : segment;
|
|
123
|
+
return typeof key === "number" ? key : String(key);
|
|
124
|
+
}
|
|
125
|
+
function capabilityIdFromRequest(request, basePath) {
|
|
126
|
+
const { pathname } = new URL(request.url);
|
|
127
|
+
if (!pathname.startsWith(`${basePath}/`)) {
|
|
128
|
+
return void 0;
|
|
129
|
+
}
|
|
130
|
+
const id = pathname.slice(basePath.length + 1);
|
|
131
|
+
if (id.length === 0) {
|
|
132
|
+
return void 0;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
return decodeURIComponent(id);
|
|
136
|
+
} catch {
|
|
137
|
+
return void 0;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function requestJson(request) {
|
|
141
|
+
try {
|
|
142
|
+
return {
|
|
143
|
+
ok: true,
|
|
144
|
+
value: await request.json()
|
|
145
|
+
};
|
|
146
|
+
} catch {
|
|
147
|
+
return failure("invalid_input", "Request body must be valid JSON.");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function contextFor(request, provider) {
|
|
151
|
+
if (provider === void 0) {
|
|
152
|
+
return {};
|
|
153
|
+
}
|
|
154
|
+
return typeof provider === "function" ? provider(request) : provider;
|
|
155
|
+
}
|
|
156
|
+
function statusForErrorCode(code) {
|
|
157
|
+
switch (code) {
|
|
158
|
+
case "invalid_input":
|
|
159
|
+
return 400;
|
|
160
|
+
case "unauthorized":
|
|
161
|
+
return 401;
|
|
162
|
+
case "forbidden":
|
|
163
|
+
return 403;
|
|
164
|
+
case "not_found":
|
|
165
|
+
return 404;
|
|
166
|
+
case "conflict":
|
|
167
|
+
return 409;
|
|
168
|
+
case "rate_limited":
|
|
169
|
+
return 429;
|
|
170
|
+
case "unavailable":
|
|
171
|
+
return 503;
|
|
172
|
+
case "internal":
|
|
173
|
+
return 500;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function normalizeBasePath(basePath) {
|
|
177
|
+
const withLeadingSlash = basePath.startsWith("/") ? basePath : `/${basePath}`;
|
|
178
|
+
return withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
|
|
179
|
+
}
|
|
180
|
+
function json(body, status) {
|
|
181
|
+
return new Response(JSON.stringify(body), {
|
|
182
|
+
headers: {
|
|
183
|
+
"content-type": "application/json"
|
|
184
|
+
},
|
|
185
|
+
status
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function noopLog() {
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export {
|
|
192
|
+
createRuntimeManifest,
|
|
193
|
+
execute,
|
|
194
|
+
createFetchHandler
|
|
195
|
+
};
|
|
196
|
+
//# sourceMappingURL=chunk-BT7K4T26.js.map
|