@every-app/sdk 0.3.0-rc.1 → 0.3.0-rc.10
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 +1 -1
- package/dist/cloudflare/catalogSettings.generated.d.ts +81 -0
- package/dist/cloudflare/catalogSettings.generated.d.ts.map +1 -0
- package/dist/cloudflare/catalogSettings.generated.js +174 -0
- package/dist/cloudflare/index.d.ts +3 -2
- package/dist/cloudflare/index.d.ts.map +1 -1
- package/dist/cloudflare/index.js +4 -1
- package/dist/cloudflare/sentinel.d.ts +60 -2
- package/dist/cloudflare/sentinel.d.ts.map +1 -1
- package/dist/cloudflare/sentinel.js +37 -2
- package/dist/cloudflare/worker.d.ts +357 -63
- package/dist/cloudflare/worker.d.ts.map +1 -1
- package/dist/cloudflare/worker.js +363 -104
- package/dist/dev/index.d.ts +1 -1
- package/dist/dev/index.d.ts.map +1 -1
- package/dist/dev/index.js +1 -1
- package/dist/dev/keypair.d.ts +2 -0
- package/dist/dev/keypair.d.ts.map +1 -1
- package/dist/dev/keypair.js +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/internal/aiDefault.d.ts +204 -0
- package/dist/internal/aiDefault.d.ts.map +1 -0
- package/dist/internal/aiDefault.js +334 -0
- package/dist/internal/aiProviders.d.ts +7 -2
- package/dist/internal/aiProviders.d.ts.map +1 -1
- package/dist/internal/aiProviders.js +17 -4
- package/dist/internal/appSettings.d.ts +82 -4
- package/dist/internal/appSettings.d.ts.map +1 -1
- package/dist/internal/appSettings.js +204 -29
- package/dist/internal/audit.d.ts +108 -0
- package/dist/internal/audit.d.ts.map +1 -0
- package/dist/internal/audit.js +235 -0
- package/dist/internal/capabilities.d.ts +2268 -438
- package/dist/internal/capabilities.d.ts.map +1 -1
- package/dist/internal/capabilities.js +128 -539
- package/dist/internal/catalog/commerce.d.ts +394 -0
- package/dist/internal/catalog/commerce.d.ts.map +1 -0
- package/dist/internal/catalog/commerce.js +477 -0
- package/dist/internal/catalog/comms.d.ts +375 -0
- package/dist/internal/catalog/comms.d.ts.map +1 -0
- package/dist/internal/catalog/comms.js +520 -0
- package/dist/internal/catalog/index.d.ts +1973 -0
- package/dist/internal/catalog/index.d.ts.map +1 -0
- package/dist/internal/catalog/index.js +19 -0
- package/dist/internal/catalog/inference.d.ts +393 -0
- package/dist/internal/catalog/inference.d.ts.map +1 -0
- package/dist/internal/catalog/inference.js +460 -0
- package/dist/internal/catalog/infra.d.ts +423 -0
- package/dist/internal/catalog/infra.d.ts.map +1 -0
- package/dist/internal/catalog/infra.js +543 -0
- package/dist/internal/catalog/search.d.ts +400 -0
- package/dist/internal/catalog/search.d.ts.map +1 -0
- package/dist/internal/catalog/search.js +463 -0
- package/dist/internal/devProviders.d.ts +2 -2
- package/dist/internal/devProviders.d.ts.map +1 -1
- package/dist/internal/devProviders.js +19 -1
- package/dist/internal/events.d.ts +13 -2
- package/dist/internal/events.d.ts.map +1 -1
- package/dist/internal/events.js +47 -4
- package/dist/internal/gatewaySettings.d.ts +3 -2
- package/dist/internal/gatewaySettings.d.ts.map +1 -1
- package/dist/internal/gatewaySettings.js +12 -6
- package/dist/internal/identity.d.ts +41 -4
- package/dist/internal/identity.d.ts.map +1 -1
- package/dist/internal/identity.js +48 -10
- package/dist/internal/index.d.ts +6 -1
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +6 -1
- package/dist/internal/members.d.ts +3 -2
- package/dist/internal/members.d.ts.map +1 -1
- package/dist/internal/oauthClient.d.ts +10 -12
- package/dist/internal/oauthClient.d.ts.map +1 -1
- package/dist/internal/oauthClient.js +20 -81
- package/dist/internal/oauthPresets.d.ts +33 -0
- package/dist/internal/oauthPresets.d.ts.map +1 -0
- package/dist/internal/oauthPresets.js +1064 -0
- package/dist/internal/oauthScopes.d.ts +56 -1
- package/dist/internal/oauthScopes.d.ts.map +1 -1
- package/dist/internal/oauthScopes.js +98 -4
- package/dist/internal/oauthSpec.d.ts +102 -0
- package/dist/internal/oauthSpec.d.ts.map +1 -0
- package/dist/internal/oauthSpec.js +789 -0
- package/dist/internal/protocol.d.ts +64 -1
- package/dist/internal/protocol.d.ts.map +1 -1
- package/dist/internal/protocol.js +51 -1
- package/dist/internal/providerRegistry.d.ts +77 -6
- package/dist/internal/providerRegistry.d.ts.map +1 -1
- package/dist/internal/providerRegistry.js +178 -8
- package/dist/internal/providerSpec.d.ts +18 -7
- package/dist/internal/providerSpec.d.ts.map +1 -1
- package/dist/internal/providerSpec.js +442 -34
- package/dist/internal/signInProviders.d.ts +104 -0
- package/dist/internal/signInProviders.d.ts.map +1 -0
- package/dist/internal/signInProviders.js +245 -0
- package/dist/manifest.d.ts +238 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +30 -0
- package/dist/server/ai.d.ts +111 -0
- package/dist/server/ai.d.ts.map +1 -0
- package/dist/server/ai.js +145 -0
- package/dist/server/aiBinding.d.ts +68 -0
- package/dist/server/aiBinding.d.ts.map +1 -0
- package/dist/server/aiBinding.js +294 -0
- package/dist/server/attribution.d.ts +9 -5
- package/dist/server/attribution.d.ts.map +1 -1
- package/dist/server/attribution.js +12 -4
- package/dist/server/authorizeUrl.d.ts +1 -1
- package/dist/server/authorizeUrl.d.ts.map +1 -1
- package/dist/server/authorizeUrl.js +2 -6
- package/dist/server/connectUrl.d.ts +21 -0
- package/dist/server/connectUrl.d.ts.map +1 -0
- package/dist/server/connectUrl.js +37 -0
- package/dist/server/context.d.ts +68 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +117 -0
- package/dist/server/emailBinding.d.ts +72 -0
- package/dist/server/emailBinding.d.ts.map +1 -0
- package/dist/server/emailBinding.js +124 -0
- package/dist/server/emit.d.ts +1 -1
- package/dist/server/emit.d.ts.map +1 -1
- package/dist/server/emit.js +7 -7
- package/dist/server/env.d.ts +44 -0
- package/dist/server/env.d.ts.map +1 -0
- package/dist/server/env.js +1 -0
- package/dist/server/everyApp.d.ts +27 -7
- package/dist/server/everyApp.d.ts.map +1 -1
- package/dist/server/everyApp.js +106 -5
- package/dist/server/fromMcpTools.d.ts +89 -0
- package/dist/server/fromMcpTools.d.ts.map +1 -0
- package/dist/server/fromMcpTools.js +192 -0
- package/dist/server/gatewayFetch.d.ts +42 -18
- package/dist/server/gatewayFetch.d.ts.map +1 -1
- package/dist/server/gatewayFetch.js +138 -19
- package/dist/server/gatewayUrl.d.ts +13 -0
- package/dist/server/gatewayUrl.d.ts.map +1 -0
- package/dist/server/gatewayUrl.js +34 -0
- package/dist/server/index.d.ts +15 -4
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +12 -2
- package/dist/server/integrationFetch.d.ts +70 -39
- package/dist/server/integrationFetch.d.ts.map +1 -1
- package/dist/server/integrationFetch.js +244 -82
- package/dist/server/listMembers.d.ts +1 -1
- package/dist/server/listMembers.d.ts.map +1 -1
- package/dist/server/listMembers.js +8 -11
- package/dist/server/refusal.d.ts +55 -0
- package/dist/server/refusal.d.ts.map +1 -0
- package/dist/server/refusal.js +193 -0
- package/dist/server/sendEmail.d.ts +1 -1
- package/dist/server/sendEmail.d.ts.map +1 -1
- package/dist/server/sendEmail.js +7 -7
- package/dist/server/toolsEndpoint.d.ts.map +1 -1
- package/dist/server/toolsEndpoint.js +11 -5
- package/dist/tanstack-start/index.d.ts +9 -0
- package/dist/tanstack-start/index.d.ts.map +1 -0
- package/dist/tanstack-start/index.js +29 -0
- package/dist/testing/index.d.ts +93 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +156 -0
- package/package.json +35 -14
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Gateway's own refusals, read the same way wherever they surface: a
|
|
3
|
+
* `Response` from a gateway fetch ({@link gatewayRefusal}), the error an
|
|
4
|
+
* SDK helper throws ({@link GatewayRefusalError}), or the error an AI
|
|
5
|
+
* client throws for a refused call (an OpenAI SDK's `APIError`, ai-sdk's
|
|
6
|
+
* `APICallError`) ({@link refusalFromError}).
|
|
7
|
+
*
|
|
8
|
+
* A refusal that means "not set up" (`integration_not_configured`,
|
|
9
|
+
* `ai_provider_not_configured`, `ai_default_not_configured`) carries what
|
|
10
|
+
* to do in the Gateway's words: `settings` (the setting names it needs),
|
|
11
|
+
* `setup` (one sentence for a person) and `command` (what an admin runs,
|
|
12
|
+
* `pnpm everyapp configure <app>`, or null). Show them as they are; never
|
|
13
|
+
* match the message text or hardcode a setting name or a command.
|
|
14
|
+
*/
|
|
15
|
+
import { ConfigurationError, GATEWAY_ERROR_HEADER } from "../internal/index.js";
|
|
16
|
+
/** The refusal codes that mean no model is set up for the call. */
|
|
17
|
+
export const AI_NOT_CONFIGURED_CODES = [
|
|
18
|
+
"ai_provider_not_configured",
|
|
19
|
+
"ai_default_not_configured",
|
|
20
|
+
];
|
|
21
|
+
/**
|
|
22
|
+
* A refusal the AppGateway binding made, thrown: what `sendEmail()` throws
|
|
23
|
+
* when the gateway won't send (`email_sender_not_allowed`,
|
|
24
|
+
* `email_budget_exhausted`, ...). The thrown twin of {@link gatewayRefusal}.
|
|
25
|
+
*/
|
|
26
|
+
export class GatewayRefusalError extends Error {
|
|
27
|
+
status;
|
|
28
|
+
code;
|
|
29
|
+
settings;
|
|
30
|
+
setup;
|
|
31
|
+
command;
|
|
32
|
+
constructor({ status, code, message, settings = [], setup = null, command = null, }) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = "GatewayRefusalError";
|
|
35
|
+
this.status = status;
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.settings = settings;
|
|
38
|
+
this.setup = setup;
|
|
39
|
+
this.command = command;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The gateway's own refusal in `response`, or null when the response is the
|
|
44
|
+
* provider's answer (which may be an error of its own). Reads the body only
|
|
45
|
+
* for a refusal, so call it before reading the body yourself.
|
|
46
|
+
*/
|
|
47
|
+
export async function gatewayRefusal(response) {
|
|
48
|
+
const code = response.headers.get(GATEWAY_ERROR_HEADER);
|
|
49
|
+
if (!code)
|
|
50
|
+
return null;
|
|
51
|
+
const body = await response.json().catch(() => null);
|
|
52
|
+
return refusalOf(response.status, code, body);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The Gateway's refusal behind a thrown error, or null when it isn't one:
|
|
56
|
+
* a {@link GatewayRefusalError} (`sendEmail()`, `listMembers()`, `emit()`),
|
|
57
|
+
* an OpenAI SDK `APIError` or an ai-sdk `APICallError` for a call the
|
|
58
|
+
* Gateway refused (also inside ai-sdk's `RetryError` or an error's
|
|
59
|
+
* `cause`), or the SDK's local-development `ConfigurationError` for a
|
|
60
|
+
* missing key, which answers as the deployed refusal would
|
|
61
|
+
* (`ai_provider_not_configured`, status 503, the `.dev.vars` names).
|
|
62
|
+
*/
|
|
63
|
+
export function refusalFromError(error) {
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
let current = error;
|
|
66
|
+
while (current && typeof current === "object" && !seen.has(current)) {
|
|
67
|
+
seen.add(current);
|
|
68
|
+
const found = refusalHere(current);
|
|
69
|
+
if (found)
|
|
70
|
+
return found;
|
|
71
|
+
const record = current;
|
|
72
|
+
current = record["lastError"] ?? record["cause"];
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Whether `error` (or a refusal already read) means no model is set up:
|
|
78
|
+
* `ai_provider_not_configured` or `ai_default_not_configured`, from the
|
|
79
|
+
* Gateway or, in local development, from the SDK with no key in
|
|
80
|
+
* `.dev.vars`. Show `refusalFromError(error).setup` to the person.
|
|
81
|
+
*/
|
|
82
|
+
export function isAiNotConfigured(error) {
|
|
83
|
+
const refusal = isRefusal(error) ? error : refusalFromError(error);
|
|
84
|
+
return refusal !== null && AI_NOT_CONFIGURED_CODES.includes(refusal.code);
|
|
85
|
+
}
|
|
86
|
+
function isRefusal(value) {
|
|
87
|
+
return (!!value &&
|
|
88
|
+
typeof value === "object" &&
|
|
89
|
+
!(value instanceof Error) &&
|
|
90
|
+
typeof value.code === "string" &&
|
|
91
|
+
typeof value.status === "number");
|
|
92
|
+
}
|
|
93
|
+
function refusalHere(error) {
|
|
94
|
+
// A Response is read with gatewayRefusal(), which can read its body.
|
|
95
|
+
if (error instanceof Response)
|
|
96
|
+
return null;
|
|
97
|
+
if (error instanceof GatewayRefusalError) {
|
|
98
|
+
return {
|
|
99
|
+
status: error.status,
|
|
100
|
+
code: error.code,
|
|
101
|
+
message: error.message,
|
|
102
|
+
settings: error.settings,
|
|
103
|
+
setup: error.setup,
|
|
104
|
+
command: error.command,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (error instanceof ConfigurationError) {
|
|
108
|
+
if (!error.code)
|
|
109
|
+
return null;
|
|
110
|
+
return {
|
|
111
|
+
status: 503,
|
|
112
|
+
code: error.code,
|
|
113
|
+
message: error.message,
|
|
114
|
+
settings: error.settings,
|
|
115
|
+
setup: error.message,
|
|
116
|
+
command: null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const record = error;
|
|
120
|
+
// ai-sdk's APICallError: lowercase header names, the body as text.
|
|
121
|
+
const responseHeaders = record["responseHeaders"];
|
|
122
|
+
if (responseHeaders && typeof responseHeaders === "object") {
|
|
123
|
+
const code = headerOf(responseHeaders, GATEWAY_ERROR_HEADER);
|
|
124
|
+
if (!code)
|
|
125
|
+
return null;
|
|
126
|
+
return refusalOf(numberOf(record["statusCode"]), code, parseJson(record["responseBody"]), messageOf(error));
|
|
127
|
+
}
|
|
128
|
+
// An OpenAI SDK APIError: `headers` (Headers or a record), `status`,
|
|
129
|
+
// and `error`, the body's `error` object (which carries the same fields).
|
|
130
|
+
const headers = record["headers"];
|
|
131
|
+
if (headers && typeof headers === "object" && "status" in record) {
|
|
132
|
+
const code = headerOf(headers, GATEWAY_ERROR_HEADER);
|
|
133
|
+
if (!code)
|
|
134
|
+
return null;
|
|
135
|
+
return refusalOf(numberOf(record["status"]), code, record["error"], messageOf(error));
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
/** A refusal from its status, code and body (the Gateway's `{ code, message, … }`, or OpenAI's `{ error: {…} }`). */
|
|
140
|
+
function refusalOf(status, code, body, fallback) {
|
|
141
|
+
const top = recordOf(body);
|
|
142
|
+
const inner = recordOf(top?.["error"]);
|
|
143
|
+
const field = (name) => top?.[name] ?? inner?.[name];
|
|
144
|
+
const message = stringOf(top?.["message"]) ??
|
|
145
|
+
stringOf(inner?.["message"]) ??
|
|
146
|
+
fallback ??
|
|
147
|
+
`The EveryApp gateway refused the call (${code}).`;
|
|
148
|
+
const settings = field("settings");
|
|
149
|
+
return {
|
|
150
|
+
status,
|
|
151
|
+
code,
|
|
152
|
+
message,
|
|
153
|
+
settings: Array.isArray(settings)
|
|
154
|
+
? settings.filter((name) => typeof name === "string")
|
|
155
|
+
: [],
|
|
156
|
+
setup: stringOf(field("setup")) ?? null,
|
|
157
|
+
command: stringOf(field("command")) ?? null,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function headerOf(headers, name) {
|
|
161
|
+
if (typeof headers.get === "function") {
|
|
162
|
+
return headers.get(name);
|
|
163
|
+
}
|
|
164
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
165
|
+
if (key.toLowerCase() === name && typeof value === "string")
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
function recordOf(value) {
|
|
171
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
172
|
+
? value
|
|
173
|
+
: null;
|
|
174
|
+
}
|
|
175
|
+
function stringOf(value) {
|
|
176
|
+
return typeof value === "string" && value ? value : undefined;
|
|
177
|
+
}
|
|
178
|
+
function numberOf(value) {
|
|
179
|
+
return typeof value === "number" ? value : 0;
|
|
180
|
+
}
|
|
181
|
+
function parseJson(value) {
|
|
182
|
+
if (typeof value !== "string")
|
|
183
|
+
return null;
|
|
184
|
+
try {
|
|
185
|
+
return JSON.parse(value);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function messageOf(error) {
|
|
192
|
+
return error instanceof Error && error.message ? error.message : undefined;
|
|
193
|
+
}
|
|
@@ -35,5 +35,5 @@ export interface SendEmailOptions {
|
|
|
35
35
|
* `.dev.vars` it goes out for real through Email Sending's REST API, from
|
|
36
36
|
* the developer's own account.
|
|
37
37
|
*/
|
|
38
|
-
export declare function sendEmail({ env, for:
|
|
38
|
+
export declare function sendEmail({ env, for: explicitTarget, message, }: SendEmailOptions): Promise<SendEmailResult>;
|
|
39
39
|
//# sourceMappingURL=sendEmail.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sendEmail.d.ts","sourceRoot":"","sources":["../../src/server/sendEmail.ts"],"names":[],"mappings":"AAAA,OAAO,EAeL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACrB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"sendEmail.d.ts","sourceRoot":"","sources":["../../src/server/sendEmail.ts"],"names":[],"mappings":"AAAA,OAAO,EAeL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACrB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;AAQ1B,MAAM,WAAW,gBAAgB;IAC/B;;;;;OAKG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,gBAAgB,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,SAAS,CAAC,EAC9B,GAAG,EACH,GAAG,EAAE,cAAc,EACnB,OAAO,GACR,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAkB7C"}
|
package/dist/server/sendEmail.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ConfigurationError, EMAIL_SENDERS_ENV, emailDomain, emailProviderRefusal, emailRestBody, emailRestUrl, emailSenderRefusal, parseEmailList, parseEmailSender, parseSendEmailMessage, readEmailRestResponse, senderAllowed, serviceRoutePath, } from "../internal/index.js";
|
|
2
|
-
import { applyAttribution,
|
|
3
|
-
import { BINDING_ORIGIN } from "./gatewayFetch.js";
|
|
4
|
-
import { GatewayRefusalError, gatewayRefusal } from "./
|
|
2
|
+
import { applyAttribution, attributionFor, } from "./attribution.js";
|
|
3
|
+
import { BINDING_ORIGIN, gatewayBinding, } from "./gatewayFetch.js";
|
|
4
|
+
import { GatewayRefusalError, gatewayRefusal } from "./refusal.js";
|
|
5
5
|
/**
|
|
6
6
|
* Send an email through the gateway's `email` capability: its sender
|
|
7
7
|
* (EMAIL_FROM) unless `message.from` names an address the installation's
|
|
@@ -19,10 +19,10 @@ import { GatewayRefusalError, gatewayRefusal } from "./integrationFetch.js";
|
|
|
19
19
|
* `.dev.vars` it goes out for real through Email Sending's REST API, from
|
|
20
20
|
* the developer's own account.
|
|
21
21
|
*/
|
|
22
|
-
export async function sendEmail({ env, for:
|
|
23
|
-
|
|
22
|
+
export async function sendEmail({ env, for: explicitTarget, message, }) {
|
|
23
|
+
const target = attributionFor(env, explicitTarget, "sendEmail");
|
|
24
24
|
const values = env;
|
|
25
|
-
const binding =
|
|
25
|
+
const binding = gatewayBinding(env);
|
|
26
26
|
if (binding) {
|
|
27
27
|
if (typeof binding.fetch !== "function") {
|
|
28
28
|
throw new ConfigurationError("EVERY_APP_GATEWAY is configured but is not a valid service binding.");
|
|
@@ -49,7 +49,7 @@ async function sendThroughGateway(binding, target, message) {
|
|
|
49
49
|
throw new GatewayRefusalError({
|
|
50
50
|
status: response.status,
|
|
51
51
|
code: "email_send_failed",
|
|
52
|
-
message: `The
|
|
52
|
+
message: `The EveryApp gateway answered ${response.status} to the send.`,
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
return (await response.json());
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolsEndpoint.d.ts","sourceRoot":"","sources":["../../src/server/toolsEndpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAcL,KAAK,sBAAsB,EAO5B,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"toolsEndpoint.d.ts","sourceRoot":"","sources":["../../src/server/toolsEndpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAcL,KAAK,sBAAsB,EAO5B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAOL,KAAK,OAAO,EACb,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,QAAQ,EAAE,sBAAsB,CAAC;CAClC;AAoFD;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,oBAAoB,EACzB,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAyB1B"}
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* The perimeter also refuses these paths from outside and strips inbound
|
|
15
15
|
* `x-everyapp-*` headers, so only the gateway's ToolBus ever reaches them.
|
|
16
16
|
*/
|
|
17
|
-
import { ConfigurationError, IDENTITY_HEADER, IdentityError, SYSTEM_HEADER, TOOLS_CALL_PATH, TOOLS_LIST_PATH, TOOLS_PROTOCOL_VERSION, catalogHash, requiredScopeForRisk, resolveVerifyOptions,
|
|
17
|
+
import { ConfigurationError, IDENTITY_HEADER, IdentityError, SYSTEM_HEADER, TOOLS_CALL_PATH, TOOLS_LIST_PATH, TOOLS_PROTOCOL_VERSION, assertOrganizationPin, catalogHash, requiredScopeForRisk, resolveVerifyOptions, verifyIdentity, verifySystemJwt, } from "../internal/index.js";
|
|
18
|
+
import { serveRequest } from "./context.js";
|
|
18
19
|
import { SDK_VERSION, ToolError, toToolResult, toolSetInternals, validateWithSchema, } from "./tools.js";
|
|
19
20
|
function toolsJson(status, body, headers = {}) {
|
|
20
21
|
return Response.json(body, {
|
|
@@ -148,9 +149,12 @@ function parseCallRequest(body) {
|
|
|
148
149
|
return { v: TOOLS_PROTOCOL_VERSION, name, arguments: args, callId };
|
|
149
150
|
}
|
|
150
151
|
async function callTool(request, env, ctx, verify, options) {
|
|
151
|
-
let user;
|
|
152
|
+
let user, expiresAt;
|
|
152
153
|
try {
|
|
153
|
-
user = await
|
|
154
|
+
({ user, expiresAt } = await verifyIdentity(request.headers.get(IDENTITY_HEADER) ?? "", verify));
|
|
155
|
+
// The same pin as the handler path: a call for another organization's
|
|
156
|
+
// install never runs a tool here.
|
|
157
|
+
assertOrganizationPin(user, options.identity.organizationId);
|
|
154
158
|
}
|
|
155
159
|
catch (error) {
|
|
156
160
|
if (error instanceof IdentityError) {
|
|
@@ -194,13 +198,15 @@ async function callTool(request, env, ctx, verify, options) {
|
|
|
194
198
|
});
|
|
195
199
|
let result;
|
|
196
200
|
try {
|
|
197
|
-
|
|
201
|
+
// The call runs in the request context like a page would: currentUser()
|
|
202
|
+
// is the caller, and its Gateway calls are attributed to them.
|
|
203
|
+
const value = await serveRequest({ request, user, expiresAt }, () => tool.handler(input.value, {
|
|
198
204
|
user,
|
|
199
205
|
env: env,
|
|
200
206
|
ctx,
|
|
201
207
|
request,
|
|
202
208
|
callId: call.callId,
|
|
203
|
-
});
|
|
209
|
+
}));
|
|
204
210
|
result = await toToolResult(tool, value);
|
|
205
211
|
}
|
|
206
212
|
catch (error) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Puts the person the Gateway signed in on `context.user` (an
|
|
3
|
+
* `EveryAppUser`), verified from the identity the Gateway sent; throws the
|
|
4
|
+
* 401 Response when there is none (a public route, anonymous).
|
|
5
|
+
*/
|
|
6
|
+
export declare const ensureUserMiddleware: import("@tanstack/react-start").FunctionMiddlewareAfterServer<{}, unknown, undefined, {
|
|
7
|
+
user: import("../internal/protocol.js").EveryAppUser;
|
|
8
|
+
}, undefined, undefined, undefined>;
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tanstack-start/index.ts"],"names":[],"mappings":"AAmBA;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;mCAK/B,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @every-app/sdk/tanstack-start — identity for a TanStack Start app's server
|
|
3
|
+
* functions:
|
|
4
|
+
*
|
|
5
|
+
* import { ensureUserMiddleware } from "@every-app/sdk/tanstack-start";
|
|
6
|
+
*
|
|
7
|
+
* export const getTodos = createServerFn()
|
|
8
|
+
* .middleware([ensureUserMiddleware])
|
|
9
|
+
* .handler(({ context: { user } }) => listTodos(user.id));
|
|
10
|
+
*
|
|
11
|
+
* The SDK lists `@tanstack/react-start` as a peer dependency, so the Start
|
|
12
|
+
* plugin compiles this module like the app's own: the `.server()` part never
|
|
13
|
+
* reaches the browser.
|
|
14
|
+
*/
|
|
15
|
+
import { createMiddleware } from "@tanstack/react-start";
|
|
16
|
+
import { getRequest } from "@tanstack/react-start/server";
|
|
17
|
+
import { env } from "cloudflare:workers";
|
|
18
|
+
import { requireEveryAppUser } from "../server/everyApp.js";
|
|
19
|
+
/**
|
|
20
|
+
* Puts the person the Gateway signed in on `context.user` (an
|
|
21
|
+
* `EveryAppUser`), verified from the identity the Gateway sent; throws the
|
|
22
|
+
* 401 Response when there is none (a public route, anonymous).
|
|
23
|
+
*/
|
|
24
|
+
export const ensureUserMiddleware = createMiddleware({
|
|
25
|
+
type: "function",
|
|
26
|
+
}).server(async ({ next }) => {
|
|
27
|
+
const user = await requireEveryAppUser(getRequest(), env);
|
|
28
|
+
return next({ context: { user } });
|
|
29
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { IDENTITY_HEADER, type IdentityChannel, type ToolCallResultV1, type ToolsCallResponseV1, type ToolsListResponseV1 } from "../internal/index.js";
|
|
2
|
+
import type { ToolSet } from "../server/tools.js";
|
|
3
|
+
export { IDENTITY_HEADER };
|
|
4
|
+
export type { ToolCallResultV1 as ToolCallResult, ToolsCallResponseV1 as ToolCallResponse, ToolsListResponseV1 as ToolsListResponse, };
|
|
5
|
+
/** The person a test signs in; each field left out takes {@link TEST_USER}'s. */
|
|
6
|
+
export interface TestUser {
|
|
7
|
+
id?: string;
|
|
8
|
+
email?: string;
|
|
9
|
+
orgId?: string;
|
|
10
|
+
/** `owner`, `admin` or `member`, as the Gateway signs it. */
|
|
11
|
+
orgRole?: string;
|
|
12
|
+
}
|
|
13
|
+
/** The person a test signs in by default: the owner of `org_test`. */
|
|
14
|
+
export declare const TEST_USER: Readonly<Required<TestUser>>;
|
|
15
|
+
/** The app id {@link testEnv} uses when none is given. */
|
|
16
|
+
export declare const TEST_APP_ID = "test-app";
|
|
17
|
+
/** What an identity is minted for, beside the person. */
|
|
18
|
+
export interface TestIdentityOptions {
|
|
19
|
+
user?: TestUser;
|
|
20
|
+
/** The app the identity is for (the token's audience). Default: {@link TEST_APP_ID}. */
|
|
21
|
+
appId?: string;
|
|
22
|
+
/** Default `["*"]`: a browser session, which holds every scope. */
|
|
23
|
+
scopes?: string[];
|
|
24
|
+
/** Default `"web"`. */
|
|
25
|
+
channel?: IdentityChannel;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The Worker env `everyApp()` needs to trust test identities: the app id,
|
|
29
|
+
* the dev issuer and key, `EVERYAPP_DEV`, and a `GATEWAY_URL`. `overrides`
|
|
30
|
+
* adds the app's own bindings (a fake `DB`, a secret) or replaces any of
|
|
31
|
+
* these.
|
|
32
|
+
*/
|
|
33
|
+
export declare function testEnv<T extends object = Record<never, never>>(overrides?: {
|
|
34
|
+
appId?: string;
|
|
35
|
+
} & T): {
|
|
36
|
+
EVERYAPP_APP_ID: string;
|
|
37
|
+
EVERYAPP_DEV: string;
|
|
38
|
+
EVERYAPP_IDENTITY_ISSUER: string;
|
|
39
|
+
EVERYAPP_IDENTITY_PUBLIC_KEYS: string;
|
|
40
|
+
GATEWAY_URL: string;
|
|
41
|
+
} & Omit<T, "appId">;
|
|
42
|
+
/**
|
|
43
|
+
* An `ExecutionContext` for calling a Worker's `fetch` in a test:
|
|
44
|
+
* `waitUntil` keeps each promise in `waited`, so a test can await them.
|
|
45
|
+
*/
|
|
46
|
+
export declare function testContext(): ExecutionContext & {
|
|
47
|
+
waited: Promise<unknown>[];
|
|
48
|
+
};
|
|
49
|
+
/** An identity token for `user`, as the Gateway sends in the identity header. */
|
|
50
|
+
export declare function mintTestIdentity(options?: TestIdentityOptions & {
|
|
51
|
+
toolCall?: {
|
|
52
|
+
id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
};
|
|
55
|
+
}): Promise<string>;
|
|
56
|
+
/**
|
|
57
|
+
* A request as the Gateway forwards it: `user` signed in (the default
|
|
58
|
+
* person when left out), or nobody with `user: null`, as on a public route.
|
|
59
|
+
*/
|
|
60
|
+
export declare function testRequest(input: string | URL, init?: RequestInit & Omit<TestIdentityOptions, "user"> & {
|
|
61
|
+
user?: TestUser | null;
|
|
62
|
+
}): Promise<Request>;
|
|
63
|
+
/** A Worker as `everyApp()` returns it. */
|
|
64
|
+
export interface TestWorker {
|
|
65
|
+
fetch(request: Request, env: never, ctx: ExecutionContext): Response | Promise<Response>;
|
|
66
|
+
}
|
|
67
|
+
export interface CallToolOptions {
|
|
68
|
+
user?: TestUser;
|
|
69
|
+
/**
|
|
70
|
+
* The caller's scopes. Default: `mcp:read`, `mcp:write` and
|
|
71
|
+
* `mcp:destructive`, so every tool may be called; pass fewer to test a
|
|
72
|
+
* refusal.
|
|
73
|
+
*/
|
|
74
|
+
scopes?: string[];
|
|
75
|
+
/** Default: {@link testEnv}() for the worker's app. */
|
|
76
|
+
env?: object;
|
|
77
|
+
ctx?: ExecutionContext;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Call one tool as the Gateway does for an AI client: a `chan: "mcp"`
|
|
81
|
+
* identity minted for this call, through the app's tools endpoint, so the
|
|
82
|
+
* SDK checks the scope, validates input and output and maps errors as it
|
|
83
|
+
* does deployed. `app` is the Worker `everyApp()` returned, or the
|
|
84
|
+
* `defineTools()` set alone. The answer is `{ result }`, or `{ error }`
|
|
85
|
+
* when the SDK refused the call.
|
|
86
|
+
*/
|
|
87
|
+
export declare function callTool(app: TestWorker | ToolSet, name: string, args?: Record<string, unknown>, options?: CallToolOptions): Promise<ToolsCallResponseV1>;
|
|
88
|
+
/** The tool catalog, read as the Gateway reads it when the app deploys. */
|
|
89
|
+
export declare function listTools(app: TestWorker | ToolSet, options?: {
|
|
90
|
+
env?: object;
|
|
91
|
+
ctx?: ExecutionContext;
|
|
92
|
+
}): Promise<ToolsListResponseV1>;
|
|
93
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAuBA,OAAO,EAIL,eAAe,EASf,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACzB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,eAAe,EAAE,CAAC;AAC3B,YAAY,EACV,gBAAgB,IAAI,cAAc,EAClC,mBAAmB,IAAI,gBAAgB,EACvC,mBAAmB,IAAI,iBAAiB,GACzC,CAAC;AAEF,iFAAiF;AACjF,MAAM,WAAW,QAAQ;IACvB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,sEAAsE;AACtE,eAAO,MAAM,SAAS,EAAE,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAKjD,CAAC;AAEH,0DAA0D;AAC1D,eAAO,MAAM,WAAW,aAAa,CAAC;AAEtC,yDAAyD;AACzD,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,wFAAwF;IACxF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,uBAAuB;IACvB,OAAO,CAAC,EAAE,eAAe,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAC7D,SAAS,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,CAAgC,GAC/D;IACD,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,wBAAwB,EAAE,MAAM,CAAC;IACjC,6BAA6B,EAAE,MAAM,CAAC;IACtC,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,CAUnB;AAED;;;GAGG;AACH,wBAAgB,WAAW,IAAI,gBAAgB,GAAG;IAChD,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;CAC5B,CAQA;AAED,iFAAiF;AACjF,wBAAsB,gBAAgB,CACpC,OAAO,GAAE,mBAAmB,GAAG;IAC7B,QAAQ,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACpC,GACL,OAAO,CAAC,MAAM,CAAC,CAgBjB;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,MAAM,GAAG,GAAG,EACnB,IAAI,GAAE,WAAW,GACf,IAAI,CAAC,mBAAmB,EAAE,MAAM,CAAC,GAAG;IAAE,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAA;CAAO,GACpE,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED,2CAA2C;AAC3C,MAAM,WAAW,UAAU;IACzB,KAAK,CACH,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,KAAK,EACV,GAAG,EAAE,gBAAgB,GACpB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,uDAAuD;IACvD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,gBAAgB,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,UAAU,GAAG,OAAO,EACzB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EAClC,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,mBAAmB,CAAC,CAwB9B;AAED,2EAA2E;AAC3E,wBAAsB,SAAS,CAC7B,GAAG,EAAE,UAAU,GAAG,OAAO,EACzB,OAAO,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,gBAAgB,CAAA;CAAO,GACrD,OAAO,CAAC,mBAAmB,CAAC,CAuB9B"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @every-app/sdk/testing — an app's tests, without a Gateway: a signed-in
|
|
3
|
+
* person for a request, and the tools called the way the Gateway calls them.
|
|
4
|
+
*
|
|
5
|
+
* import { callTool, testContext, testEnv, testRequest } from "@every-app/sdk/testing";
|
|
6
|
+
*
|
|
7
|
+
* const worker = everyApp(handler, { tools });
|
|
8
|
+
* const response = await worker.fetch(
|
|
9
|
+
* await testRequest("https://todo.test/api/todos", { user: { id: "ana" } }),
|
|
10
|
+
* testEnv(),
|
|
11
|
+
* testContext(),
|
|
12
|
+
* );
|
|
13
|
+
* const result = await callTool(worker, "list_todos", {}, { user: { id: "ana" } });
|
|
14
|
+
*
|
|
15
|
+
* Identities are signed with the well-known dev keypair, which `everyApp()`
|
|
16
|
+
* trusts only with `EVERYAPP_DEV` set, as {@link testEnv} sets it: never in a
|
|
17
|
+
* deployed Worker. For tests only; a Worker never imports this module.
|
|
18
|
+
*/
|
|
19
|
+
import { DEV_ISSUER, DEV_PRIVATE_KEY_PEM, DEV_PUBLIC_KEY_PEM, } from "../dev/keypair.js";
|
|
20
|
+
import { APP_ID_ENV, AUDIENCE_ENV, IDENTITY_DEV_KEY_ID, IDENTITY_HEADER, MCP_DESTRUCTIVE_SCOPE, MCP_READ_SCOPE, MCP_WRITE_SCOPE, SYSTEM_HEADER, TOOLS_CALL_PATH, TOOLS_LIST_PATH, mintIdentityJwt, mintSystemJwt, } from "../internal/index.js";
|
|
21
|
+
import { everyApp } from "../server/everyApp.js";
|
|
22
|
+
export { IDENTITY_HEADER };
|
|
23
|
+
/** The person a test signs in by default: the owner of `org_test`. */
|
|
24
|
+
export const TEST_USER = Object.freeze({
|
|
25
|
+
id: "user_test",
|
|
26
|
+
email: "test@example.com",
|
|
27
|
+
orgId: "org_test",
|
|
28
|
+
orgRole: "owner",
|
|
29
|
+
});
|
|
30
|
+
/** The app id {@link testEnv} uses when none is given. */
|
|
31
|
+
export const TEST_APP_ID = "test-app";
|
|
32
|
+
/**
|
|
33
|
+
* The Worker env `everyApp()` needs to trust test identities: the app id,
|
|
34
|
+
* the dev issuer and key, `EVERYAPP_DEV`, and a `GATEWAY_URL`. `overrides`
|
|
35
|
+
* adds the app's own bindings (a fake `DB`, a secret) or replaces any of
|
|
36
|
+
* these.
|
|
37
|
+
*/
|
|
38
|
+
export function testEnv(overrides = {}) {
|
|
39
|
+
const { appId = TEST_APP_ID, ...rest } = overrides;
|
|
40
|
+
return {
|
|
41
|
+
EVERYAPP_APP_ID: appId,
|
|
42
|
+
EVERYAPP_DEV: "1",
|
|
43
|
+
EVERYAPP_IDENTITY_ISSUER: DEV_ISSUER,
|
|
44
|
+
EVERYAPP_IDENTITY_PUBLIC_KEYS: JSON.stringify([DEV_PUBLIC_KEY_PEM]),
|
|
45
|
+
GATEWAY_URL: "https://gateway.test",
|
|
46
|
+
...rest,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* An `ExecutionContext` for calling a Worker's `fetch` in a test:
|
|
51
|
+
* `waitUntil` keeps each promise in `waited`, so a test can await them.
|
|
52
|
+
*/
|
|
53
|
+
export function testContext() {
|
|
54
|
+
const waited = [];
|
|
55
|
+
return {
|
|
56
|
+
waited,
|
|
57
|
+
waitUntil: (promise) => void waited.push(promise),
|
|
58
|
+
passThroughOnException: () => { },
|
|
59
|
+
props: {},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** An identity token for `user`, as the Gateway sends in the identity header. */
|
|
63
|
+
export async function mintTestIdentity(options = {}) {
|
|
64
|
+
const user = { ...TEST_USER, ...options.user };
|
|
65
|
+
return mintIdentityJwt(DEV_PRIVATE_KEY_PEM, {
|
|
66
|
+
subject: {
|
|
67
|
+
sub: user.id,
|
|
68
|
+
email: user.email,
|
|
69
|
+
orgId: user.orgId,
|
|
70
|
+
orgRole: user.orgRole,
|
|
71
|
+
},
|
|
72
|
+
audience: options.appId ?? TEST_APP_ID,
|
|
73
|
+
issuer: DEV_ISSUER,
|
|
74
|
+
keyId: IDENTITY_DEV_KEY_ID,
|
|
75
|
+
channel: options.channel,
|
|
76
|
+
scopes: options.scopes,
|
|
77
|
+
toolCall: options.toolCall,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A request as the Gateway forwards it: `user` signed in (the default
|
|
82
|
+
* person when left out), or nobody with `user: null`, as on a public route.
|
|
83
|
+
*/
|
|
84
|
+
export async function testRequest(input, init = {}) {
|
|
85
|
+
const { user, appId, scopes, channel, ...requestInit } = init;
|
|
86
|
+
const headers = new Headers(requestInit.headers);
|
|
87
|
+
if (user !== null) {
|
|
88
|
+
headers.set(IDENTITY_HEADER, await mintTestIdentity({ user, appId, scopes, channel }));
|
|
89
|
+
}
|
|
90
|
+
return new Request(input, { ...requestInit, headers });
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Call one tool as the Gateway does for an AI client: a `chan: "mcp"`
|
|
94
|
+
* identity minted for this call, through the app's tools endpoint, so the
|
|
95
|
+
* SDK checks the scope, validates input and output and maps errors as it
|
|
96
|
+
* does deployed. `app` is the Worker `everyApp()` returned, or the
|
|
97
|
+
* `defineTools()` set alone. The answer is `{ result }`, or `{ error }`
|
|
98
|
+
* when the SDK refused the call.
|
|
99
|
+
*/
|
|
100
|
+
export async function callTool(app, name, args = {}, options = {}) {
|
|
101
|
+
const env = options.env ?? testEnv();
|
|
102
|
+
const callId = crypto.randomUUID();
|
|
103
|
+
const identity = await mintTestIdentity({
|
|
104
|
+
user: options.user,
|
|
105
|
+
appId: audienceOf(env),
|
|
106
|
+
channel: "mcp",
|
|
107
|
+
scopes: options.scopes ?? [
|
|
108
|
+
MCP_READ_SCOPE,
|
|
109
|
+
MCP_WRITE_SCOPE,
|
|
110
|
+
MCP_DESTRUCTIVE_SCOPE,
|
|
111
|
+
],
|
|
112
|
+
toolCall: { id: callId, name },
|
|
113
|
+
});
|
|
114
|
+
const response = await workerOf(app).fetch(new Request(`https://app.test${TOOLS_CALL_PATH}`, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: { [IDENTITY_HEADER]: identity },
|
|
117
|
+
body: JSON.stringify({ v: 1, name, arguments: args, callId }),
|
|
118
|
+
}), env, options.ctx ?? testContext());
|
|
119
|
+
return (await response.json());
|
|
120
|
+
}
|
|
121
|
+
/** The tool catalog, read as the Gateway reads it when the app deploys. */
|
|
122
|
+
export async function listTools(app, options = {}) {
|
|
123
|
+
const env = options.env ?? testEnv();
|
|
124
|
+
const token = await mintSystemJwt(DEV_PRIVATE_KEY_PEM, {
|
|
125
|
+
audience: audienceOf(env),
|
|
126
|
+
issuer: DEV_ISSUER,
|
|
127
|
+
purpose: "tools.list",
|
|
128
|
+
keyId: IDENTITY_DEV_KEY_ID,
|
|
129
|
+
});
|
|
130
|
+
const response = await workerOf(app).fetch(new Request(`https://app.test${TOOLS_LIST_PATH}`, {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: { [SYSTEM_HEADER]: token },
|
|
133
|
+
body: JSON.stringify({ v: 1 }),
|
|
134
|
+
}), env, options.ctx ?? testContext());
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
throw new Error(`the tools list answered ${response.status}: ${await response.text()}`);
|
|
137
|
+
}
|
|
138
|
+
return (await response.json());
|
|
139
|
+
}
|
|
140
|
+
/** Who the app's identities are minted for: the deployed audience, else the app id. */
|
|
141
|
+
function audienceOf(env) {
|
|
142
|
+
const values = env;
|
|
143
|
+
for (const key of [AUDIENCE_ENV, APP_ID_ENV]) {
|
|
144
|
+
const value = values[key];
|
|
145
|
+
if (typeof value === "string" && value)
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
return TEST_APP_ID;
|
|
149
|
+
}
|
|
150
|
+
function workerOf(app) {
|
|
151
|
+
if ("fetch" in app)
|
|
152
|
+
return app;
|
|
153
|
+
return everyApp(() => new Response("Not found", { status: 404 }), {
|
|
154
|
+
tools: app,
|
|
155
|
+
});
|
|
156
|
+
}
|