@every-app/sdk 0.2.0 → 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 +609 -3
- 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 +23 -0
- package/dist/cloudflare/index.d.ts.map +1 -0
- package/dist/cloudflare/index.js +24 -0
- package/dist/cloudflare/sentinel.d.ts +127 -0
- package/dist/cloudflare/sentinel.d.ts.map +1 -0
- package/dist/cloudflare/sentinel.js +85 -0
- package/dist/cloudflare/worker.d.ts +805 -0
- package/dist/cloudflare/worker.d.ts.map +1 -0
- package/dist/cloudflare/worker.js +997 -0
- package/dist/dev/index.d.ts +8 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +7 -0
- package/dist/dev/keypair.d.ts +26 -0
- package/dist/dev/keypair.d.ts.map +1 -0
- package/dist/dev/keypair.js +62 -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 +72 -0
- package/dist/internal/aiProviders.d.ts.map +1 -0
- package/dist/internal/aiProviders.js +233 -0
- package/dist/internal/appSettings.d.ts +315 -0
- package/dist/internal/appSettings.d.ts.map +1 -0
- package/dist/internal/appSettings.js +549 -0
- 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 +3174 -0
- package/dist/internal/capabilities.d.ts.map +1 -0
- package/dist/internal/capabilities.js +1110 -0
- 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 +49 -0
- package/dist/internal/devProviders.d.ts.map +1 -0
- package/dist/internal/devProviders.js +80 -0
- package/dist/internal/email.d.ts +280 -0
- package/dist/internal/email.d.ts.map +1 -0
- package/dist/internal/email.js +613 -0
- package/dist/internal/events.d.ts +193 -0
- package/dist/internal/events.d.ts.map +1 -0
- package/dist/internal/events.js +367 -0
- package/dist/internal/gatewaySettings.d.ts +83 -0
- package/dist/internal/gatewaySettings.d.ts.map +1 -0
- package/dist/internal/gatewaySettings.js +215 -0
- package/dist/internal/identity.d.ts +57 -5
- package/dist/internal/identity.d.ts.map +1 -1
- package/dist/internal/identity.js +99 -22
- package/dist/internal/index.d.ts +22 -1
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +22 -1
- package/dist/internal/members.d.ts +36 -0
- package/dist/internal/members.d.ts.map +1 -0
- package/dist/internal/members.js +7 -0
- package/dist/internal/oauthClient.d.ts +61 -0
- package/dist/internal/oauthClient.d.ts.map +1 -0
- package/dist/internal/oauthClient.js +133 -0
- 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 +172 -0
- package/dist/internal/oauthScopes.d.ts.map +1 -0
- package/dist/internal/oauthScopes.js +510 -0
- 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/organizationName.d.ts +18 -0
- package/dist/internal/organizationName.d.ts.map +1 -0
- package/dist/internal/organizationName.js +24 -0
- package/dist/internal/protocol.d.ts +150 -3
- package/dist/internal/protocol.d.ts.map +1 -1
- package/dist/internal/protocol.js +163 -2
- package/dist/internal/providerApps.d.ts +75 -0
- package/dist/internal/providerApps.d.ts.map +1 -0
- package/dist/internal/providerApps.js +59 -0
- package/dist/internal/providerRegistry.d.ts +218 -0
- package/dist/internal/providerRegistry.d.ts.map +1 -0
- package/dist/internal/providerRegistry.js +432 -0
- package/dist/internal/providerSpec.d.ts +66 -0
- package/dist/internal/providerSpec.d.ts.map +1 -0
- package/dist/internal/providerSpec.js +1154 -0
- package/dist/internal/remoteMcp.d.ts +188 -0
- package/dist/internal/remoteMcp.d.ts.map +1 -0
- package/dist/internal/remoteMcp.js +543 -0
- 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/internal/tools.d.ts +115 -0
- package/dist/internal/tools.d.ts.map +1 -0
- package/dist/internal/tools.js +80 -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 +43 -0
- package/dist/server/attribution.d.ts.map +1 -0
- package/dist/server/attribution.js +75 -0
- package/dist/server/authorizeUrl.d.ts +43 -0
- package/dist/server/authorizeUrl.d.ts.map +1 -0
- package/dist/server/authorizeUrl.js +106 -0
- 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 +63 -0
- package/dist/server/emit.d.ts.map +1 -0
- package/dist/server/emit.js +101 -0
- 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 +65 -19
- package/dist/server/everyApp.d.ts.map +1 -1
- package/dist/server/everyApp.js +195 -30
- 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 +54 -14
- package/dist/server/gatewayFetch.d.ts.map +1 -1
- package/dist/server/gatewayFetch.js +201 -37
- 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 +23 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +19 -2
- package/dist/server/integrationFetch.d.ts +162 -0
- package/dist/server/integrationFetch.d.ts.map +1 -0
- package/dist/server/integrationFetch.js +688 -0
- package/dist/server/listMembers.d.ts +32 -0
- package/dist/server/listMembers.d.ts.map +1 -0
- package/dist/server/listMembers.js +71 -0
- package/dist/server/nodeServer.d.ts +48 -0
- package/dist/server/nodeServer.d.ts.map +1 -0
- package/dist/server/nodeServer.js +45 -0
- 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 +39 -0
- package/dist/server/sendEmail.d.ts.map +1 -0
- package/dist/server/sendEmail.js +114 -0
- package/dist/server/tools.d.ts +184 -0
- package/dist/server/tools.d.ts.map +1 -0
- package/dist/server/tools.js +347 -0
- package/dist/server/toolsEndpoint.d.ts +32 -0
- package/dist/server/toolsEndpoint.d.ts.map +1 -0
- package/dist/server/toolsEndpoint.js +237 -0
- 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 +42 -8
- package/dist/server/mcp.d.ts +0 -66
- package/dist/server/mcp.d.ts.map +0 -1
- package/dist/server/mcp.js +0 -147
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type ListMembersResult } from "../internal/index.js";
|
|
2
|
+
import { type GatewayCallFor } from "./attribution.js";
|
|
3
|
+
export interface ListMembersOptions {
|
|
4
|
+
/** The Worker env: the EVERY_APP_GATEWAY binding when deployed; EVERYAPP_DEV in local dev. */
|
|
5
|
+
env: object;
|
|
6
|
+
/**
|
|
7
|
+
* Whom the list is for: the inbound request (its user), or
|
|
8
|
+
* `{ background: true, organizationId }` for work no person started (a
|
|
9
|
+
* cron sending reports). An organization's own app may omit it for
|
|
10
|
+
* background work; a shared app's Worker must pass one or the other
|
|
11
|
+
* ({@link AttributionError}).
|
|
12
|
+
*/
|
|
13
|
+
for?: GatewayCallFor;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The organization's members who can open the app now, through the
|
|
17
|
+
* gateway's `members` capability: people deactivated, removed from the
|
|
18
|
+
* organization or (for an organization's own app) not granted the app are
|
|
19
|
+
* left out. Read it when acting for members who aren't in a request, such
|
|
20
|
+
* as choosing whom a scheduled report goes to, instead of a copy the app
|
|
21
|
+
* kept. The app lists `capabilities: { members: { access: "read" } }`.
|
|
22
|
+
*
|
|
23
|
+
* Throws {@link GatewayRefusalError} when the gateway won't list
|
|
24
|
+
* (`capability_not_requested`, `integration_agent_not_allowed` for an
|
|
25
|
+
* agent's tool call, ...).
|
|
26
|
+
*
|
|
27
|
+
* In local dev (`everyapp dev`, no binding) there is no directory: a call
|
|
28
|
+
* for a request answers its signed-in user alone, and background work
|
|
29
|
+
* answers no one.
|
|
30
|
+
*/
|
|
31
|
+
export declare function listMembers({ env, for: explicitTarget, }: ListMembersOptions): Promise<ListMembersResult>;
|
|
32
|
+
//# sourceMappingURL=listMembers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listMembers.d.ts","sourceRoot":"","sources":["../../src/server/listMembers.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;AAS1B,MAAM,WAAW,kBAAkB;IACjC,8FAA8F;IAC9F,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,cAAc,CAAC;CACtB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,WAAW,CAAC,EAChC,GAAG,EACH,GAAG,EAAE,cAAc,GACpB,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAkBjD"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ConfigurationError, serviceRoutePath, } from "../internal/index.js";
|
|
2
|
+
import { applyAttribution, attributionFor, isRequest, } from "./attribution.js";
|
|
3
|
+
import { getEveryAppUser } from "./everyApp.js";
|
|
4
|
+
import { BINDING_ORIGIN, gatewayBinding, } from "./gatewayFetch.js";
|
|
5
|
+
import { GatewayRefusalError, gatewayRefusal } from "./refusal.js";
|
|
6
|
+
/**
|
|
7
|
+
* The organization's members who can open the app now, through the
|
|
8
|
+
* gateway's `members` capability: people deactivated, removed from the
|
|
9
|
+
* organization or (for an organization's own app) not granted the app are
|
|
10
|
+
* left out. Read it when acting for members who aren't in a request, such
|
|
11
|
+
* as choosing whom a scheduled report goes to, instead of a copy the app
|
|
12
|
+
* kept. The app lists `capabilities: { members: { access: "read" } }`.
|
|
13
|
+
*
|
|
14
|
+
* Throws {@link GatewayRefusalError} when the gateway won't list
|
|
15
|
+
* (`capability_not_requested`, `integration_agent_not_allowed` for an
|
|
16
|
+
* agent's tool call, ...).
|
|
17
|
+
*
|
|
18
|
+
* In local dev (`everyapp dev`, no binding) there is no directory: a call
|
|
19
|
+
* for a request answers its signed-in user alone, and background work
|
|
20
|
+
* answers no one.
|
|
21
|
+
*/
|
|
22
|
+
export async function listMembers({ env, for: explicitTarget, }) {
|
|
23
|
+
const target = attributionFor(env, explicitTarget, "listMembers");
|
|
24
|
+
const values = env;
|
|
25
|
+
const binding = gatewayBinding(env);
|
|
26
|
+
if (binding) {
|
|
27
|
+
if (typeof binding.fetch !== "function") {
|
|
28
|
+
throw new ConfigurationError("EVERY_APP_GATEWAY is configured but is not a valid service binding.");
|
|
29
|
+
}
|
|
30
|
+
return listThroughGateway(binding, target);
|
|
31
|
+
}
|
|
32
|
+
if (!values["EVERYAPP_DEV"]) {
|
|
33
|
+
throw new ConfigurationError("Members are unavailable: deploy with the EVERY_APP_GATEWAY binding (everyapp deploy), or run under everyapp dev.");
|
|
34
|
+
}
|
|
35
|
+
return listInDev(env, target);
|
|
36
|
+
}
|
|
37
|
+
async function listThroughGateway(binding, target) {
|
|
38
|
+
const headers = new Headers();
|
|
39
|
+
applyAttribution(headers, target);
|
|
40
|
+
const response = await binding.fetch(new Request(`${BINDING_ORIGIN}${serviceRoutePath("members", "list")}`, {
|
|
41
|
+
method: "GET",
|
|
42
|
+
headers,
|
|
43
|
+
}));
|
|
44
|
+
const refusal = await gatewayRefusal(response);
|
|
45
|
+
if (refusal)
|
|
46
|
+
throw new GatewayRefusalError(refusal);
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new GatewayRefusalError({
|
|
49
|
+
status: response.status,
|
|
50
|
+
code: "members_list_failed",
|
|
51
|
+
message: `The EveryApp gateway answered ${response.status} to the members list.`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return (await response.json());
|
|
55
|
+
}
|
|
56
|
+
/** The dev gateway has one person: the request's, if the call is for one. */
|
|
57
|
+
async function listInDev(env, target) {
|
|
58
|
+
const user = target && isRequest(target) ? await getEveryAppUser(target, env) : null;
|
|
59
|
+
if (!user)
|
|
60
|
+
return { members: [] };
|
|
61
|
+
return {
|
|
62
|
+
members: [
|
|
63
|
+
{
|
|
64
|
+
id: user.id,
|
|
65
|
+
email: user.email,
|
|
66
|
+
name: "",
|
|
67
|
+
role: user.orgRole,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nodeServerHandler()` — serve a Node HTTP framework (Express, NestJS, or
|
|
3
|
+
* anything on `node:http`) from an Every App Worker, through workerd's
|
|
4
|
+
* `cloudflare:node` bridge:
|
|
5
|
+
*
|
|
6
|
+
* export default everyApp(
|
|
7
|
+
* nodeServerHandler({
|
|
8
|
+
* boot: async () => {
|
|
9
|
+
* const app = await NestFactory.create(AppModule);
|
|
10
|
+
* await app.listen(0);
|
|
11
|
+
* return { port: app.getHttpServer().address().port };
|
|
12
|
+
* },
|
|
13
|
+
* }),
|
|
14
|
+
* );
|
|
15
|
+
*
|
|
16
|
+
* `everyApp()` verifies the identity before the Node app sees the request,
|
|
17
|
+
* which still carries it as `x-everyapp-identity`. The bridge needs
|
|
18
|
+
* `nodejs_compat` and a compatibility date from 2025-09-01, which every
|
|
19
|
+
* `everyAppWorker()` has. See "Node frameworks" in the README for the rules
|
|
20
|
+
* such a Worker must follow (minify it, no work at module scope).
|
|
21
|
+
*/
|
|
22
|
+
import type { EveryAppHandler } from "./everyApp.js";
|
|
23
|
+
/** A booted Node server: the port its `http.Server` listens on. */
|
|
24
|
+
export interface NodeServer {
|
|
25
|
+
port: number;
|
|
26
|
+
}
|
|
27
|
+
export interface NodeServerHandlerOptions<TEnv> {
|
|
28
|
+
/**
|
|
29
|
+
* Start the server and return the port it listens on, e.g. `await
|
|
30
|
+
* app.listen(0)` and then `app.getHttpServer().address().port`. Port 0
|
|
31
|
+
* lets workerd pick one; a fixed port is still held by the server of the
|
|
32
|
+
* module graph a Vite reload replaced, in the same isolate. Runs once per
|
|
33
|
+
* isolate, on its first request rather than at module scope; requests
|
|
34
|
+
* that arrive meanwhile wait for it, and a failed boot is retried on the
|
|
35
|
+
* next request.
|
|
36
|
+
*/
|
|
37
|
+
boot(env: TEnv): Promise<NodeServer>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A handler for `everyApp()` that hands each request to the Node server
|
|
41
|
+
* `boot` started. On the way back it drops `Set-Cookie` (the gateway owns
|
|
42
|
+
* the session, and strips cookies from what reaches the app), and passes a
|
|
43
|
+
* body Node already encoded (`content-encoding`, e.g. from `compression()`)
|
|
44
|
+
* through as is: the bridge would let workerd encode it again, and clients
|
|
45
|
+
* would get unreadable bytes.
|
|
46
|
+
*/
|
|
47
|
+
export declare function nodeServerHandler<TEnv = Record<string, unknown>>(options: NodeServerHandlerOptions<TEnv>): EveryAppHandler<TEnv>;
|
|
48
|
+
//# sourceMappingURL=nodeServer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nodeServer.d.ts","sourceRoot":"","sources":["../../src/server/nodeServer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD,mEAAmE;AACnE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,wBAAwB,CAAC,IAAI;IAC5C;;;;;;;;OAQG;IACH,IAAI,CAAC,GAAG,EAAE,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CACtC;AAKD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9D,OAAO,EAAE,wBAAwB,CAAC,IAAI,CAAC,GACtC,eAAe,CAAC,IAAI,CAAC,CAiBvB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Statuses whose responses can't carry a body. */
|
|
2
|
+
const NULL_BODY_STATUSES = new Set([101, 204, 205, 304]);
|
|
3
|
+
/**
|
|
4
|
+
* A handler for `everyApp()` that hands each request to the Node server
|
|
5
|
+
* `boot` started. On the way back it drops `Set-Cookie` (the gateway owns
|
|
6
|
+
* the session, and strips cookies from what reaches the app), and passes a
|
|
7
|
+
* body Node already encoded (`content-encoding`, e.g. from `compression()`)
|
|
8
|
+
* through as is: the bridge would let workerd encode it again, and clients
|
|
9
|
+
* would get unreadable bytes.
|
|
10
|
+
*/
|
|
11
|
+
export function nodeServerHandler(options) {
|
|
12
|
+
let booting;
|
|
13
|
+
const server = (env) => {
|
|
14
|
+
if (!booting) {
|
|
15
|
+
const attempt = boot(options, env);
|
|
16
|
+
booting = attempt;
|
|
17
|
+
attempt.catch(() => {
|
|
18
|
+
if (booting === attempt)
|
|
19
|
+
booting = undefined;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return booting;
|
|
23
|
+
};
|
|
24
|
+
return async (request, env) => {
|
|
25
|
+
const { port, handleAsNodeRequest } = await server(env);
|
|
26
|
+
return fromNode(await handleAsNodeRequest(port, request));
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function boot(options, env) {
|
|
30
|
+
// Imported here rather than at the top, so that loading the SDK needs no
|
|
31
|
+
// bridge (Node test runners, Workers on an older compatibility date).
|
|
32
|
+
const { handleAsNodeRequest } = await import("cloudflare:node");
|
|
33
|
+
const { port } = await options.boot(env);
|
|
34
|
+
return { port, handleAsNodeRequest };
|
|
35
|
+
}
|
|
36
|
+
function fromNode(response) {
|
|
37
|
+
const headers = new Headers(response.headers);
|
|
38
|
+
headers.delete("set-cookie");
|
|
39
|
+
return new Response(NULL_BODY_STATUSES.has(response.status) ? null : response.body, {
|
|
40
|
+
status: response.status,
|
|
41
|
+
statusText: response.statusText,
|
|
42
|
+
headers,
|
|
43
|
+
...(headers.has("content-encoding") ? { encodeBody: "manual" } : {}),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** What would make it work, as the Gateway words it. */
|
|
2
|
+
export interface SetupInfo {
|
|
3
|
+
/** The Gateway settings it needs (`["DATAFORSEO_API_KEY"]`); `[]` when ready or not a setting. */
|
|
4
|
+
settings: readonly string[];
|
|
5
|
+
/** One sentence for a person, naming what to set and how; null when ready. */
|
|
6
|
+
setup: string | null;
|
|
7
|
+
/** The command an admin runs (`pnpm everyapp configure crm`), or null. */
|
|
8
|
+
command: string | null;
|
|
9
|
+
}
|
|
10
|
+
/** A refusal the AppGateway binding made itself, not the provider. */
|
|
11
|
+
export interface GatewayRefusal extends SetupInfo {
|
|
12
|
+
status: number;
|
|
13
|
+
/** e.g. `capability_not_requested`, `integration_not_configured`. */
|
|
14
|
+
code: string;
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
17
|
+
/** The refusal codes that mean no model is set up for the call. */
|
|
18
|
+
export declare const AI_NOT_CONFIGURED_CODES: readonly string[];
|
|
19
|
+
/**
|
|
20
|
+
* A refusal the AppGateway binding made, thrown: what `sendEmail()` throws
|
|
21
|
+
* when the gateway won't send (`email_sender_not_allowed`,
|
|
22
|
+
* `email_budget_exhausted`, ...). The thrown twin of {@link gatewayRefusal}.
|
|
23
|
+
*/
|
|
24
|
+
export declare class GatewayRefusalError extends Error implements GatewayRefusal {
|
|
25
|
+
readonly status: number;
|
|
26
|
+
readonly code: string;
|
|
27
|
+
readonly settings: readonly string[];
|
|
28
|
+
readonly setup: string | null;
|
|
29
|
+
readonly command: string | null;
|
|
30
|
+
constructor({ status, code, message, settings, setup, command, }: Pick<GatewayRefusal, "status" | "code" | "message"> & Partial<SetupInfo>);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The gateway's own refusal in `response`, or null when the response is the
|
|
34
|
+
* provider's answer (which may be an error of its own). Reads the body only
|
|
35
|
+
* for a refusal, so call it before reading the body yourself.
|
|
36
|
+
*/
|
|
37
|
+
export declare function gatewayRefusal(response: Response): Promise<GatewayRefusal | null>;
|
|
38
|
+
/**
|
|
39
|
+
* The Gateway's refusal behind a thrown error, or null when it isn't one:
|
|
40
|
+
* a {@link GatewayRefusalError} (`sendEmail()`, `listMembers()`, `emit()`),
|
|
41
|
+
* an OpenAI SDK `APIError` or an ai-sdk `APICallError` for a call the
|
|
42
|
+
* Gateway refused (also inside ai-sdk's `RetryError` or an error's
|
|
43
|
+
* `cause`), or the SDK's local-development `ConfigurationError` for a
|
|
44
|
+
* missing key, which answers as the deployed refusal would
|
|
45
|
+
* (`ai_provider_not_configured`, status 503, the `.dev.vars` names).
|
|
46
|
+
*/
|
|
47
|
+
export declare function refusalFromError(error: unknown): GatewayRefusal | null;
|
|
48
|
+
/**
|
|
49
|
+
* Whether `error` (or a refusal already read) means no model is set up:
|
|
50
|
+
* `ai_provider_not_configured` or `ai_default_not_configured`, from the
|
|
51
|
+
* Gateway or, in local development, from the SDK with no key in
|
|
52
|
+
* `.dev.vars`. Show `refusalFromError(error).setup` to the person.
|
|
53
|
+
*/
|
|
54
|
+
export declare function isAiNotConfigured(error: unknown): boolean;
|
|
55
|
+
//# sourceMappingURL=refusal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refusal.d.ts","sourceRoot":"","sources":["../../src/server/refusal.ts"],"names":[],"mappings":"AAgBA,wDAAwD;AACxD,MAAM,WAAW,SAAS;IACxB,kGAAkG;IAClG,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,8EAA8E;IAC9E,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,0EAA0E;IAC1E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,sEAAsE;AACtE,MAAM,WAAW,cAAe,SAAQ,SAAS;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,mEAAmE;AACnE,eAAO,MAAM,uBAAuB,EAAE,SAAS,MAAM,EAGpD,CAAC;AAEF;;;;GAIG;AACH,qBAAa,mBAAoB,SAAQ,KAAM,YAAW,cAAc;IACtE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;gBAEpB,EACV,MAAM,EACN,IAAI,EACJ,OAAO,EACP,QAAa,EACb,KAAY,EACZ,OAAc,GACf,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC;CAS5E;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAKhC;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,GAAG,IAAI,CAWtE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAGzD"}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type SendEmailMessage, type SendEmailResult } from "../internal/index.js";
|
|
2
|
+
import { type GatewayCallFor } from "./attribution.js";
|
|
3
|
+
export interface SendEmailOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The Worker env: the EVERY_APP_GATEWAY binding when deployed; in local
|
|
6
|
+
* dev EVERYAPP_DEV, EVERYAPP_EMAIL_SENDERS in `.dev.vars` for the senders
|
|
7
|
+
* a `from` may name, and optionally EMAIL_REST_API_TOKEN,
|
|
8
|
+
* CLOUDFLARE_ACCOUNT_ID and EMAIL_FROM there to really send.
|
|
9
|
+
*/
|
|
10
|
+
env: object;
|
|
11
|
+
/**
|
|
12
|
+
* Whom the mail is for: the inbound request (its user), or
|
|
13
|
+
* `{ background: true, organizationId }` for work no person started (an
|
|
14
|
+
* `email()` handler, a cron, a Workflow). An organization's own app may
|
|
15
|
+
* omit it for background work; a shared app's Worker must pass one or
|
|
16
|
+
* the other ({@link AttributionError}).
|
|
17
|
+
*/
|
|
18
|
+
for?: GatewayCallFor;
|
|
19
|
+
message: SendEmailMessage;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Send an email through the gateway's `email` capability: its sender
|
|
23
|
+
* (EMAIL_FROM) unless `message.from` names an address the installation's
|
|
24
|
+
* senders allow (`everyapp email set --sender`; in dev, `.dev.vars`'
|
|
25
|
+
* EVERYAPP_EMAIL_SENDERS), within the gateway's daily budgets. The app
|
|
26
|
+
* lists `capabilities: { email: { access: "write" } }`.
|
|
27
|
+
*
|
|
28
|
+
* Throws {@link GatewayRefusalError} when the gateway won't send
|
|
29
|
+
* (`email_sender_not_allowed`, `email_budget_exhausted`,
|
|
30
|
+
* `integration_not_configured`, ...), with the code and a message to show.
|
|
31
|
+
*
|
|
32
|
+
* In local dev (`everyapp dev`, no binding) the message is checked the way
|
|
33
|
+
* the gateway checks it, then printed and answered `{ status: "logged" }`;
|
|
34
|
+
* with EMAIL_REST_API_TOKEN, CLOUDFLARE_ACCOUNT_ID and EMAIL_FROM in
|
|
35
|
+
* `.dev.vars` it goes out for real through Email Sending's REST API, from
|
|
36
|
+
* the developer's own account.
|
|
37
|
+
*/
|
|
38
|
+
export declare function sendEmail({ env, for: explicitTarget, message, }: SendEmailOptions): Promise<SendEmailResult>;
|
|
39
|
+
//# sourceMappingURL=sendEmail.d.ts.map
|
|
@@ -0,0 +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;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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { ConfigurationError, EMAIL_SENDERS_ENV, emailDomain, emailProviderRefusal, emailRestBody, emailRestUrl, emailSenderRefusal, parseEmailList, parseEmailSender, parseSendEmailMessage, readEmailRestResponse, senderAllowed, serviceRoutePath, } from "../internal/index.js";
|
|
2
|
+
import { applyAttribution, attributionFor, } from "./attribution.js";
|
|
3
|
+
import { BINDING_ORIGIN, gatewayBinding, } from "./gatewayFetch.js";
|
|
4
|
+
import { GatewayRefusalError, gatewayRefusal } from "./refusal.js";
|
|
5
|
+
/**
|
|
6
|
+
* Send an email through the gateway's `email` capability: its sender
|
|
7
|
+
* (EMAIL_FROM) unless `message.from` names an address the installation's
|
|
8
|
+
* senders allow (`everyapp email set --sender`; in dev, `.dev.vars`'
|
|
9
|
+
* EVERYAPP_EMAIL_SENDERS), within the gateway's daily budgets. The app
|
|
10
|
+
* lists `capabilities: { email: { access: "write" } }`.
|
|
11
|
+
*
|
|
12
|
+
* Throws {@link GatewayRefusalError} when the gateway won't send
|
|
13
|
+
* (`email_sender_not_allowed`, `email_budget_exhausted`,
|
|
14
|
+
* `integration_not_configured`, ...), with the code and a message to show.
|
|
15
|
+
*
|
|
16
|
+
* In local dev (`everyapp dev`, no binding) the message is checked the way
|
|
17
|
+
* the gateway checks it, then printed and answered `{ status: "logged" }`;
|
|
18
|
+
* with EMAIL_REST_API_TOKEN, CLOUDFLARE_ACCOUNT_ID and EMAIL_FROM in
|
|
19
|
+
* `.dev.vars` it goes out for real through Email Sending's REST API, from
|
|
20
|
+
* the developer's own account.
|
|
21
|
+
*/
|
|
22
|
+
export async function sendEmail({ env, for: explicitTarget, message, }) {
|
|
23
|
+
const target = attributionFor(env, explicitTarget, "sendEmail");
|
|
24
|
+
const values = env;
|
|
25
|
+
const binding = gatewayBinding(env);
|
|
26
|
+
if (binding) {
|
|
27
|
+
if (typeof binding.fetch !== "function") {
|
|
28
|
+
throw new ConfigurationError("EVERY_APP_GATEWAY is configured but is not a valid service binding.");
|
|
29
|
+
}
|
|
30
|
+
return sendThroughGateway(binding, target, message);
|
|
31
|
+
}
|
|
32
|
+
if (!values["EVERYAPP_DEV"]) {
|
|
33
|
+
throw new ConfigurationError("Email is unavailable: deploy with the EVERY_APP_GATEWAY binding (everyapp deploy), or run under everyapp dev.");
|
|
34
|
+
}
|
|
35
|
+
return sendInDev(values, message);
|
|
36
|
+
}
|
|
37
|
+
async function sendThroughGateway(binding, target, message) {
|
|
38
|
+
const headers = new Headers({ "content-type": "application/json" });
|
|
39
|
+
applyAttribution(headers, target);
|
|
40
|
+
const response = await binding.fetch(new Request(`${BINDING_ORIGIN}${serviceRoutePath("email", "send")}`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers,
|
|
43
|
+
body: JSON.stringify(message),
|
|
44
|
+
}));
|
|
45
|
+
const refusal = await gatewayRefusal(response);
|
|
46
|
+
if (refusal)
|
|
47
|
+
throw new GatewayRefusalError(refusal);
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new GatewayRefusalError({
|
|
50
|
+
status: response.status,
|
|
51
|
+
code: "email_send_failed",
|
|
52
|
+
message: `The EveryApp gateway answered ${response.status} to the send.`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return (await response.json());
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* What the gateway would do, with no gateway: the same checks and sender
|
|
59
|
+
* rule, then the developer's own Email Sending account or the console.
|
|
60
|
+
*/
|
|
61
|
+
async function sendInDev(values, input) {
|
|
62
|
+
const parsed = parseSendEmailMessage(input);
|
|
63
|
+
if (!parsed.ok) {
|
|
64
|
+
throw new GatewayRefusalError({
|
|
65
|
+
status: 400,
|
|
66
|
+
code: "email_invalid_message",
|
|
67
|
+
message: parsed.problem,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const message = parsed.message;
|
|
71
|
+
if (message.from &&
|
|
72
|
+
!senderAllowed(message.from.address, parseEmailList(values[EMAIL_SENDERS_ENV]))) {
|
|
73
|
+
throw new GatewayRefusalError({
|
|
74
|
+
status: 403,
|
|
75
|
+
code: "email_sender_not_allowed",
|
|
76
|
+
message: emailSenderRefusal(message.from.address, "dev"),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const token = stringValue(values["EMAIL_REST_API_TOKEN"]);
|
|
80
|
+
const accountId = stringValue(values["CLOUDFLARE_ACCOUNT_ID"]);
|
|
81
|
+
const configuredFrom = stringValue(values["EMAIL_FROM"]);
|
|
82
|
+
// The gateway needs a name for its sender; a developer's address alone will do.
|
|
83
|
+
const sender = configuredFrom
|
|
84
|
+
? (parseEmailSender(configuredFrom, stringValue(values["EMAIL_FROM_NAME"])) ?? { address: configuredFrom })
|
|
85
|
+
: null;
|
|
86
|
+
if (token && accountId && sender) {
|
|
87
|
+
return sendWithRest(message, message.from ?? sender, token, accountId);
|
|
88
|
+
}
|
|
89
|
+
const recipients = message.to.length + message.cc.length + message.bcc.length;
|
|
90
|
+
console.info(`[everyapp dev] email to ${recipients} recipient${recipients === 1 ? "" : "s"} subject ${JSON.stringify(message.subject)} (not sent: set EMAIL_REST_API_TOKEN, CLOUDFLARE_ACCOUNT_ID and EMAIL_FROM in .dev.vars to send)`);
|
|
91
|
+
return { status: "logged", messageId: `dev-${crypto.randomUUID()}` };
|
|
92
|
+
}
|
|
93
|
+
async function sendWithRest(message, from, token, accountId) {
|
|
94
|
+
const response = await fetch(emailRestUrl(accountId), {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: {
|
|
97
|
+
authorization: `Bearer ${token}`,
|
|
98
|
+
"content-type": "application/json",
|
|
99
|
+
},
|
|
100
|
+
body: JSON.stringify(emailRestBody({ ...message, from })),
|
|
101
|
+
});
|
|
102
|
+
const body = await response.json().catch(() => null);
|
|
103
|
+
const result = readEmailRestResponse(response.status, body);
|
|
104
|
+
if (!result.ok) {
|
|
105
|
+
throw new GatewayRefusalError(emailProviderRefusal(result.providerCode, emailDomain(from.address)));
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
status: result.status,
|
|
109
|
+
...(result.messageId ? { messageId: result.messageId } : {}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function stringValue(value) {
|
|
113
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
114
|
+
}
|