@webiny/api-event-handler-standalone 6.6.0-alpha.4
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 +11 -0
- package/bulkActions/bulkActionsServer.d.ts +2 -0
- package/bulkActions/bulkActionsServer.js +31 -0
- package/bulkActions/bulkActionsServer.js.map +1 -0
- package/createWebinyApiHandler.d.ts +32 -0
- package/createWebinyApiHandler.js +52 -0
- package/createWebinyApiHandler.js.map +1 -0
- package/handlers/NodeHttpIdentityLoaderDecorator.d.ts +32 -0
- package/handlers/NodeHttpIdentityLoaderDecorator.js +45 -0
- package/handlers/NodeHttpIdentityLoaderDecorator.js.map +1 -0
- package/handlers/NodeHttpTenantLoaderDecorator.d.ts +24 -0
- package/handlers/NodeHttpTenantLoaderDecorator.js +29 -0
- package/handlers/NodeHttpTenantLoaderDecorator.js.map +1 -0
- package/index.d.ts +4 -0
- package/index.js +3 -0
- package/package.json +52 -0
- package/scheduler/ScheduledActionRecoverRoute.d.ts +34 -0
- package/scheduler/ScheduledActionRecoverRoute.js +90 -0
- package/scheduler/ScheduledActionRecoverRoute.js.map +1 -0
- package/scheduler/ScheduledActionRunRoute.d.ts +31 -0
- package/scheduler/ScheduledActionRunRoute.js +75 -0
- package/scheduler/ScheduledActionRunRoute.js.map +1 -0
- package/scheduler/abstractions/InternalToken.d.ts +14 -0
- package/scheduler/abstractions/InternalToken.js +5 -0
- package/scheduler/abstractions/InternalToken.js.map +1 -0
- package/scheduler/abstractions/SchedulerSingleton.d.ts +10 -0
- package/scheduler/abstractions/SchedulerSingleton.js +5 -0
- package/scheduler/abstractions/SchedulerSingleton.js.map +1 -0
- package/scheduler/schedulerServer.d.ts +20 -0
- package/scheduler/schedulerServer.js +81 -0
- package/scheduler/schedulerServer.js.map +1 -0
- package/websockets/createWebsocketsAuthenticator.d.ts +24 -0
- package/websockets/createWebsocketsAuthenticator.js +22 -0
- package/websockets/createWebsocketsAuthenticator.js.map +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Webiny
|
|
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,11 @@
|
|
|
1
|
+
# @webiny/api-event-handler-standalone
|
|
2
|
+
|
|
3
|
+
> [!NOTE]
|
|
4
|
+
> This package is part of the [Webiny](https://www.webiny.com) monorepo.
|
|
5
|
+
> It’s **included in every Webiny project by default** and is not meant to be used as a standalone package.
|
|
6
|
+
|
|
7
|
+
📘 **Documentation:** [https://www.webiny.com/docs](https://www.webiny.com/docs)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
_This README file is automatically generated during the publish process._
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { BulkActionsInternalToken } from "@webiny/api-headless-cms-bulk-actions-standalone";
|
|
2
|
+
const BULK_ACTIONS_HEADER = "x-webiny-bulk-actions-token";
|
|
3
|
+
const serverBase = ()=>`http://localhost:${process.env.PORT || "3002"}`;
|
|
4
|
+
const EMPTY_TRASH_INTERVAL_MS = 21600000;
|
|
5
|
+
function startBulkActionsServer(rootContainer) {
|
|
6
|
+
const token = rootContainer.resolve(BulkActionsInternalToken).value;
|
|
7
|
+
const trigger = async ()=>{
|
|
8
|
+
try {
|
|
9
|
+
const res = await fetch(`${serverBase()}/empty-trash-bins`, {
|
|
10
|
+
method: "POST",
|
|
11
|
+
headers: {
|
|
12
|
+
"content-type": "application/json",
|
|
13
|
+
[BULK_ACTIONS_HEADER]: token
|
|
14
|
+
},
|
|
15
|
+
body: JSON.stringify({})
|
|
16
|
+
});
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
const body = await res.text().catch(()=>"");
|
|
19
|
+
console.error(`[bulk-actions] empty-trash-bins returned HTTP ${res.status}: ${body}`);
|
|
20
|
+
}
|
|
21
|
+
} catch (err) {
|
|
22
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
23
|
+
console.error(`[bulk-actions] failed to reach empty-trash-bins route: ${message}`);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
setTimeout(trigger, 5000);
|
|
27
|
+
setInterval(trigger, EMPTY_TRASH_INTERVAL_MS);
|
|
28
|
+
}
|
|
29
|
+
export { startBulkActionsServer };
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=bulkActionsServer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bulkActions/bulkActionsServer.js","sources":["../../src/bulkActions/bulkActionsServer.ts"],"sourcesContent":["import type { Container } from \"@webiny/di\";\nimport { BulkActionsInternalToken } from \"@webiny/api-headless-cms-bulk-actions-standalone\";\n\nconst BULK_ACTIONS_HEADER = \"x-webiny-bulk-actions-token\";\nconst serverBase = () => `http://localhost:${process.env.PORT || \"3002\"}`;\n\n// Default: trigger every 6 hours (in milliseconds).\nconst EMPTY_TRASH_INTERVAL_MS = 6 * 60 * 60 * 1000;\n\nexport function startBulkActionsServer(rootContainer: Container): void {\n const token = rootContainer.resolve(BulkActionsInternalToken).value;\n\n const trigger = async () => {\n try {\n const res = await fetch(`${serverBase()}/empty-trash-bins`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", [BULK_ACTIONS_HEADER]: token },\n body: JSON.stringify({})\n });\n if (!res.ok) {\n const body = await res.text().catch(() => \"\");\n console.error(\n `[bulk-actions] empty-trash-bins returned HTTP ${res.status}: ${body}`\n );\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.error(`[bulk-actions] failed to reach empty-trash-bins route: ${message}`);\n }\n };\n\n // Deferred initial trigger + periodic interval.\n setTimeout(trigger, 5000);\n setInterval(trigger, EMPTY_TRASH_INTERVAL_MS);\n}\n"],"names":["BULK_ACTIONS_HEADER","serverBase","process","EMPTY_TRASH_INTERVAL_MS","startBulkActionsServer","rootContainer","token","BulkActionsInternalToken","trigger","res","fetch","JSON","body","console","err","message","Error","String","setTimeout","setInterval"],"mappings":";AAGA,MAAMA,sBAAsB;AAC5B,MAAMC,aAAa,IAAM,CAAC,iBAAiB,EAAEC,QAAQ,GAAG,CAAC,IAAI,IAAI,QAAQ;AAGzE,MAAMC,0BAA0B;AAEzB,SAASC,uBAAuBC,aAAwB;IAC3D,MAAMC,QAAQD,cAAc,OAAO,CAACE,0BAA0B,KAAK;IAEnE,MAAMC,UAAU;QACZ,IAAI;YACA,MAAMC,MAAM,MAAMC,MAAM,GAAGT,aAAa,iBAAiB,CAAC,EAAE;gBACxD,QAAQ;gBACR,SAAS;oBAAE,gBAAgB;oBAAoB,CAACD,oBAAoB,EAAEM;gBAAM;gBAC5E,MAAMK,KAAK,SAAS,CAAC,CAAC;YAC1B;YACA,IAAI,CAACF,IAAI,EAAE,EAAE;gBACT,MAAMG,OAAO,MAAMH,IAAI,IAAI,GAAG,KAAK,CAAC,IAAM;gBAC1CI,QAAQ,KAAK,CACT,CAAC,8CAA8C,EAAEJ,IAAI,MAAM,CAAC,EAAE,EAAEG,MAAM;YAE9E;QACJ,EAAE,OAAOE,KAAK;YACV,MAAMC,UAAUD,eAAeE,QAAQF,IAAI,OAAO,GAAGG,OAAOH;YAC5DD,QAAQ,KAAK,CAAC,CAAC,uDAAuD,EAAEE,SAAS;QACrF;IACJ;IAGAG,WAAWV,SAAS;IACpBW,YAAYX,SAASL;AACzB"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webiny API handler for the standalone Node HTTP server transport — storage-agnostic BASE.
|
|
3
|
+
*
|
|
4
|
+
* The ROOT container wires the Node HTTP transport (NodeHttpFeature = event type + router + HttpFeature)
|
|
5
|
+
* plus the auth/tenant loader decorators (extract token / x-tenant from the IncomingMessage → shared
|
|
6
|
+
* RequestIdentityLoader / RequestTenantLoader). The per-request feature stack is the transport-agnostic
|
|
7
|
+
* `registerApiRequestStack` from `@webiny/api-event-handler-core` — the SAME stack the AWS handler uses.
|
|
8
|
+
* The realtime transport hook installs the server WebSockets transport (vs AWS's API Gateway Management
|
|
9
|
+
* API) with a SINGLE-PROCESS, in-process implementation. Background tasks and the Bree scheduler are
|
|
10
|
+
* ROOT-container singletons (below) — mirroring how the AWS handler registers background tasks at root,
|
|
11
|
+
* and the WebSockets connection manager — rather than per-request transports; the scheduler fires timers
|
|
12
|
+
* in-process (vs AWS's EventBridge). The storage variant (and identity provider) is injected via
|
|
13
|
+
* `registerRootStorage`.
|
|
14
|
+
*
|
|
15
|
+
* The identity provider (e.g. `@webiny/self-hosted-auth`'s JWT IdP) must be registered by the variant
|
|
16
|
+
* in `registerRootStorage`, so the RequestIdentityLoader driven by the identity decorator can resolve it.
|
|
17
|
+
*/
|
|
18
|
+
import type { Container } from "@webiny/di";
|
|
19
|
+
import { registerExtensions } from "@webiny/handler";
|
|
20
|
+
export interface CreateWebinyApiHandlerConfig {
|
|
21
|
+
/**
|
|
22
|
+
* Project-defined extensions, applied at register() time.
|
|
23
|
+
*/
|
|
24
|
+
extensions: () => Parameters<typeof registerExtensions>[1];
|
|
25
|
+
/**
|
|
26
|
+
* Register ALL root-container storage for this deployment (the DB feature + CMS storage ops +
|
|
27
|
+
* registries) AND the identity provider. Unlike the AWS base, the server base bakes in no DB or
|
|
28
|
+
* IdP assumption — the variant supplies the complete storage + auth wiring.
|
|
29
|
+
*/
|
|
30
|
+
registerRootStorage: (container: Container) => void | Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
export declare function createWebinyApiHandler(config: CreateWebinyApiHandlerConfig): Promise<import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { NodeHttpFeature, createServerHandler } from "@webiny/event-handler-standalone";
|
|
2
|
+
import { registerApiRequestStack } from "@webiny/api-event-handler-core";
|
|
3
|
+
import { NodeWsAdapter, ServerConnectionManager, WebsocketsConnectionManager, WebsocketsStandaloneFeature, attachWebsocketsServer } from "@webiny/api-websockets-standalone";
|
|
4
|
+
import { BackgroundTasksStandaloneFeature } from "@webiny/background-tasks-standalone";
|
|
5
|
+
import { FileManagerStandaloneFeature } from "@webiny/api-file-manager-standalone";
|
|
6
|
+
import { registerSchedulerServer, startSchedulerServer } from "./scheduler/schedulerServer.js";
|
|
7
|
+
import { startBulkActionsServer } from "./bulkActions/bulkActionsServer.js";
|
|
8
|
+
import { NodeHttpIdentityLoaderDecorator } from "./handlers/NodeHttpIdentityLoaderDecorator.js";
|
|
9
|
+
import { NodeHttpTenantLoaderDecorator } from "./handlers/NodeHttpTenantLoaderDecorator.js";
|
|
10
|
+
import { createWebsocketsAuthenticator } from "./websockets/createWebsocketsAuthenticator.js";
|
|
11
|
+
import { EmptyTrashBinRouteFeature } from "@webiny/api-headless-cms-bulk-actions-standalone";
|
|
12
|
+
function createWebinyApiHandler(config) {
|
|
13
|
+
return createServerHandler({
|
|
14
|
+
root: async (rootContainer)=>{
|
|
15
|
+
NodeHttpFeature.register(rootContainer);
|
|
16
|
+
rootContainer.registerDecorator(NodeHttpIdentityLoaderDecorator);
|
|
17
|
+
rootContainer.registerDecorator(NodeHttpTenantLoaderDecorator);
|
|
18
|
+
await config.registerRootStorage(rootContainer);
|
|
19
|
+
rootContainer.register(ServerConnectionManager).inSingletonScope();
|
|
20
|
+
rootContainer.register(NodeWsAdapter).inSingletonScope();
|
|
21
|
+
BackgroundTasksStandaloneFeature.register(rootContainer);
|
|
22
|
+
registerSchedulerServer(rootContainer);
|
|
23
|
+
EmptyTrashBinRouteFeature.register(rootContainer);
|
|
24
|
+
},
|
|
25
|
+
child: async (container)=>{
|
|
26
|
+
await registerApiRequestStack(container, {
|
|
27
|
+
extensions: config.extensions,
|
|
28
|
+
transports: {
|
|
29
|
+
realtime: (requestContainer)=>{
|
|
30
|
+
WebsocketsStandaloneFeature.register(requestContainer);
|
|
31
|
+
},
|
|
32
|
+
fileManager: (requestContainer)=>{
|
|
33
|
+
FileManagerStandaloneFeature.register(requestContainer);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
onServer: async (server, rootContainer)=>{
|
|
39
|
+
const websockets = attachWebsocketsServer({
|
|
40
|
+
server,
|
|
41
|
+
connectionManager: rootContainer.resolve(WebsocketsConnectionManager),
|
|
42
|
+
authenticate: createWebsocketsAuthenticator(rootContainer, config.extensions)
|
|
43
|
+
});
|
|
44
|
+
await websockets.start();
|
|
45
|
+
await startSchedulerServer(rootContainer);
|
|
46
|
+
startBulkActionsServer(rootContainer);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export { createWebinyApiHandler };
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=createWebinyApiHandler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createWebinyApiHandler.js","sources":["../src/createWebinyApiHandler.ts"],"sourcesContent":["/**\n * Webiny API handler for the standalone Node HTTP server transport — storage-agnostic BASE.\n *\n * The ROOT container wires the Node HTTP transport (NodeHttpFeature = event type + router + HttpFeature)\n * plus the auth/tenant loader decorators (extract token / x-tenant from the IncomingMessage → shared\n * RequestIdentityLoader / RequestTenantLoader). The per-request feature stack is the transport-agnostic\n * `registerApiRequestStack` from `@webiny/api-event-handler-core` — the SAME stack the AWS handler uses.\n * The realtime transport hook installs the server WebSockets transport (vs AWS's API Gateway Management\n * API) with a SINGLE-PROCESS, in-process implementation. Background tasks and the Bree scheduler are\n * ROOT-container singletons (below) — mirroring how the AWS handler registers background tasks at root,\n * and the WebSockets connection manager — rather than per-request transports; the scheduler fires timers\n * in-process (vs AWS's EventBridge). The storage variant (and identity provider) is injected via\n * `registerRootStorage`.\n *\n * The identity provider (e.g. `@webiny/self-hosted-auth`'s JWT IdP) must be registered by the variant\n * in `registerRootStorage`, so the RequestIdentityLoader driven by the identity decorator can resolve it.\n */\nimport type { Container } from \"@webiny/di\";\nimport { createServerHandler, NodeHttpFeature } from \"@webiny/event-handler-standalone\";\nimport { registerExtensions } from \"@webiny/handler\";\nimport { registerApiRequestStack } from \"@webiny/api-event-handler-core\";\nimport {\n ServerConnectionManager,\n NodeWsAdapter,\n WebsocketsStandaloneFeature,\n WebsocketsConnectionManager,\n attachWebsocketsServer\n} from \"@webiny/api-websockets-standalone\";\nimport { BackgroundTasksStandaloneFeature } from \"@webiny/background-tasks-standalone\";\nimport { FileManagerStandaloneFeature } from \"@webiny/api-file-manager-standalone\";\nimport { registerSchedulerServer, startSchedulerServer } from \"~/scheduler/schedulerServer.js\";\nimport { startBulkActionsServer } from \"~/bulkActions/bulkActionsServer.js\";\nimport { NodeHttpIdentityLoaderDecorator } from \"~/handlers/NodeHttpIdentityLoaderDecorator.js\";\nimport { NodeHttpTenantLoaderDecorator } from \"~/handlers/NodeHttpTenantLoaderDecorator.js\";\nimport { createWebsocketsAuthenticator } from \"~/websockets/createWebsocketsAuthenticator.js\";\nimport { EmptyTrashBinRouteFeature } from \"@webiny/api-headless-cms-bulk-actions-standalone\";\n\nexport interface CreateWebinyApiHandlerConfig {\n /**\n * Project-defined extensions, applied at register() time.\n */\n extensions: () => Parameters<typeof registerExtensions>[1];\n /**\n * Register ALL root-container storage for this deployment (the DB feature + CMS storage ops +\n * registries) AND the identity provider. Unlike the AWS base, the server base bakes in no DB or\n * IdP assumption — the variant supplies the complete storage + auth wiring.\n */\n registerRootStorage: (container: Container) => void | Promise<void>;\n}\n\nexport function createWebinyApiHandler(config: CreateWebinyApiHandlerConfig) {\n return createServerHandler({\n root: async rootContainer => {\n // ── Transport (Node HTTP) ──────────────────────────────────\n // NodeHttpFeature registers the event type + HttpFeature (router) + the routing terminal.\n NodeHttpFeature.register(rootContainer);\n\n // ── Tenant + auth (extract → shared load) ──────────────────\n // registerDecorator applies LATER registrations as the OUTER wrapper (whose execute()\n // runs first). TENANT must be established before IDENTITY: API-key authentication resolves\n // the key by tenant partition (ApiKeysRepository reads TenantContext.getTenant()), so\n // identity depends on the tenant; the reverse is not true (RequestTenantLoader has no\n // identity dependency). Register identity first (inner) and tenant last (outer) → tenant\n // runs, then identity, then the router.\n rootContainer.registerDecorator(NodeHttpIdentityLoaderDecorator);\n rootContainer.registerDecorator(NodeHttpTenantLoaderDecorator);\n\n // ── Storage + identity provider (variant-supplied) ─────────\n await config.registerRootStorage(rootContainer);\n\n // ── WebSockets transport (root) ────────────────────────────\n // Live-socket registry + adapter live in the root as singletons: the connection manager\n // is shared between the upgrade acceptor (attachWebsocketsServer, below) and the\n // per-request ServerWebsocketsTransport that sends to those live sockets. The persistent\n // connection registry (ConnectionRegistry) is the storage variant's job (e.g. sql).\n rootContainer.register(ServerConnectionManager).inSingletonScope();\n rootContainer.register(NodeWsAdapter).inSingletonScope();\n\n // ── Background tasks (root) ────────────────────────────────\n // Mirrors the AWS handler registering its background-task transport at root. There is no\n // Step Functions / Lambda re-invocation in a single process, so the transport is in-process:\n // WorkerService (the BackgroundTasks/TaskService dispatch abstraction) runs each triggered\n // task in a Node worker_thread that POSTs back to this server's `/background-task` HTTP route\n // (BackgroundTaskRoute), which runs the task loop (continue/timeout/abort) in-process. Root,\n // not per-request, is load-bearing: the shared InternalToken (registered here as a singleton)\n // gates the route against the worker's callback, so dispatcher and route MUST see the SAME\n // token — a per-request registration would mint a fresh token per request and always 403.\n // The route is an HttpRoute; the per-request HttpRouter collects it via the parent chain.\n BackgroundTasksStandaloneFeature.register(rootContainer);\n\n // ── Scheduler (root) ───────────────────────────────────────\n // The Bree scheduler is a single long-lived instance for ALL tenants, started once at boot\n // (onServer, below) — the counterpart of the WebSockets connection manager, NOT a per-request\n // transport. Registered here as SchedulerService (per-request create/update/delete during\n // mutations manipulate this one live timer set) plus the run/recover HTTP routes + internal\n // token. When a timer fires (outside any request) it POSTs `/scheduled-action-run`, which\n // rebuilds the tenant's request context and executes the action.\n registerSchedulerServer(rootContainer);\n\n // ── Bulk actions (root) ───────────────────────────────────\n // Registers the `/empty-trash-bins` HTTP route + internal token so the periodic trigger\n // (startBulkActionsServer, onServer below) can POST to it after the server is listening.\n EmptyTrashBinRouteFeature.register(rootContainer);\n },\n\n child: async container => {\n // The transport-agnostic per-request stack. The realtime hook installs the server\n // WebSockets transport (overriding the domain's NullWebsocketsTransport); it resolves the\n // shared connection manager + adapter from the root. Scheduler is NOT a per-request\n // transport here — it's a root singleton wired in `root` + `onServer` (see above).\n await registerApiRequestStack(container, {\n extensions: config.extensions,\n // Why hooks (and not just registering the transports ourselves): `.register()` calls\n // are otherwise order-independent — you can register Features in any order, because\n // they only REGISTER, they don't RESOLVE during registration (resolution happens\n // later, in Initializers / SchemaFactories). The one thing that makes order matter is\n // a DEFAULT registration: e.g. `WebsocketsFeature` registers `NullWebsocketsTransport`\n // so the abstraction is always resolvable. Overriding it (last-registration-wins) means\n // our transport MUST be registered AFTER that Feature. These hooks are the seams\n // `registerApiRequestStack` provides for exactly that — each runs right after its Null\n // default, so the override is guaranteed without the caller knowing the internal order.\n transports: {\n // Server WebSockets transport; resolves the shared connection manager + adapter\n // from the root (registered as singletons above).\n realtime: requestContainer => {\n WebsocketsStandaloneFeature.register(requestContainer);\n },\n // File-manager storage transport: local disk. Where AWS uses S3 (+ a separate asset-\n // delivery Lambda), the single-process server stores files on disk and serves them\n // in-process — FileManagerStandaloneFeature registers local asset delivery (overriding\n // the domain's null impls), the upload/multipart HTTP routes, and disk file ops.\n fileManager: requestContainer => {\n FileManagerStandaloneFeature.register(requestContainer);\n }\n }\n });\n },\n\n onServer: async (server, rootContainer) => {\n // Attach the WebSockets upgrade handler to the running HTTP server, backed by the shared\n // (root) connection manager so request-time sends reach the live sockets.\n //\n // Authenticate each WebSocket connection from its `?token` JWT so it's registered under\n // the real identity (targeted server→client sends match by identity id). It's a callback\n // the handler supplies rather than something the WS server resolves itself — see\n // createWebsocketsAuthenticator for why (AuthenticationContext is per-request, unreachable\n // from the root container the WS server is attached to).\n const websockets = attachWebsocketsServer({\n server,\n connectionManager: rootContainer.resolve(WebsocketsConnectionManager),\n authenticate: createWebsocketsAuthenticator(rootContainer, config.extensions)\n });\n await websockets.start();\n\n // Start the in-process scheduler timers, then re-arm persisted schedules (deferred until\n // the server is listening — see startSchedulerServer).\n await startSchedulerServer(rootContainer);\n\n // Start the periodic empty-trash-bin trigger (deferred until the server is listening).\n startBulkActionsServer(rootContainer);\n }\n });\n}\n"],"names":["createWebinyApiHandler","config","createServerHandler","rootContainer","NodeHttpFeature","NodeHttpIdentityLoaderDecorator","NodeHttpTenantLoaderDecorator","ServerConnectionManager","NodeWsAdapter","BackgroundTasksStandaloneFeature","registerSchedulerServer","EmptyTrashBinRouteFeature","container","registerApiRequestStack","requestContainer","WebsocketsStandaloneFeature","FileManagerStandaloneFeature","server","websockets","attachWebsocketsServer","WebsocketsConnectionManager","createWebsocketsAuthenticator","startSchedulerServer","startBulkActionsServer"],"mappings":";;;;;;;;;;;AAkDO,SAASA,uBAAuBC,MAAoC;IACvE,OAAOC,oBAAoB;QACvB,MAAM,OAAMC;YAGRC,gBAAgB,QAAQ,CAACD;YASzBA,cAAc,iBAAiB,CAACE;YAChCF,cAAc,iBAAiB,CAACG;YAGhC,MAAML,OAAO,mBAAmB,CAACE;YAOjCA,cAAc,QAAQ,CAACI,yBAAyB,gBAAgB;YAChEJ,cAAc,QAAQ,CAACK,eAAe,gBAAgB;YAYtDC,iCAAiC,QAAQ,CAACN;YAS1CO,wBAAwBP;YAKxBQ,0BAA0B,QAAQ,CAACR;QACvC;QAEA,OAAO,OAAMS;YAKT,MAAMC,wBAAwBD,WAAW;gBACrC,YAAYX,OAAO,UAAU;gBAU7B,YAAY;oBAGR,UAAUa,CAAAA;wBACNC,4BAA4B,QAAQ,CAACD;oBACzC;oBAKA,aAAaA,CAAAA;wBACTE,6BAA6B,QAAQ,CAACF;oBAC1C;gBACJ;YACJ;QACJ;QAEA,UAAU,OAAOG,QAAQd;YASrB,MAAMe,aAAaC,uBAAuB;gBACtCF;gBACA,mBAAmBd,cAAc,OAAO,CAACiB;gBACzC,cAAcC,8BAA8BlB,eAAeF,OAAO,UAAU;YAChF;YACA,MAAMiB,WAAW,KAAK;YAItB,MAAMI,qBAAqBnB;YAG3BoB,uBAAuBpB;QAC3B;IACJ;AACJ"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { IncomingMessage } from "node:http";
|
|
2
|
+
import { NodeHttpEventHandler } from "@webiny/event-handler-standalone";
|
|
3
|
+
import { RawAuthToken } from "@webiny/api-core/features/requestContext/index.js";
|
|
4
|
+
import type { IRequestIdentityLoader } from "@webiny/api-core/features/requestContext/abstractions.js";
|
|
5
|
+
import type { EventContext, NextFunction } from "@webiny/event-handler-core";
|
|
6
|
+
/**
|
|
7
|
+
* EXTRACT (transport-specific): reads the auth token from a Node `IncomingMessage` —
|
|
8
|
+
* `x-webiny-authorization` first, then the bearer `Authorization` header, then the `wby-id-token`
|
|
9
|
+
* cookie — into RawAuthToken, then invokes the shared LOAD step (RequestIdentityLoader) which
|
|
10
|
+
* authenticates it (via the registered identity provider, e.g. the self-hosted JWT IdP) and sets
|
|
11
|
+
* IdentityContext.
|
|
12
|
+
*
|
|
13
|
+
* `x-webiny-authorization` exists for AWS parity: behind CloudFront with Origin Access Control, SigV4
|
|
14
|
+
* occupies the `Authorization` header, so clients that may traverse such an origin send the token in
|
|
15
|
+
* that header instead. Standalone has no such constraint, but it accepts the same header so one
|
|
16
|
+
* client works against both deployments.
|
|
17
|
+
*
|
|
18
|
+
* Node mirror of ApiGatewayIdentityLoaderDecorator. Registered BEFORE the tenant loader so identity
|
|
19
|
+
* is established before tenant.
|
|
20
|
+
*/
|
|
21
|
+
declare class NodeHttpIdentityLoaderDecoratorImpl implements NodeHttpEventHandler.Interface {
|
|
22
|
+
private rawAuthToken;
|
|
23
|
+
private identityLoader;
|
|
24
|
+
private decoratee;
|
|
25
|
+
constructor(rawAuthToken: RawAuthToken.Interface, identityLoader: IRequestIdentityLoader, decoratee: NodeHttpEventHandler.Interface);
|
|
26
|
+
execute(ctx: EventContext<IncomingMessage>, next: NextFunction): Promise<any>;
|
|
27
|
+
private extractToken;
|
|
28
|
+
}
|
|
29
|
+
export declare const NodeHttpIdentityLoaderDecorator: typeof NodeHttpIdentityLoaderDecoratorImpl & {
|
|
30
|
+
__abstraction: import("@webiny/di").Abstraction<import("@webiny/event-handler-standalone").INodeHttpEventHandler>;
|
|
31
|
+
};
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { NodeHttpEventHandler } from "@webiny/event-handler-standalone";
|
|
2
|
+
import { RawAuthToken, RequestIdentityLoader } from "@webiny/api-core/features/requestContext/index.js";
|
|
3
|
+
function headerValue(value) {
|
|
4
|
+
if (Array.isArray(value)) return value[0] ?? "";
|
|
5
|
+
return value ?? "";
|
|
6
|
+
}
|
|
7
|
+
function parseCookieHeader(cookieHeader) {
|
|
8
|
+
return cookieHeader.split(";").reduce((acc, pair)=>{
|
|
9
|
+
const idx = pair.indexOf("=");
|
|
10
|
+
if (idx > 0) acc[pair.slice(0, idx).trim()] = decodeURIComponent(pair.slice(idx + 1).trim());
|
|
11
|
+
return acc;
|
|
12
|
+
}, {});
|
|
13
|
+
}
|
|
14
|
+
class NodeHttpIdentityLoaderDecoratorImpl {
|
|
15
|
+
constructor(rawAuthToken, identityLoader, decoratee){
|
|
16
|
+
this.rawAuthToken = rawAuthToken;
|
|
17
|
+
this.identityLoader = identityLoader;
|
|
18
|
+
this.decoratee = decoratee;
|
|
19
|
+
}
|
|
20
|
+
async execute(ctx, next) {
|
|
21
|
+
this.rawAuthToken.set(this.extractToken(ctx.event));
|
|
22
|
+
await this.identityLoader.establish();
|
|
23
|
+
return this.decoratee.execute(ctx, next);
|
|
24
|
+
}
|
|
25
|
+
extractToken(event) {
|
|
26
|
+
const headers = event?.headers;
|
|
27
|
+
if (!headers) return null;
|
|
28
|
+
const webinyAuth = headerValue(headers["x-webiny-authorization"] ?? headers["X-Webiny-Authorization"]).replace(/^Bearer\s+/i, "");
|
|
29
|
+
if (webinyAuth) return webinyAuth;
|
|
30
|
+
const bearer = headerValue(headers["authorization"] ?? headers["Authorization"]).replace(/^Bearer\s+/i, "");
|
|
31
|
+
if (bearer) return bearer;
|
|
32
|
+
const cookieHeader = headerValue(headers["cookie"] ?? headers["Cookie"]);
|
|
33
|
+
return parseCookieHeader(cookieHeader)["wby-id-token"] ?? null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const NodeHttpIdentityLoaderDecorator = NodeHttpEventHandler.createDecorator({
|
|
37
|
+
decorator: NodeHttpIdentityLoaderDecoratorImpl,
|
|
38
|
+
dependencies: [
|
|
39
|
+
RawAuthToken,
|
|
40
|
+
RequestIdentityLoader
|
|
41
|
+
]
|
|
42
|
+
});
|
|
43
|
+
export { NodeHttpIdentityLoaderDecorator };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=NodeHttpIdentityLoaderDecorator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handlers/NodeHttpIdentityLoaderDecorator.js","sources":["../../src/handlers/NodeHttpIdentityLoaderDecorator.ts"],"sourcesContent":["import type { IncomingMessage } from \"node:http\";\nimport { NodeHttpEventHandler } from \"@webiny/event-handler-standalone\";\nimport {\n RawAuthToken,\n RequestIdentityLoader\n} from \"@webiny/api-core/features/requestContext/index.js\";\nimport type { IRequestIdentityLoader } from \"@webiny/api-core/features/requestContext/abstractions.js\";\nimport type { EventContext, NextFunction } from \"@webiny/event-handler-core\";\n\nfunction headerValue(value: string | string[] | undefined): string {\n if (Array.isArray(value)) {\n return value[0] ?? \"\";\n }\n return value ?? \"\";\n}\n\nfunction parseCookieHeader(cookieHeader: string): Record<string, string> {\n return cookieHeader.split(\";\").reduce<Record<string, string>>((acc, pair) => {\n const idx = pair.indexOf(\"=\");\n if (idx > 0) {\n acc[pair.slice(0, idx).trim()] = decodeURIComponent(pair.slice(idx + 1).trim());\n }\n return acc;\n }, {});\n}\n\n/**\n * EXTRACT (transport-specific): reads the auth token from a Node `IncomingMessage` —\n * `x-webiny-authorization` first, then the bearer `Authorization` header, then the `wby-id-token`\n * cookie — into RawAuthToken, then invokes the shared LOAD step (RequestIdentityLoader) which\n * authenticates it (via the registered identity provider, e.g. the self-hosted JWT IdP) and sets\n * IdentityContext.\n *\n * `x-webiny-authorization` exists for AWS parity: behind CloudFront with Origin Access Control, SigV4\n * occupies the `Authorization` header, so clients that may traverse such an origin send the token in\n * that header instead. Standalone has no such constraint, but it accepts the same header so one\n * client works against both deployments.\n *\n * Node mirror of ApiGatewayIdentityLoaderDecorator. Registered BEFORE the tenant loader so identity\n * is established before tenant.\n */\nclass NodeHttpIdentityLoaderDecoratorImpl implements NodeHttpEventHandler.Interface {\n constructor(\n private rawAuthToken: RawAuthToken.Interface,\n private identityLoader: IRequestIdentityLoader,\n private decoratee: NodeHttpEventHandler.Interface\n ) {}\n\n async execute(ctx: EventContext<IncomingMessage>, next: NextFunction): Promise<any> {\n this.rawAuthToken.set(this.extractToken(ctx.event));\n await this.identityLoader.establish();\n return this.decoratee.execute(ctx, next);\n }\n\n private extractToken(event: IncomingMessage): string | null {\n const headers = event?.headers;\n if (!headers) {\n return null;\n }\n const webinyAuth = headerValue(\n headers[\"x-webiny-authorization\"] ?? headers[\"X-Webiny-Authorization\"]\n ).replace(/^Bearer\\s+/i, \"\");\n if (webinyAuth) {\n return webinyAuth;\n }\n const bearer = headerValue(headers[\"authorization\"] ?? headers[\"Authorization\"]).replace(\n /^Bearer\\s+/i,\n \"\"\n );\n if (bearer) {\n return bearer;\n }\n const cookieHeader = headerValue(headers[\"cookie\"] ?? headers[\"Cookie\"]);\n return parseCookieHeader(cookieHeader)[\"wby-id-token\"] ?? null;\n }\n}\n\nexport const NodeHttpIdentityLoaderDecorator = NodeHttpEventHandler.createDecorator({\n decorator: NodeHttpIdentityLoaderDecoratorImpl,\n dependencies: [RawAuthToken, RequestIdentityLoader]\n});\n"],"names":["headerValue","value","Array","parseCookieHeader","cookieHeader","acc","pair","idx","decodeURIComponent","NodeHttpIdentityLoaderDecoratorImpl","rawAuthToken","identityLoader","decoratee","ctx","next","event","headers","webinyAuth","bearer","NodeHttpIdentityLoaderDecorator","NodeHttpEventHandler","RawAuthToken","RequestIdentityLoader"],"mappings":";;AASA,SAASA,YAAYC,KAAoC;IACrD,IAAIC,MAAM,OAAO,CAACD,QACd,OAAOA,KAAK,CAAC,EAAE,IAAI;IAEvB,OAAOA,SAAS;AACpB;AAEA,SAASE,kBAAkBC,YAAoB;IAC3C,OAAOA,aAAa,KAAK,CAAC,KAAK,MAAM,CAAyB,CAACC,KAAKC;QAChE,MAAMC,MAAMD,KAAK,OAAO,CAAC;QACzB,IAAIC,MAAM,GACNF,GAAG,CAACC,KAAK,KAAK,CAAC,GAAGC,KAAK,IAAI,GAAG,GAAGC,mBAAmBF,KAAK,KAAK,CAACC,MAAM,GAAG,IAAI;QAEhF,OAAOF;IACX,GAAG,CAAC;AACR;AAiBA,MAAMI;IACF,YACYC,YAAoC,EACpCC,cAAsC,EACtCC,SAAyC,CACnD;aAHUF,YAAY,GAAZA;aACAC,cAAc,GAAdA;aACAC,SAAS,GAATA;IACT;IAEH,MAAM,QAAQC,GAAkC,EAAEC,IAAkB,EAAgB;QAChF,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAACD,IAAI,KAAK;QACjD,MAAM,IAAI,CAAC,cAAc,CAAC,SAAS;QACnC,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,CAACA,KAAKC;IACvC;IAEQ,aAAaC,KAAsB,EAAiB;QACxD,MAAMC,UAAUD,OAAO;QACvB,IAAI,CAACC,SACD,OAAO;QAEX,MAAMC,aAAajB,YACfgB,OAAO,CAAC,yBAAyB,IAAIA,OAAO,CAAC,yBAAyB,EACxE,OAAO,CAAC,eAAe;QACzB,IAAIC,YACA,OAAOA;QAEX,MAAMC,SAASlB,YAAYgB,OAAO,CAAC,gBAAgB,IAAIA,OAAO,CAAC,gBAAgB,EAAE,OAAO,CACpF,eACA;QAEJ,IAAIE,QACA,OAAOA;QAEX,MAAMd,eAAeJ,YAAYgB,OAAO,CAAC,SAAS,IAAIA,OAAO,CAAC,SAAS;QACvE,OAAOb,kBAAkBC,aAAa,CAAC,eAAe,IAAI;IAC9D;AACJ;AAEO,MAAMe,kCAAkCC,qBAAqB,eAAe,CAAC;IAChF,WAAWX;IACX,cAAc;QAACY;QAAcC;KAAsB;AACvD"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { IncomingMessage } from "node:http";
|
|
2
|
+
import { NodeHttpEventHandler } from "@webiny/event-handler-standalone";
|
|
3
|
+
import { RawTenantId } from "@webiny/api-core/features/requestContext/index.js";
|
|
4
|
+
import type { IRequestTenantLoader } from "@webiny/api-core/features/requestContext/abstractions.js";
|
|
5
|
+
import type { EventContext, NextFunction } from "@webiny/event-handler-core";
|
|
6
|
+
/**
|
|
7
|
+
* EXTRACT (transport-specific): reads the tenant id from the `x-tenant` header of a Node
|
|
8
|
+
* `IncomingMessage` into RawTenantId, then invokes the shared LOAD step (RequestTenantLoader) which
|
|
9
|
+
* resolves the Tenant and sets TenantContext. A missing header leaves RawTenantId null → the loader
|
|
10
|
+
* defaults to the "root" tenant.
|
|
11
|
+
*
|
|
12
|
+
* Node mirror of ApiGatewayTenantLoaderDecorator. Registered AFTER the identity loader.
|
|
13
|
+
*/
|
|
14
|
+
declare class NodeHttpTenantLoaderDecoratorImpl implements NodeHttpEventHandler.Interface {
|
|
15
|
+
private rawTenantId;
|
|
16
|
+
private tenantLoader;
|
|
17
|
+
private decoratee;
|
|
18
|
+
constructor(rawTenantId: RawTenantId.Interface, tenantLoader: IRequestTenantLoader, decoratee: NodeHttpEventHandler.Interface);
|
|
19
|
+
execute(ctx: EventContext<IncomingMessage>, next: NextFunction): Promise<any>;
|
|
20
|
+
}
|
|
21
|
+
export declare const NodeHttpTenantLoaderDecorator: typeof NodeHttpTenantLoaderDecoratorImpl & {
|
|
22
|
+
__abstraction: import("@webiny/di").Abstraction<import("@webiny/event-handler-standalone").INodeHttpEventHandler>;
|
|
23
|
+
};
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { NodeHttpEventHandler } from "@webiny/event-handler-standalone";
|
|
2
|
+
import { RawTenantId, RequestTenantLoader } from "@webiny/api-core/features/requestContext/index.js";
|
|
3
|
+
function headerValue(value) {
|
|
4
|
+
if (Array.isArray(value)) return value[0] ?? null;
|
|
5
|
+
return value ?? null;
|
|
6
|
+
}
|
|
7
|
+
class NodeHttpTenantLoaderDecoratorImpl {
|
|
8
|
+
constructor(rawTenantId, tenantLoader, decoratee){
|
|
9
|
+
this.rawTenantId = rawTenantId;
|
|
10
|
+
this.tenantLoader = tenantLoader;
|
|
11
|
+
this.decoratee = decoratee;
|
|
12
|
+
}
|
|
13
|
+
async execute(ctx, next) {
|
|
14
|
+
const headers = ctx.event?.headers;
|
|
15
|
+
this.rawTenantId.set(headers ? headerValue(headers["x-tenant"] ?? headers["X-Tenant"]) : null);
|
|
16
|
+
await this.tenantLoader.establish();
|
|
17
|
+
return this.decoratee.execute(ctx, next);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const NodeHttpTenantLoaderDecorator = NodeHttpEventHandler.createDecorator({
|
|
21
|
+
decorator: NodeHttpTenantLoaderDecoratorImpl,
|
|
22
|
+
dependencies: [
|
|
23
|
+
RawTenantId,
|
|
24
|
+
RequestTenantLoader
|
|
25
|
+
]
|
|
26
|
+
});
|
|
27
|
+
export { NodeHttpTenantLoaderDecorator };
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=NodeHttpTenantLoaderDecorator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handlers/NodeHttpTenantLoaderDecorator.js","sources":["../../src/handlers/NodeHttpTenantLoaderDecorator.ts"],"sourcesContent":["import type { IncomingMessage } from \"node:http\";\nimport { NodeHttpEventHandler } from \"@webiny/event-handler-standalone\";\nimport {\n RawTenantId,\n RequestTenantLoader\n} from \"@webiny/api-core/features/requestContext/index.js\";\nimport type { IRequestTenantLoader } from \"@webiny/api-core/features/requestContext/abstractions.js\";\nimport type { EventContext, NextFunction } from \"@webiny/event-handler-core\";\n\nfunction headerValue(value: string | string[] | undefined): string | null {\n if (Array.isArray(value)) {\n return value[0] ?? null;\n }\n return value ?? null;\n}\n\n/**\n * EXTRACT (transport-specific): reads the tenant id from the `x-tenant` header of a Node\n * `IncomingMessage` into RawTenantId, then invokes the shared LOAD step (RequestTenantLoader) which\n * resolves the Tenant and sets TenantContext. A missing header leaves RawTenantId null → the loader\n * defaults to the \"root\" tenant.\n *\n * Node mirror of ApiGatewayTenantLoaderDecorator. Registered AFTER the identity loader.\n */\nclass NodeHttpTenantLoaderDecoratorImpl implements NodeHttpEventHandler.Interface {\n constructor(\n private rawTenantId: RawTenantId.Interface,\n private tenantLoader: IRequestTenantLoader,\n private decoratee: NodeHttpEventHandler.Interface\n ) {}\n\n async execute(ctx: EventContext<IncomingMessage>, next: NextFunction): Promise<any> {\n const headers = ctx.event?.headers;\n this.rawTenantId.set(\n headers ? headerValue(headers[\"x-tenant\"] ?? headers[\"X-Tenant\"]) : null\n );\n await this.tenantLoader.establish();\n return this.decoratee.execute(ctx, next);\n }\n}\n\nexport const NodeHttpTenantLoaderDecorator = NodeHttpEventHandler.createDecorator({\n decorator: NodeHttpTenantLoaderDecoratorImpl,\n dependencies: [RawTenantId, RequestTenantLoader]\n});\n"],"names":["headerValue","value","Array","NodeHttpTenantLoaderDecoratorImpl","rawTenantId","tenantLoader","decoratee","ctx","next","headers","NodeHttpTenantLoaderDecorator","NodeHttpEventHandler","RawTenantId","RequestTenantLoader"],"mappings":";;AASA,SAASA,YAAYC,KAAoC;IACrD,IAAIC,MAAM,OAAO,CAACD,QACd,OAAOA,KAAK,CAAC,EAAE,IAAI;IAEvB,OAAOA,SAAS;AACpB;AAUA,MAAME;IACF,YACYC,WAAkC,EAClCC,YAAkC,EAClCC,SAAyC,CACnD;aAHUF,WAAW,GAAXA;aACAC,YAAY,GAAZA;aACAC,SAAS,GAATA;IACT;IAEH,MAAM,QAAQC,GAAkC,EAAEC,IAAkB,EAAgB;QAChF,MAAMC,UAAUF,IAAI,KAAK,EAAE;QAC3B,IAAI,CAAC,WAAW,CAAC,GAAG,CAChBE,UAAUT,YAAYS,OAAO,CAAC,WAAW,IAAIA,OAAO,CAAC,WAAW,IAAI;QAExE,MAAM,IAAI,CAAC,YAAY,CAAC,SAAS;QACjC,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,CAACF,KAAKC;IACvC;AACJ;AAEO,MAAME,gCAAgCC,qBAAqB,eAAe,CAAC;IAC9E,WAAWR;IACX,cAAc;QAACS;QAAaC;KAAoB;AACpD"}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createWebinyApiHandler } from "./createWebinyApiHandler.js";
|
|
2
|
+
export type { CreateWebinyApiHandlerConfig } from "./createWebinyApiHandler.js";
|
|
3
|
+
export { NodeHttpIdentityLoaderDecorator } from "./handlers/NodeHttpIdentityLoaderDecorator.js";
|
|
4
|
+
export { NodeHttpTenantLoaderDecorator } from "./handlers/NodeHttpTenantLoaderDecorator.js";
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@webiny/api-event-handler-standalone",
|
|
3
|
+
"version": "6.6.0-alpha.4",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./index.js",
|
|
8
|
+
"./*": "./*"
|
|
9
|
+
},
|
|
10
|
+
"types": "index.d.ts",
|
|
11
|
+
"description": "EXPERIMENTAL: Webiny API handler for the standalone Node HTTP server transport.",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/webiny/webiny-js.git"
|
|
15
|
+
},
|
|
16
|
+
"contributors": [
|
|
17
|
+
"Webiny Team"
|
|
18
|
+
],
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@webiny/api-core": "6.6.0-alpha.4",
|
|
22
|
+
"@webiny/api-event-handler-core": "6.6.0-alpha.4",
|
|
23
|
+
"@webiny/api-file-manager-standalone": "6.6.0-alpha.4",
|
|
24
|
+
"@webiny/api-graphql": "6.6.0-alpha.4",
|
|
25
|
+
"@webiny/api-headless-cms-bulk-actions-standalone": "6.6.0-alpha.4",
|
|
26
|
+
"@webiny/api-scheduler": "6.6.0-alpha.4",
|
|
27
|
+
"@webiny/api-scheduler-standalone": "6.6.0-alpha.4",
|
|
28
|
+
"@webiny/api-websockets-standalone": "6.6.0-alpha.4",
|
|
29
|
+
"@webiny/background-tasks-standalone": "6.6.0-alpha.4",
|
|
30
|
+
"@webiny/event-handler-core": "6.6.0-alpha.4",
|
|
31
|
+
"@webiny/event-handler-standalone": "6.6.0-alpha.4",
|
|
32
|
+
"@webiny/feature": "6.6.0-alpha.4",
|
|
33
|
+
"@webiny/handler": "6.6.0-alpha.4",
|
|
34
|
+
"@webiny/stdlib": "0.0.17"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@webiny/di": ">=0.2.3"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@webiny/build-tools": "6.6.0-alpha.4",
|
|
41
|
+
"@webiny/di": "1.1.0",
|
|
42
|
+
"rimraf": "6.1.3",
|
|
43
|
+
"typescript": "7.0.2",
|
|
44
|
+
"vitest": "4.1.11"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"webiny": {
|
|
50
|
+
"publishFrom": "dist"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { HttpRouteDefinition, HttpRouteHandler } from "@webiny/event-handler-core";
|
|
2
|
+
import type { Container } from "@webiny/feature/api";
|
|
3
|
+
import { SchedulerInternalToken } from "./abstractions/InternalToken.js";
|
|
4
|
+
/**
|
|
5
|
+
* Re-arms the persisted (pending) scheduled actions for a tenant into the root Bree singleton — used
|
|
6
|
+
* at boot to restore schedules after a restart. Runs the full request-context bootstrap (like the run
|
|
7
|
+
* route) so ListScheduledActionsUseCase has its CMS model, then hands the results to the singleton's
|
|
8
|
+
* `recover()`. Defaults to the root tenant when no tenant is given.
|
|
9
|
+
*
|
|
10
|
+
* NOTE: recovers ONE tenant per call. Boot recovery for additional (non-root) tenants would enumerate
|
|
11
|
+
* tenants and call this per tenant — left as a follow-up; single/root-tenant deployments are covered.
|
|
12
|
+
*/
|
|
13
|
+
declare class ScheduledActionRecoverRouteImpl implements HttpRouteHandler.Interface {
|
|
14
|
+
private readonly container;
|
|
15
|
+
private readonly internalToken;
|
|
16
|
+
constructor(container: Container, internalToken: SchedulerInternalToken.Interface);
|
|
17
|
+
handle(request: HttpRouteHandler.Request, response: HttpRouteHandler.Response): Promise<import("@webiny/event-handler-core").IHttpResponseBuilder>;
|
|
18
|
+
}
|
|
19
|
+
export declare const ScheduledActionRecoverRoute: typeof ScheduledActionRecoverRouteImpl & {
|
|
20
|
+
__abstraction: import("@webiny/di").Abstraction<import("@webiny/event-handler-core").IHttpRoute>;
|
|
21
|
+
};
|
|
22
|
+
declare class ScheduledActionRecoverRouteDefinitionImpl implements HttpRouteDefinition.Interface {
|
|
23
|
+
readonly name = "scheduled-action-recover";
|
|
24
|
+
readonly method = "POST";
|
|
25
|
+
readonly path = "/scheduled-action-recover";
|
|
26
|
+
readonly handler: typeof ScheduledActionRecoverRouteImpl & {
|
|
27
|
+
__abstraction: import("@webiny/di").Abstraction<import("@webiny/event-handler-core").IHttpRoute>;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** What the router matches on. Zero dependencies, so building it costs nothing. */
|
|
31
|
+
export declare const ScheduledActionRecoverRouteDefinition: typeof ScheduledActionRecoverRouteDefinitionImpl & {
|
|
32
|
+
__abstraction: import("@webiny/di").Abstraction<import("@webiny/event-handler-core").IHttpRouteDefinition>;
|
|
33
|
+
};
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { HttpRouteDefinition, HttpRouteHandler, RequestContainer } from "@webiny/event-handler-core";
|
|
2
|
+
import { GraphQLContextEnhancer, GraphQLContextualSchema } from "@webiny/api-graphql";
|
|
3
|
+
import { RawTenantId, RequestTenantLoader } from "@webiny/api-core/features/requestContext/index.js";
|
|
4
|
+
import { GetRootTenantUseCase } from "@webiny/api-core/features/tenancy/GetRootTenant/index.js";
|
|
5
|
+
import { IdentityContext } from "@webiny/api-core/exports/api/security.js";
|
|
6
|
+
import { ListScheduledActionsUseCase } from "@webiny/api-scheduler/features/ListScheduledActions/index.js";
|
|
7
|
+
import { SchedulerInternalToken } from "./abstractions/InternalToken.js";
|
|
8
|
+
import { SchedulerSingleton } from "./abstractions/SchedulerSingleton.js";
|
|
9
|
+
const INTERNAL_HEADER = "x-webiny-scheduler-token";
|
|
10
|
+
class ScheduledActionRecoverRouteImpl {
|
|
11
|
+
constructor(container, internalToken){
|
|
12
|
+
this.container = container;
|
|
13
|
+
this.internalToken = internalToken;
|
|
14
|
+
}
|
|
15
|
+
async handle(request, response) {
|
|
16
|
+
if (request.headers[INTERNAL_HEADER] !== this.internalToken.value) return response.status(403).json({
|
|
17
|
+
error: "Forbidden."
|
|
18
|
+
});
|
|
19
|
+
try {
|
|
20
|
+
let tenant = request.body?.tenant;
|
|
21
|
+
if (!tenant) {
|
|
22
|
+
const rootResult = await this.container.resolve(GetRootTenantUseCase).execute();
|
|
23
|
+
if (rootResult.isFail()) return response.json({
|
|
24
|
+
status: "ok",
|
|
25
|
+
recovered: 0
|
|
26
|
+
});
|
|
27
|
+
tenant = rootResult.value.id;
|
|
28
|
+
}
|
|
29
|
+
this.container.resolve(RawTenantId).set(tenant);
|
|
30
|
+
await this.container.resolve(RequestTenantLoader).establish();
|
|
31
|
+
const ctx = {
|
|
32
|
+
container: this.container
|
|
33
|
+
};
|
|
34
|
+
for (const enhancer of this.container.resolveAll(GraphQLContextEnhancer))await enhancer.enhance(ctx);
|
|
35
|
+
for (const schema of this.container.resolveAll(GraphQLContextualSchema))await schema.build(ctx);
|
|
36
|
+
const listResult = await this.container.resolve(IdentityContext).withoutAuthorization(()=>this.container.resolve(ListScheduledActionsUseCase).execute({
|
|
37
|
+
where: {},
|
|
38
|
+
limit: 1000
|
|
39
|
+
}));
|
|
40
|
+
if (listResult.isFail()) return response.status(500).json({
|
|
41
|
+
status: "error",
|
|
42
|
+
error: {
|
|
43
|
+
message: listResult.error.message
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
const pending = listResult.value.items.map((action)=>({
|
|
47
|
+
id: action.id,
|
|
48
|
+
namespace: action.namespace,
|
|
49
|
+
tenant: tenant,
|
|
50
|
+
scheduledFor: action.scheduledFor
|
|
51
|
+
}));
|
|
52
|
+
await this.container.resolve(SchedulerSingleton).recover(pending);
|
|
53
|
+
return response.json({
|
|
54
|
+
status: "ok",
|
|
55
|
+
recovered: pending.length
|
|
56
|
+
});
|
|
57
|
+
} catch (err) {
|
|
58
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
59
|
+
console.error(`Scheduled action recover route error: ${message}`);
|
|
60
|
+
return response.status(500).json({
|
|
61
|
+
status: "error",
|
|
62
|
+
error: {
|
|
63
|
+
message
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const ScheduledActionRecoverRoute = HttpRouteHandler.createImplementation({
|
|
70
|
+
implementation: ScheduledActionRecoverRouteImpl,
|
|
71
|
+
dependencies: [
|
|
72
|
+
RequestContainer,
|
|
73
|
+
SchedulerInternalToken
|
|
74
|
+
]
|
|
75
|
+
});
|
|
76
|
+
class ScheduledActionRecoverRouteDefinitionImpl {
|
|
77
|
+
constructor(){
|
|
78
|
+
this.name = "scheduled-action-recover";
|
|
79
|
+
this.method = "POST";
|
|
80
|
+
this.path = "/scheduled-action-recover";
|
|
81
|
+
this.handler = ScheduledActionRecoverRoute;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const ScheduledActionRecoverRouteDefinition = HttpRouteDefinition.createImplementation({
|
|
85
|
+
implementation: ScheduledActionRecoverRouteDefinitionImpl,
|
|
86
|
+
dependencies: []
|
|
87
|
+
});
|
|
88
|
+
export { ScheduledActionRecoverRoute, ScheduledActionRecoverRouteDefinition };
|
|
89
|
+
|
|
90
|
+
//# sourceMappingURL=ScheduledActionRecoverRoute.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler/ScheduledActionRecoverRoute.js","sources":["../../src/scheduler/ScheduledActionRecoverRoute.ts"],"sourcesContent":["import {\n HttpRouteDefinition,\n HttpRouteHandler,\n RequestContainer\n} from \"@webiny/event-handler-core\";\nimport { GraphQLContextEnhancer, GraphQLContextualSchema } from \"@webiny/api-graphql\";\nimport {\n RawTenantId,\n RequestTenantLoader\n} from \"@webiny/api-core/features/requestContext/index.js\";\nimport { GetRootTenantUseCase } from \"@webiny/api-core/features/tenancy/GetRootTenant/index.js\";\nimport { IdentityContext } from \"@webiny/api-core/exports/api/security.js\";\nimport { ListScheduledActionsUseCase } from \"@webiny/api-scheduler/features/ListScheduledActions/index.js\";\nimport type { Container } from \"@webiny/feature/api\";\nimport { SchedulerInternalToken } from \"./abstractions/InternalToken.js\";\nimport { SchedulerSingleton } from \"./abstractions/SchedulerSingleton.js\";\n\nconst INTERNAL_HEADER = \"x-webiny-scheduler-token\";\n\n/**\n * Re-arms the persisted (pending) scheduled actions for a tenant into the root Bree singleton — used\n * at boot to restore schedules after a restart. Runs the full request-context bootstrap (like the run\n * route) so ListScheduledActionsUseCase has its CMS model, then hands the results to the singleton's\n * `recover()`. Defaults to the root tenant when no tenant is given.\n *\n * NOTE: recovers ONE tenant per call. Boot recovery for additional (non-root) tenants would enumerate\n * tenants and call this per tenant — left as a follow-up; single/root-tenant deployments are covered.\n */\nclass ScheduledActionRecoverRouteImpl implements HttpRouteHandler.Interface {\n public constructor(\n private readonly container: Container,\n private readonly internalToken: SchedulerInternalToken.Interface\n ) {}\n\n public async handle(request: HttpRouteHandler.Request, response: HttpRouteHandler.Response) {\n if (request.headers[INTERNAL_HEADER] !== this.internalToken.value) {\n return response.status(403).json({ error: \"Forbidden.\" });\n }\n\n try {\n let tenant: string | undefined = request.body?.tenant;\n if (!tenant) {\n const rootResult = await this.container.resolve(GetRootTenantUseCase).execute();\n if (rootResult.isFail()) {\n return response.json({ status: \"ok\", recovered: 0 });\n }\n tenant = rootResult.value.id;\n }\n\n this.container.resolve(RawTenantId).set(tenant);\n await this.container.resolve(RequestTenantLoader).establish();\n\n const ctx: Record<string, any> = { container: this.container };\n for (const enhancer of this.container.resolveAll(GraphQLContextEnhancer)) {\n await enhancer.enhance(ctx);\n }\n for (const schema of this.container.resolveAll(GraphQLContextualSchema)) {\n await schema.build(ctx);\n }\n\n // At boot there's no request and no identity, so we skip authorization for the list — the\n // same thing ExecuteScheduledActionUseCase does on the run route. Without it, the\n // permission check in ListScheduledActions sees the anonymous identity and fails with\n // \"Not authorized!\".\n const listResult = await this.container\n .resolve(IdentityContext)\n .withoutAuthorization(() =>\n this.container\n .resolve(ListScheduledActionsUseCase)\n .execute({ where: {}, limit: 1000 })\n );\n\n if (listResult.isFail()) {\n return response\n .status(500)\n .json({ status: \"error\", error: { message: listResult.error.message } });\n }\n\n const pending = listResult.value.items.map(action => ({\n id: action.id,\n namespace: action.namespace,\n tenant: tenant as string,\n scheduledFor: action.scheduledFor\n }));\n\n await this.container.resolve(SchedulerSingleton).recover(pending);\n\n return response.json({ status: \"ok\", recovered: pending.length });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.error(`Scheduled action recover route error: ${message}`);\n return response.status(500).json({ status: \"error\", error: { message } });\n }\n }\n}\n\nexport const ScheduledActionRecoverRoute = HttpRouteHandler.createImplementation({\n implementation: ScheduledActionRecoverRouteImpl,\n dependencies: [RequestContainer, SchedulerInternalToken]\n});\n\nclass ScheduledActionRecoverRouteDefinitionImpl implements HttpRouteDefinition.Interface {\n readonly name = \"scheduled-action-recover\";\n readonly method = \"POST\";\n readonly path = \"/scheduled-action-recover\";\n readonly handler = ScheduledActionRecoverRoute;\n}\n\n/** What the router matches on. Zero dependencies, so building it costs nothing. */\nexport const ScheduledActionRecoverRouteDefinition = HttpRouteDefinition.createImplementation({\n implementation: ScheduledActionRecoverRouteDefinitionImpl,\n dependencies: []\n});\n"],"names":["INTERNAL_HEADER","ScheduledActionRecoverRouteImpl","container","internalToken","request","response","tenant","rootResult","GetRootTenantUseCase","RawTenantId","RequestTenantLoader","ctx","enhancer","GraphQLContextEnhancer","schema","GraphQLContextualSchema","listResult","IdentityContext","ListScheduledActionsUseCase","pending","action","SchedulerSingleton","err","message","Error","String","console","ScheduledActionRecoverRoute","HttpRouteHandler","RequestContainer","SchedulerInternalToken","ScheduledActionRecoverRouteDefinitionImpl","ScheduledActionRecoverRouteDefinition","HttpRouteDefinition"],"mappings":";;;;;;;;AAiBA,MAAMA,kBAAkB;AAWxB,MAAMC;IACF,YACqBC,SAAoB,EACpBC,aAA+C,CAClE;aAFmBD,SAAS,GAATA;aACAC,aAAa,GAAbA;IAClB;IAEH,MAAa,OAAOC,OAAiC,EAAEC,QAAmC,EAAE;QACxF,IAAID,QAAQ,OAAO,CAACJ,gBAAgB,KAAK,IAAI,CAAC,aAAa,CAAC,KAAK,EAC7D,OAAOK,SAAS,MAAM,CAAC,KAAK,IAAI,CAAC;YAAE,OAAO;QAAa;QAG3D,IAAI;YACA,IAAIC,SAA6BF,QAAQ,IAAI,EAAE;YAC/C,IAAI,CAACE,QAAQ;gBACT,MAAMC,aAAa,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAACC,sBAAsB,OAAO;gBAC7E,IAAID,WAAW,MAAM,IACjB,OAAOF,SAAS,IAAI,CAAC;oBAAE,QAAQ;oBAAM,WAAW;gBAAE;gBAEtDC,SAASC,WAAW,KAAK,CAAC,EAAE;YAChC;YAEA,IAAI,CAAC,SAAS,CAAC,OAAO,CAACE,aAAa,GAAG,CAACH;YACxC,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAACI,qBAAqB,SAAS;YAE3D,MAAMC,MAA2B;gBAAE,WAAW,IAAI,CAAC,SAAS;YAAC;YAC7D,KAAK,MAAMC,YAAY,IAAI,CAAC,SAAS,CAAC,UAAU,CAACC,wBAC7C,MAAMD,SAAS,OAAO,CAACD;YAE3B,KAAK,MAAMG,UAAU,IAAI,CAAC,SAAS,CAAC,UAAU,CAACC,yBAC3C,MAAMD,OAAO,KAAK,CAACH;YAOvB,MAAMK,aAAa,MAAM,IAAI,CAAC,SAAS,CAClC,OAAO,CAACC,iBACR,oBAAoB,CAAC,IAClB,IAAI,CAAC,SAAS,CACT,OAAO,CAACC,6BACR,OAAO,CAAC;oBAAE,OAAO,CAAC;oBAAG,OAAO;gBAAK;YAG9C,IAAIF,WAAW,MAAM,IACjB,OAAOX,SACF,MAAM,CAAC,KACP,IAAI,CAAC;gBAAE,QAAQ;gBAAS,OAAO;oBAAE,SAASW,WAAW,KAAK,CAAC,OAAO;gBAAC;YAAE;YAG9E,MAAMG,UAAUH,WAAW,KAAK,CAAC,KAAK,CAAC,GAAG,CAACI,CAAAA,SAAW;oBAClD,IAAIA,OAAO,EAAE;oBACb,WAAWA,OAAO,SAAS;oBAC3B,QAAQd;oBACR,cAAcc,OAAO,YAAY;gBACrC;YAEA,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAACC,oBAAoB,OAAO,CAACF;YAEzD,OAAOd,SAAS,IAAI,CAAC;gBAAE,QAAQ;gBAAM,WAAWc,QAAQ,MAAM;YAAC;QACnE,EAAE,OAAOG,KAAK;YACV,MAAMC,UAAUD,eAAeE,QAAQF,IAAI,OAAO,GAAGG,OAAOH;YAC5DI,QAAQ,KAAK,CAAC,CAAC,sCAAsC,EAAEH,SAAS;YAChE,OAAOlB,SAAS,MAAM,CAAC,KAAK,IAAI,CAAC;gBAAE,QAAQ;gBAAS,OAAO;oBAAEkB;gBAAQ;YAAE;QAC3E;IACJ;AACJ;AAEO,MAAMI,8BAA8BC,iBAAiB,oBAAoB,CAAC;IAC7E,gBAAgB3B;IAChB,cAAc;QAAC4B;QAAkBC;KAAuB;AAC5D;AAEA,MAAMC;;aACO,IAAI,GAAG;aACP,MAAM,GAAG;aACT,IAAI,GAAG;aACP,OAAO,GAAGJ;;AACvB;AAGO,MAAMK,wCAAwCC,oBAAoB,oBAAoB,CAAC;IAC1F,gBAAgBF;IAChB,cAAc,EAAE;AACpB"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { HttpRouteDefinition, HttpRouteHandler } from "@webiny/event-handler-core";
|
|
2
|
+
import type { Container } from "@webiny/feature/api";
|
|
3
|
+
import { SchedulerInternalToken } from "./abstractions/InternalToken.js";
|
|
4
|
+
/**
|
|
5
|
+
* Runs a single scheduled action. The in-process Bree singleton (root) POSTs here when a timer fires —
|
|
6
|
+
* the timer fires OUTSIDE any HTTP request, so this route rebuilds the full request context (tenant +
|
|
7
|
+
* CMS models via the request-context initializers + contextual schemas) for the action's tenant, then
|
|
8
|
+
* runs ExecuteScheduledActionUseCase. Mirrors the background-task run route.
|
|
9
|
+
*/
|
|
10
|
+
declare class ScheduledActionRunRouteImpl implements HttpRouteHandler.Interface {
|
|
11
|
+
private readonly container;
|
|
12
|
+
private readonly internalToken;
|
|
13
|
+
constructor(container: Container, internalToken: SchedulerInternalToken.Interface);
|
|
14
|
+
handle(request: HttpRouteHandler.Request, response: HttpRouteHandler.Response): Promise<import("@webiny/event-handler-core").IHttpResponseBuilder>;
|
|
15
|
+
}
|
|
16
|
+
export declare const ScheduledActionRunRoute: typeof ScheduledActionRunRouteImpl & {
|
|
17
|
+
__abstraction: import("@webiny/di").Abstraction<import("@webiny/event-handler-core").IHttpRoute>;
|
|
18
|
+
};
|
|
19
|
+
declare class ScheduledActionRunRouteDefinitionImpl implements HttpRouteDefinition.Interface {
|
|
20
|
+
readonly name = "scheduled-action-run";
|
|
21
|
+
readonly method = "POST";
|
|
22
|
+
readonly path = "/scheduled-action-run";
|
|
23
|
+
readonly handler: typeof ScheduledActionRunRouteImpl & {
|
|
24
|
+
__abstraction: import("@webiny/di").Abstraction<import("@webiny/event-handler-core").IHttpRoute>;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** What the router matches on. Zero dependencies, so building it costs nothing. */
|
|
28
|
+
export declare const ScheduledActionRunRouteDefinition: typeof ScheduledActionRunRouteDefinitionImpl & {
|
|
29
|
+
__abstraction: import("@webiny/di").Abstraction<import("@webiny/event-handler-core").IHttpRouteDefinition>;
|
|
30
|
+
};
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { HttpRouteDefinition, HttpRouteHandler, RequestContainer } from "@webiny/event-handler-core";
|
|
2
|
+
import { GraphQLContextEnhancer, GraphQLContextualSchema } from "@webiny/api-graphql";
|
|
3
|
+
import { RawTenantId, RequestTenantLoader } from "@webiny/api-core/features/requestContext/index.js";
|
|
4
|
+
import { ExecuteScheduledActionUseCase } from "@webiny/api-scheduler/features/ExecuteScheduledAction/index.js";
|
|
5
|
+
import { SchedulerInternalToken } from "./abstractions/InternalToken.js";
|
|
6
|
+
const INTERNAL_HEADER = "x-webiny-scheduler-token";
|
|
7
|
+
class ScheduledActionRunRouteImpl {
|
|
8
|
+
constructor(container, internalToken){
|
|
9
|
+
this.container = container;
|
|
10
|
+
this.internalToken = internalToken;
|
|
11
|
+
}
|
|
12
|
+
async handle(request, response) {
|
|
13
|
+
if (request.headers[INTERNAL_HEADER] !== this.internalToken.value) return response.status(403).json({
|
|
14
|
+
error: "Forbidden."
|
|
15
|
+
});
|
|
16
|
+
const { id, namespace, tenant } = request.body ?? {};
|
|
17
|
+
if (!id || !namespace || !tenant) return response.status(400).json({
|
|
18
|
+
error: "Missing id, namespace or tenant."
|
|
19
|
+
});
|
|
20
|
+
try {
|
|
21
|
+
this.container.resolve(RawTenantId).set(tenant);
|
|
22
|
+
await this.container.resolve(RequestTenantLoader).establish();
|
|
23
|
+
const ctx = {
|
|
24
|
+
container: this.container
|
|
25
|
+
};
|
|
26
|
+
for (const enhancer of this.container.resolveAll(GraphQLContextEnhancer))await enhancer.enhance(ctx);
|
|
27
|
+
for (const schema of this.container.resolveAll(GraphQLContextualSchema))await schema.build(ctx);
|
|
28
|
+
const result = await this.container.resolve(ExecuteScheduledActionUseCase).execute({
|
|
29
|
+
id,
|
|
30
|
+
namespace,
|
|
31
|
+
tenant
|
|
32
|
+
});
|
|
33
|
+
if (result.isFail()) return response.status(500).json({
|
|
34
|
+
status: "error",
|
|
35
|
+
error: {
|
|
36
|
+
message: result.error.message
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return response.json({
|
|
40
|
+
status: "ok"
|
|
41
|
+
});
|
|
42
|
+
} catch (err) {
|
|
43
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
44
|
+
console.error(`Scheduled action run route error: ${message}`);
|
|
45
|
+
return response.status(500).json({
|
|
46
|
+
status: "error",
|
|
47
|
+
error: {
|
|
48
|
+
message
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const ScheduledActionRunRoute = HttpRouteHandler.createImplementation({
|
|
55
|
+
implementation: ScheduledActionRunRouteImpl,
|
|
56
|
+
dependencies: [
|
|
57
|
+
RequestContainer,
|
|
58
|
+
SchedulerInternalToken
|
|
59
|
+
]
|
|
60
|
+
});
|
|
61
|
+
class ScheduledActionRunRouteDefinitionImpl {
|
|
62
|
+
constructor(){
|
|
63
|
+
this.name = "scheduled-action-run";
|
|
64
|
+
this.method = "POST";
|
|
65
|
+
this.path = "/scheduled-action-run";
|
|
66
|
+
this.handler = ScheduledActionRunRoute;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const ScheduledActionRunRouteDefinition = HttpRouteDefinition.createImplementation({
|
|
70
|
+
implementation: ScheduledActionRunRouteDefinitionImpl,
|
|
71
|
+
dependencies: []
|
|
72
|
+
});
|
|
73
|
+
export { ScheduledActionRunRoute, ScheduledActionRunRouteDefinition };
|
|
74
|
+
|
|
75
|
+
//# sourceMappingURL=ScheduledActionRunRoute.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler/ScheduledActionRunRoute.js","sources":["../../src/scheduler/ScheduledActionRunRoute.ts"],"sourcesContent":["import {\n HttpRouteDefinition,\n HttpRouteHandler,\n RequestContainer\n} from \"@webiny/event-handler-core\";\nimport { GraphQLContextEnhancer, GraphQLContextualSchema } from \"@webiny/api-graphql\";\nimport {\n RawTenantId,\n RequestTenantLoader\n} from \"@webiny/api-core/features/requestContext/index.js\";\nimport { ExecuteScheduledActionUseCase } from \"@webiny/api-scheduler/features/ExecuteScheduledAction/index.js\";\nimport type { Container } from \"@webiny/feature/api\";\nimport { SchedulerInternalToken } from \"./abstractions/InternalToken.js\";\n\n/* Shared between the Bree singleton and this route to gate access. */\nconst INTERNAL_HEADER = \"x-webiny-scheduler-token\";\n\n/**\n * Runs a single scheduled action. The in-process Bree singleton (root) POSTs here when a timer fires —\n * the timer fires OUTSIDE any HTTP request, so this route rebuilds the full request context (tenant +\n * CMS models via the request-context initializers + contextual schemas) for the action's tenant, then\n * runs ExecuteScheduledActionUseCase. Mirrors the background-task run route.\n */\nclass ScheduledActionRunRouteImpl implements HttpRouteHandler.Interface {\n public constructor(\n private readonly container: Container,\n private readonly internalToken: SchedulerInternalToken.Interface\n ) {}\n\n public async handle(request: HttpRouteHandler.Request, response: HttpRouteHandler.Response) {\n if (request.headers[INTERNAL_HEADER] !== this.internalToken.value) {\n return response.status(403).json({ error: \"Forbidden.\" });\n }\n\n const { id, namespace, tenant } = request.body ?? {};\n if (!id || !namespace || !tenant) {\n return response.status(400).json({ error: \"Missing id, namespace or tenant.\" });\n }\n\n try {\n this.container.resolve(RawTenantId).set(tenant);\n await this.container.resolve(RequestTenantLoader).establish();\n\n /* TODO: remove once legacy ctx is gone — resolve services directly from the container. */\n const ctx: Record<string, any> = { container: this.container };\n for (const enhancer of this.container.resolveAll(GraphQLContextEnhancer)) {\n await enhancer.enhance(ctx);\n }\n for (const schema of this.container.resolveAll(GraphQLContextualSchema)) {\n await schema.build(ctx);\n }\n\n const result = await this.container\n .resolve(ExecuteScheduledActionUseCase)\n .execute({ id, namespace, tenant });\n\n if (result.isFail()) {\n return response\n .status(500)\n .json({ status: \"error\", error: { message: result.error.message } });\n }\n\n return response.json({ status: \"ok\" });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.error(`Scheduled action run route error: ${message}`);\n return response.status(500).json({ status: \"error\", error: { message } });\n }\n }\n}\n\nexport const ScheduledActionRunRoute = HttpRouteHandler.createImplementation({\n implementation: ScheduledActionRunRouteImpl,\n dependencies: [RequestContainer, SchedulerInternalToken]\n});\n\nclass ScheduledActionRunRouteDefinitionImpl implements HttpRouteDefinition.Interface {\n readonly name = \"scheduled-action-run\";\n readonly method = \"POST\";\n readonly path = \"/scheduled-action-run\";\n readonly handler = ScheduledActionRunRoute;\n}\n\n/** What the router matches on. Zero dependencies, so building it costs nothing. */\nexport const ScheduledActionRunRouteDefinition = HttpRouteDefinition.createImplementation({\n implementation: ScheduledActionRunRouteDefinitionImpl,\n dependencies: []\n});\n"],"names":["INTERNAL_HEADER","ScheduledActionRunRouteImpl","container","internalToken","request","response","id","namespace","tenant","RawTenantId","RequestTenantLoader","ctx","enhancer","GraphQLContextEnhancer","schema","GraphQLContextualSchema","result","ExecuteScheduledActionUseCase","err","message","Error","String","console","ScheduledActionRunRoute","HttpRouteHandler","RequestContainer","SchedulerInternalToken","ScheduledActionRunRouteDefinitionImpl","ScheduledActionRunRouteDefinition","HttpRouteDefinition"],"mappings":";;;;;AAeA,MAAMA,kBAAkB;AAQxB,MAAMC;IACF,YACqBC,SAAoB,EACpBC,aAA+C,CAClE;aAFmBD,SAAS,GAATA;aACAC,aAAa,GAAbA;IAClB;IAEH,MAAa,OAAOC,OAAiC,EAAEC,QAAmC,EAAE;QACxF,IAAID,QAAQ,OAAO,CAACJ,gBAAgB,KAAK,IAAI,CAAC,aAAa,CAAC,KAAK,EAC7D,OAAOK,SAAS,MAAM,CAAC,KAAK,IAAI,CAAC;YAAE,OAAO;QAAa;QAG3D,MAAM,EAAEC,EAAE,EAAEC,SAAS,EAAEC,MAAM,EAAE,GAAGJ,QAAQ,IAAI,IAAI,CAAC;QACnD,IAAI,CAACE,MAAM,CAACC,aAAa,CAACC,QACtB,OAAOH,SAAS,MAAM,CAAC,KAAK,IAAI,CAAC;YAAE,OAAO;QAAmC;QAGjF,IAAI;YACA,IAAI,CAAC,SAAS,CAAC,OAAO,CAACI,aAAa,GAAG,CAACD;YACxC,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAACE,qBAAqB,SAAS;YAG3D,MAAMC,MAA2B;gBAAE,WAAW,IAAI,CAAC,SAAS;YAAC;YAC7D,KAAK,MAAMC,YAAY,IAAI,CAAC,SAAS,CAAC,UAAU,CAACC,wBAC7C,MAAMD,SAAS,OAAO,CAACD;YAE3B,KAAK,MAAMG,UAAU,IAAI,CAAC,SAAS,CAAC,UAAU,CAACC,yBAC3C,MAAMD,OAAO,KAAK,CAACH;YAGvB,MAAMK,SAAS,MAAM,IAAI,CAAC,SAAS,CAC9B,OAAO,CAACC,+BACR,OAAO,CAAC;gBAAEX;gBAAIC;gBAAWC;YAAO;YAErC,IAAIQ,OAAO,MAAM,IACb,OAAOX,SACF,MAAM,CAAC,KACP,IAAI,CAAC;gBAAE,QAAQ;gBAAS,OAAO;oBAAE,SAASW,OAAO,KAAK,CAAC,OAAO;gBAAC;YAAE;YAG1E,OAAOX,SAAS,IAAI,CAAC;gBAAE,QAAQ;YAAK;QACxC,EAAE,OAAOa,KAAK;YACV,MAAMC,UAAUD,eAAeE,QAAQF,IAAI,OAAO,GAAGG,OAAOH;YAC5DI,QAAQ,KAAK,CAAC,CAAC,kCAAkC,EAAEH,SAAS;YAC5D,OAAOd,SAAS,MAAM,CAAC,KAAK,IAAI,CAAC;gBAAE,QAAQ;gBAAS,OAAO;oBAAEc;gBAAQ;YAAE;QAC3E;IACJ;AACJ;AAEO,MAAMI,0BAA0BC,iBAAiB,oBAAoB,CAAC;IACzE,gBAAgBvB;IAChB,cAAc;QAACwB;QAAkBC;KAAuB;AAC5D;AAEA,MAAMC;;aACO,IAAI,GAAG;aACP,MAAM,GAAG;aACT,IAAI,GAAG;aACP,OAAO,GAAGJ;;AACvB;AAGO,MAAMK,oCAAoCC,oBAAoB,oBAAoB,CAAC;IACtF,gBAAgBF;IAChB,cAAc,EAAE;AACpB"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-process token generated at startup, shared between the in-process Bree scheduler singleton and
|
|
3
|
+
* the scheduler routes via DI. The singleton sends it as a header when a timer fires; the routes
|
|
4
|
+
* reject requests without a matching value. Single-process only — for multi-server, replace with a
|
|
5
|
+
* shared secret. Mirrors the background-tasks InternalToken.
|
|
6
|
+
*/
|
|
7
|
+
interface IInternalToken {
|
|
8
|
+
readonly value: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const SchedulerInternalToken: import("@webiny/di").Abstraction<IInternalToken>;
|
|
11
|
+
export declare namespace SchedulerInternalToken {
|
|
12
|
+
type Interface = IInternalToken;
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler/abstractions/InternalToken.js","sources":["../../../src/scheduler/abstractions/InternalToken.ts"],"sourcesContent":["import { createAbstraction } from \"@webiny/feature/api\";\n\n/**\n * Per-process token generated at startup, shared between the in-process Bree scheduler singleton and\n * the scheduler routes via DI. The singleton sends it as a header when a timer fires; the routes\n * reject requests without a matching value. Single-process only — for multi-server, replace with a\n * shared secret. Mirrors the background-tasks InternalToken.\n */\ninterface IInternalToken {\n readonly value: string;\n}\n\nexport const SchedulerInternalToken = createAbstraction<IInternalToken>(\n \"SchedulerServer/InternalToken\"\n);\n\nexport namespace SchedulerInternalToken {\n export type Interface = IInternalToken;\n}\n"],"names":["SchedulerInternalToken","createAbstraction"],"mappings":";AAYO,MAAMA,yBAAyBC,kBAClC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BreeSchedulerService } from "@webiny/api-scheduler-standalone";
|
|
2
|
+
/**
|
|
3
|
+
* DI token for the single root Bree scheduler instance. Distinct from the `SchedulerService`
|
|
4
|
+
* abstraction (create/update/delete/exists): this exposes the concrete service so the recover route +
|
|
5
|
+
* boot step can call `recover()`, which isn't part of the SchedulerService contract.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SchedulerSingleton: import("@webiny/di").Abstraction<BreeSchedulerService>;
|
|
8
|
+
export declare namespace SchedulerSingleton {
|
|
9
|
+
type Interface = BreeSchedulerService;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler/abstractions/SchedulerSingleton.js","sources":["../../../src/scheduler/abstractions/SchedulerSingleton.ts"],"sourcesContent":["import { createAbstraction } from \"@webiny/feature/api\";\nimport type { BreeSchedulerService } from \"@webiny/api-scheduler-standalone\";\n\n/**\n * DI token for the single root Bree scheduler instance. Distinct from the `SchedulerService`\n * abstraction (create/update/delete/exists): this exposes the concrete service so the recover route +\n * boot step can call `recover()`, which isn't part of the SchedulerService contract.\n */\nexport const SchedulerSingleton = createAbstraction<BreeSchedulerService>(\n \"SchedulerServer/Singleton\"\n);\n\nexport namespace SchedulerSingleton {\n export type Interface = BreeSchedulerService;\n}\n"],"names":["SchedulerSingleton","createAbstraction"],"mappings":";AAQO,MAAMA,qBAAqBC,kBAC9B"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Container } from "@webiny/di";
|
|
2
|
+
/**
|
|
3
|
+
* ROOT wiring for the standalone (Bree, in-process) scheduler. Unlike AWS (per-request EventBridge
|
|
4
|
+
* binding), the server holds ONE long-lived Bree instance for all tenants, started once at boot — the
|
|
5
|
+
* counterpart of the WebSockets connection manager. Registered as `SchedulerService` so per-request
|
|
6
|
+
* create/update/delete (during GraphQL mutations) manipulate that single live timer set.
|
|
7
|
+
*
|
|
8
|
+
* When a timer fires (outside any request), the singleton POSTs to `/scheduled-action-run`, which
|
|
9
|
+
* rebuilds the request context for the action's tenant and executes it (see the route).
|
|
10
|
+
*/
|
|
11
|
+
export declare function registerSchedulerServer(rootContainer: Container): void;
|
|
12
|
+
/**
|
|
13
|
+
* Boot step (onServer): start the timers, then re-arm persisted schedules by POSTing the recover route
|
|
14
|
+
* (root tenant).
|
|
15
|
+
*
|
|
16
|
+
* `onServer` runs BEFORE the HTTP server starts listening, so the recover POST (which hits this same
|
|
17
|
+
* server) is DEFERRED to the next tick — by then `createServerHandler` has returned and the runner has
|
|
18
|
+
* called `.listen()`. Fire-and-forget: a recovery failure must never block or crash startup.
|
|
19
|
+
*/
|
|
20
|
+
export declare function startSchedulerServer(rootContainer: Container): Promise<void>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { uuid } from "@webiny/stdlib";
|
|
2
|
+
import { SchedulerService } from "@webiny/api-scheduler/shared/abstractions.js";
|
|
3
|
+
import { BreeSchedulerService } from "@webiny/api-scheduler-standalone";
|
|
4
|
+
import { SchedulerInternalToken } from "./abstractions/InternalToken.js";
|
|
5
|
+
import { SchedulerSingleton } from "./abstractions/SchedulerSingleton.js";
|
|
6
|
+
import { ScheduledActionRunRouteDefinition } from "./ScheduledActionRunRoute.js";
|
|
7
|
+
import { ScheduledActionRecoverRouteDefinition } from "./ScheduledActionRecoverRoute.js";
|
|
8
|
+
const SCHEDULER_HEADER = "x-webiny-scheduler-token";
|
|
9
|
+
const consoleLogger = {
|
|
10
|
+
trace: (...args)=>console.trace(...args),
|
|
11
|
+
debug: (...args)=>console.debug(...args),
|
|
12
|
+
info: (...args)=>console.info(...args),
|
|
13
|
+
warn: (...args)=>console.warn(...args),
|
|
14
|
+
error: (...args)=>console.error(...args),
|
|
15
|
+
fatal: (...args)=>console.error(...args),
|
|
16
|
+
log: (...args)=>console.log(...args)
|
|
17
|
+
};
|
|
18
|
+
const serverBase = ()=>`http://localhost:${process.env.PORT || "3002"}`;
|
|
19
|
+
function registerSchedulerServer(rootContainer) {
|
|
20
|
+
const token = uuid();
|
|
21
|
+
rootContainer.registerInstance(SchedulerInternalToken, {
|
|
22
|
+
value: token
|
|
23
|
+
});
|
|
24
|
+
const service = new BreeSchedulerService({
|
|
25
|
+
logger: consoleLogger,
|
|
26
|
+
onTrigger: async (id, namespace, tenant)=>{
|
|
27
|
+
console.log(`[scheduler] timer fired for "${id}" (namespace=${namespace}, tenant=${tenant}); calling run route`);
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(`${serverBase()}/scheduled-action-run`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
"content-type": "application/json",
|
|
33
|
+
[SCHEDULER_HEADER]: token
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify({
|
|
36
|
+
id,
|
|
37
|
+
namespace,
|
|
38
|
+
tenant
|
|
39
|
+
})
|
|
40
|
+
});
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
const body = await res.text().catch(()=>"");
|
|
43
|
+
console.error(`[scheduler] run route returned HTTP ${res.status} for "${id}": ${body}`);
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
47
|
+
console.error(`Scheduler trigger for "${id}" failed to reach the run route: ${message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
rootContainer.registerInstance(SchedulerSingleton, service);
|
|
52
|
+
rootContainer.registerInstance(SchedulerService, service);
|
|
53
|
+
rootContainer.register(ScheduledActionRunRouteDefinition);
|
|
54
|
+
rootContainer.register(ScheduledActionRecoverRouteDefinition);
|
|
55
|
+
}
|
|
56
|
+
async function startSchedulerServer(rootContainer) {
|
|
57
|
+
await rootContainer.resolve(SchedulerSingleton).start();
|
|
58
|
+
const token = rootContainer.resolve(SchedulerInternalToken).value;
|
|
59
|
+
console.log(`[scheduler] boot: re-arming persisted schedules via ${serverBase()} ...`);
|
|
60
|
+
setTimeout(async ()=>{
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch(`${serverBase()}/scheduled-action-recover`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
"content-type": "application/json",
|
|
66
|
+
[SCHEDULER_HEADER]: token
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify({})
|
|
69
|
+
});
|
|
70
|
+
const body = await res.json().catch(()=>({}));
|
|
71
|
+
if (!res.ok) return void console.error(`[scheduler] boot recovery failed: HTTP ${res.status}`, body);
|
|
72
|
+
console.log(`[scheduler] boot recovery: re-armed ${body.recovered ?? "?"} pending action(s)`);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
75
|
+
console.error(`[scheduler] boot recovery failed to reach the recover route: ${message}`);
|
|
76
|
+
}
|
|
77
|
+
}, 1000);
|
|
78
|
+
}
|
|
79
|
+
export { registerSchedulerServer, startSchedulerServer };
|
|
80
|
+
|
|
81
|
+
//# sourceMappingURL=schedulerServer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler/schedulerServer.js","sources":["../../src/scheduler/schedulerServer.ts"],"sourcesContent":["import type { Container } from \"@webiny/di\";\nimport { uuid } from \"@webiny/stdlib\";\nimport { SchedulerService } from \"@webiny/api-scheduler/shared/abstractions.js\";\nimport { BreeSchedulerService } from \"@webiny/api-scheduler-standalone\";\nimport type { Logger } from \"@webiny/api-core/features/logger/abstractions.js\";\nimport { SchedulerInternalToken } from \"./abstractions/InternalToken.js\";\nimport { SchedulerSingleton } from \"./abstractions/SchedulerSingleton.js\";\nimport { ScheduledActionRunRouteDefinition } from \"./ScheduledActionRunRoute.js\";\nimport { ScheduledActionRecoverRouteDefinition } from \"./ScheduledActionRecoverRoute.js\";\n\nconst SCHEDULER_HEADER = \"x-webiny-scheduler-token\";\n\n/**\n * Minimal console-backed logger for the root scheduler singleton. The real DI Logger is registered\n * per-request (via ApiCoreFeature/LoggerFeature), so it isn't resolvable here at the root where the\n * singleton is built. And even if we bridged to it, timers fire outside any request — there's no\n * request context for a context-aware logger to enrich — so console is functionally equivalent, not\n * a downgrade. Revisit (a shared boot-time logger for all root singletons) tracked in\n * https://github.com/webiny/webiny-js/issues/5446.\n */\nconst consoleLogger: Logger.Interface = {\n trace: (...args: any[]) => console.trace(...args),\n debug: (...args: any[]) => console.debug(...args),\n info: (...args: any[]) => console.info(...args),\n warn: (...args: any[]) => console.warn(...args),\n error: (...args: any[]) => console.error(...args),\n fatal: (...args: any[]) => console.error(...args),\n log: (...args: any[]) => console.log(...args)\n};\n\n// Self-callback base. process.env.PORT is the resolved listening port injected by spawnApiServer;\n// the 3002 fallback matches findFreePort's search base (and the background-tasks default) so every\n// self-callback agrees, though in practice PORT is always set. A shared helper + fail-loud handling\n// is tracked in https://github.com/webiny/webiny-js/issues/5448.\nconst serverBase = () => `http://localhost:${process.env.PORT || \"3002\"}`;\n\n/**\n * ROOT wiring for the standalone (Bree, in-process) scheduler. Unlike AWS (per-request EventBridge\n * binding), the server holds ONE long-lived Bree instance for all tenants, started once at boot — the\n * counterpart of the WebSockets connection manager. Registered as `SchedulerService` so per-request\n * create/update/delete (during GraphQL mutations) manipulate that single live timer set.\n *\n * When a timer fires (outside any request), the singleton POSTs to `/scheduled-action-run`, which\n * rebuilds the request context for the action's tenant and executes it (see the route).\n */\nexport function registerSchedulerServer(rootContainer: Container): void {\n const token = uuid();\n rootContainer.registerInstance(SchedulerInternalToken, { value: token });\n\n const service = new BreeSchedulerService({\n logger: consoleLogger,\n onTrigger: async (id, namespace, tenant) => {\n console.log(\n `[scheduler] timer fired for \"${id}\" (namespace=${namespace}, tenant=${tenant}); calling run route`\n );\n try {\n const res = await fetch(`${serverBase()}/scheduled-action-run`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", [SCHEDULER_HEADER]: token },\n body: JSON.stringify({ id, namespace, tenant })\n });\n // fetch only throws on network errors; a 403 or 500 comes back as a normal non-ok\n // response, so we check for that explicitly — otherwise a failed run would be silent.\n if (!res.ok) {\n const body = await res.text().catch(() => \"\");\n console.error(\n `[scheduler] run route returned HTTP ${res.status} for \"${id}\": ${body}`\n );\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.error(\n `Scheduler trigger for \"${id}\" failed to reach the run route: ${message}`\n );\n }\n }\n });\n\n // Register the ONE Bree instance under TWO tokens — two views of the same object:\n //\n // - SchedulerService: the hosting-agnostic contract (create/update/delete/exists) that AWS\n // implements too. This is what per-request GraphQL mutations resolve to schedule/reschedule.\n // - SchedulerSingleton: typed as the CONCRETE BreeSchedulerService, so it also exposes the\n // methods that aren't on that contract — start() and recover(). Those are Bree-only: AWS's\n // EventBridge is managed infra (nothing to start) and persists its own schedules (nothing to\n // recover), so they don't belong on the shared interface. The boot step + recover route resolve\n // this token precisely because they need start()/recover().\n //\n // registerInstance (not register) because it's a single live object holding all tenants' timers —\n // every caller must get the SAME instance, not a per-scope construction. Registered at root, so\n // per-request child containers inherit it via the parent chain; nothing registers a per-request\n // SchedulerService default that would shadow it (unlike e.g. the WS transport's Null default).\n rootContainer.registerInstance(SchedulerSingleton, service);\n rootContainer.registerInstance(SchedulerService, service);\n\n rootContainer.register(ScheduledActionRunRouteDefinition);\n rootContainer.register(ScheduledActionRecoverRouteDefinition);\n}\n\n/**\n * Boot step (onServer): start the timers, then re-arm persisted schedules by POSTing the recover route\n * (root tenant).\n *\n * `onServer` runs BEFORE the HTTP server starts listening, so the recover POST (which hits this same\n * server) is DEFERRED to the next tick — by then `createServerHandler` has returned and the runner has\n * called `.listen()`. Fire-and-forget: a recovery failure must never block or crash startup.\n */\nexport async function startSchedulerServer(rootContainer: Container): Promise<void> {\n await rootContainer.resolve(SchedulerSingleton).start();\n\n const token = rootContainer.resolve(SchedulerInternalToken).value;\n\n // Deferred: give the runner a moment to bind the listener before we call back into it.\n console.log(`[scheduler] boot: re-arming persisted schedules via ${serverBase()} ...`);\n setTimeout(async () => {\n try {\n const res = await fetch(`${serverBase()}/scheduled-action-recover`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", [SCHEDULER_HEADER]: token },\n body: JSON.stringify({})\n });\n const body = await res.json().catch(() => ({}) as Record<string, unknown>);\n // fetch only throws on network errors; a 403 or 500 comes back as a normal non-ok\n // response, so we check for it here (a token-mismatch 403 used to fail silently).\n if (!res.ok) {\n console.error(`[scheduler] boot recovery failed: HTTP ${res.status}`, body);\n return;\n }\n console.log(\n `[scheduler] boot recovery: re-armed ${body.recovered ?? \"?\"} pending action(s)`\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.error(\n `[scheduler] boot recovery failed to reach the recover route: ${message}`\n );\n }\n }, 1000);\n}\n"],"names":["SCHEDULER_HEADER","consoleLogger","args","console","serverBase","process","registerSchedulerServer","rootContainer","token","uuid","SchedulerInternalToken","service","BreeSchedulerService","id","namespace","tenant","res","fetch","JSON","body","err","message","Error","String","SchedulerSingleton","SchedulerService","ScheduledActionRunRouteDefinition","ScheduledActionRecoverRouteDefinition","startSchedulerServer","setTimeout"],"mappings":";;;;;;;AAUA,MAAMA,mBAAmB;AAUzB,MAAMC,gBAAkC;IACpC,OAAO,CAAC,GAAGC,OAAgBC,QAAQ,KAAK,IAAID;IAC5C,OAAO,CAAC,GAAGA,OAAgBC,QAAQ,KAAK,IAAID;IAC5C,MAAM,CAAC,GAAGA,OAAgBC,QAAQ,IAAI,IAAID;IAC1C,MAAM,CAAC,GAAGA,OAAgBC,QAAQ,IAAI,IAAID;IAC1C,OAAO,CAAC,GAAGA,OAAgBC,QAAQ,KAAK,IAAID;IAC5C,OAAO,CAAC,GAAGA,OAAgBC,QAAQ,KAAK,IAAID;IAC5C,KAAK,CAAC,GAAGA,OAAgBC,QAAQ,GAAG,IAAID;AAC5C;AAMA,MAAME,aAAa,IAAM,CAAC,iBAAiB,EAAEC,QAAQ,GAAG,CAAC,IAAI,IAAI,QAAQ;AAWlE,SAASC,wBAAwBC,aAAwB;IAC5D,MAAMC,QAAQC;IACdF,cAAc,gBAAgB,CAACG,wBAAwB;QAAE,OAAOF;IAAM;IAEtE,MAAMG,UAAU,IAAIC,qBAAqB;QACrC,QAAQX;QACR,WAAW,OAAOY,IAAIC,WAAWC;YAC7BZ,QAAQ,GAAG,CACP,CAAC,6BAA6B,EAAEU,GAAG,aAAa,EAAEC,UAAU,SAAS,EAAEC,OAAO,oBAAoB,CAAC;YAEvG,IAAI;gBACA,MAAMC,MAAM,MAAMC,MAAM,GAAGb,aAAa,qBAAqB,CAAC,EAAE;oBAC5D,QAAQ;oBACR,SAAS;wBAAE,gBAAgB;wBAAoB,CAACJ,iBAAiB,EAAEQ;oBAAM;oBACzE,MAAMU,KAAK,SAAS,CAAC;wBAAEL;wBAAIC;wBAAWC;oBAAO;gBACjD;gBAGA,IAAI,CAACC,IAAI,EAAE,EAAE;oBACT,MAAMG,OAAO,MAAMH,IAAI,IAAI,GAAG,KAAK,CAAC,IAAM;oBAC1Cb,QAAQ,KAAK,CACT,CAAC,oCAAoC,EAAEa,IAAI,MAAM,CAAC,MAAM,EAAEH,GAAG,GAAG,EAAEM,MAAM;gBAEhF;YACJ,EAAE,OAAOC,KAAK;gBACV,MAAMC,UAAUD,eAAeE,QAAQF,IAAI,OAAO,GAAGG,OAAOH;gBAC5DjB,QAAQ,KAAK,CACT,CAAC,uBAAuB,EAAEU,GAAG,iCAAiC,EAAEQ,SAAS;YAEjF;QACJ;IACJ;IAgBAd,cAAc,gBAAgB,CAACiB,oBAAoBb;IACnDJ,cAAc,gBAAgB,CAACkB,kBAAkBd;IAEjDJ,cAAc,QAAQ,CAACmB;IACvBnB,cAAc,QAAQ,CAACoB;AAC3B;AAUO,eAAeC,qBAAqBrB,aAAwB;IAC/D,MAAMA,cAAc,OAAO,CAACiB,oBAAoB,KAAK;IAErD,MAAMhB,QAAQD,cAAc,OAAO,CAACG,wBAAwB,KAAK;IAGjEP,QAAQ,GAAG,CAAC,CAAC,oDAAoD,EAAEC,aAAa,IAAI,CAAC;IACrFyB,WAAW;QACP,IAAI;YACA,MAAMb,MAAM,MAAMC,MAAM,GAAGb,aAAa,yBAAyB,CAAC,EAAE;gBAChE,QAAQ;gBACR,SAAS;oBAAE,gBAAgB;oBAAoB,CAACJ,iBAAiB,EAAEQ;gBAAM;gBACzE,MAAMU,KAAK,SAAS,CAAC,CAAC;YAC1B;YACA,MAAMC,OAAO,MAAMH,IAAI,IAAI,GAAG,KAAK,CAAC,IAAO,EAAC;YAG5C,IAAI,CAACA,IAAI,EAAE,EAAE,YACTb,QAAQ,KAAK,CAAC,CAAC,uCAAuC,EAAEa,IAAI,MAAM,EAAE,EAAEG;YAG1EhB,QAAQ,GAAG,CACP,CAAC,oCAAoC,EAAEgB,KAAK,SAAS,IAAI,IAAI,kBAAkB,CAAC;QAExF,EAAE,OAAOC,KAAK;YACV,MAAMC,UAAUD,eAAeE,QAAQF,IAAI,OAAO,GAAGG,OAAOH;YAC5DjB,QAAQ,KAAK,CACT,CAAC,6DAA6D,EAAEkB,SAAS;QAEjF;IACJ,GAAG;AACP"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Container } from "@webiny/di";
|
|
2
|
+
import { type RegisterApiRequestStackConfig } from "@webiny/api-event-handler-core";
|
|
3
|
+
import type { WebsocketsConnectionAuthenticator } from "@webiny/api-websockets-standalone";
|
|
4
|
+
/**
|
|
5
|
+
* Builds the `authenticate` callback the server WebSocket upgrade acceptor uses to turn a
|
|
6
|
+
* connection's `?token` JWT into an identity.
|
|
7
|
+
*
|
|
8
|
+
* Each new WebSocket connection (the HTTP upgrade handshake) is authenticated from its `?token` JWT
|
|
9
|
+
* so the connection is registered under the real identity — targeted server→client sends need this,
|
|
10
|
+
* since SendToIdentity matches connections by identity id. The catch is that AuthenticationContext
|
|
11
|
+
* lives in the per-request stack (registered by ApiCoreFeature, not registerRootStorage), so we can't
|
|
12
|
+
* resolve it from the root container. Instead we spin up a short-lived request-scoped child container,
|
|
13
|
+
* the same way createHandler does for each HTTP request, and resolve it there. We only need the core
|
|
14
|
+
* stack, not the transports, because this is just the token→identity step the HTTP stack already runs
|
|
15
|
+
* via RequestIdentityLoader — and that step doesn't depend on the tenant (the HTTP stack resolves
|
|
16
|
+
* identity before tenant), so there's no request or tenant state to set up. Connections are rare (one
|
|
17
|
+
* per admin session), so a fresh child per connection is cheap enough.
|
|
18
|
+
*
|
|
19
|
+
* This is also why the WebSocket server takes `authenticate` as a plain callback instead of resolving
|
|
20
|
+
* auth itself: AuthenticationContext isn't reachable from the root container (it's per-request), so
|
|
21
|
+
* only the handler — which can build a request-scoped child — can supply it. So authenticate has to be
|
|
22
|
+
* a callback the handler builds, regardless of DI.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createWebsocketsAuthenticator(rootContainer: Container, extensions: RegisterApiRequestStackConfig["extensions"]): WebsocketsConnectionAuthenticator;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { RequestContainer } from "@webiny/event-handler-core";
|
|
2
|
+
import { registerApiRequestStack } from "@webiny/api-event-handler-core";
|
|
3
|
+
import { AuthenticationContext } from "@webiny/api-core/features/security/authentication/AuthenticationContext/index.js";
|
|
4
|
+
function createWebsocketsAuthenticator(rootContainer, extensions) {
|
|
5
|
+
return async (token)=>{
|
|
6
|
+
const child = rootContainer.createChildContainer();
|
|
7
|
+
child.registerInstance(RequestContainer, child);
|
|
8
|
+
await registerApiRequestStack(child, {
|
|
9
|
+
extensions
|
|
10
|
+
});
|
|
11
|
+
const identity = await child.resolve(AuthenticationContext).authenticate(token);
|
|
12
|
+
if (!identity?.id) return null;
|
|
13
|
+
return {
|
|
14
|
+
id: identity.id,
|
|
15
|
+
displayName: identity.displayName,
|
|
16
|
+
type: identity.type
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export { createWebsocketsAuthenticator };
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=createWebsocketsAuthenticator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websockets/createWebsocketsAuthenticator.js","sources":["../../src/websockets/createWebsocketsAuthenticator.ts"],"sourcesContent":["import type { Container } from \"@webiny/di\";\nimport { RequestContainer } from \"@webiny/event-handler-core\";\nimport {\n registerApiRequestStack,\n type RegisterApiRequestStackConfig\n} from \"@webiny/api-event-handler-core\";\nimport { AuthenticationContext } from \"@webiny/api-core/features/security/authentication/AuthenticationContext/index.js\";\nimport type { WebsocketsConnectionAuthenticator } from \"@webiny/api-websockets-standalone\";\n\n/**\n * Builds the `authenticate` callback the server WebSocket upgrade acceptor uses to turn a\n * connection's `?token` JWT into an identity.\n *\n * Each new WebSocket connection (the HTTP upgrade handshake) is authenticated from its `?token` JWT\n * so the connection is registered under the real identity — targeted server→client sends need this,\n * since SendToIdentity matches connections by identity id. The catch is that AuthenticationContext\n * lives in the per-request stack (registered by ApiCoreFeature, not registerRootStorage), so we can't\n * resolve it from the root container. Instead we spin up a short-lived request-scoped child container,\n * the same way createHandler does for each HTTP request, and resolve it there. We only need the core\n * stack, not the transports, because this is just the token→identity step the HTTP stack already runs\n * via RequestIdentityLoader — and that step doesn't depend on the tenant (the HTTP stack resolves\n * identity before tenant), so there's no request or tenant state to set up. Connections are rare (one\n * per admin session), so a fresh child per connection is cheap enough.\n *\n * This is also why the WebSocket server takes `authenticate` as a plain callback instead of resolving\n * auth itself: AuthenticationContext isn't reachable from the root container (it's per-request), so\n * only the handler — which can build a request-scoped child — can supply it. So authenticate has to be\n * a callback the handler builds, regardless of DI.\n */\nexport function createWebsocketsAuthenticator(\n rootContainer: Container,\n extensions: RegisterApiRequestStackConfig[\"extensions\"]\n): WebsocketsConnectionAuthenticator {\n return async token => {\n const child = rootContainer.createChildContainer();\n child.registerInstance(RequestContainer, child);\n await registerApiRequestStack(child, { extensions });\n const identity = await child.resolve(AuthenticationContext).authenticate(token);\n if (!identity?.id) {\n return null;\n }\n return {\n id: identity.id,\n displayName: identity.displayName,\n type: identity.type\n };\n };\n}\n"],"names":["createWebsocketsAuthenticator","rootContainer","extensions","token","child","RequestContainer","registerApiRequestStack","identity","AuthenticationContext"],"mappings":";;;AA6BO,SAASA,8BACZC,aAAwB,EACxBC,UAAuD;IAEvD,OAAO,OAAMC;QACT,MAAMC,QAAQH,cAAc,oBAAoB;QAChDG,MAAM,gBAAgB,CAACC,kBAAkBD;QACzC,MAAME,wBAAwBF,OAAO;YAAEF;QAAW;QAClD,MAAMK,WAAW,MAAMH,MAAM,OAAO,CAACI,uBAAuB,YAAY,CAACL;QACzE,IAAI,CAACI,UAAU,IACX,OAAO;QAEX,OAAO;YACH,IAAIA,SAAS,EAAE;YACf,aAAaA,SAAS,WAAW;YACjC,MAAMA,SAAS,IAAI;QACvB;IACJ;AACJ"}
|