@plitzi/sdk-server 0.37.2 → 0.37.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/CHANGELOG.md +1170 -0
- package/README.md +100 -7
- package/dist/actions.d.ts +6 -2
- package/dist/actions.js +5 -2
- package/dist/adapters/cloudAdapters.js +4 -3
- package/dist/adapters/jsonAdapters.js +28 -6
- package/dist/adapters/jsonAdapters.test.d.ts +1 -0
- package/dist/auth.d.ts +7 -4
- package/dist/auth.js +8 -6
- package/dist/core/auth/api.d.ts +29 -1
- package/dist/core/auth/api.js +47 -13
- package/dist/core/auth/createAuth.d.ts +4 -0
- package/dist/core/auth/createAuth.js +3 -3
- package/dist/core/auth/credentials.d.ts +3 -0
- package/dist/core/auth/credentials.js +1 -1
- package/dist/core/auth/csrf.d.ts +25 -1
- package/dist/core/auth/csrf.js +29 -4
- package/dist/core/auth/renewal.d.ts +26 -0
- package/dist/core/auth/renewal.js +24 -0
- package/dist/core/auth/routes.js +8 -6
- package/dist/core/auth/session.d.ts +45 -0
- package/dist/core/auth/session.js +47 -1
- package/dist/core/auth/sessionActivity.d.ts +11 -0
- package/dist/core/auth/sessionActivity.js +14 -0
- package/dist/core/auth/throttle.d.ts +15 -0
- package/dist/core/auth/throttle.js +29 -1
- package/dist/core/auth/totp.d.ts +9 -1
- package/dist/core/auth/totp.js +11 -6
- package/dist/core/createServer.actions.test.d.ts +1 -0
- package/dist/core/createServer.d.ts +14 -1
- package/dist/core/createServer.functions.test.d.ts +1 -0
- package/dist/core/createServer.js +22 -12
- package/dist/core/fontStore.js +1 -1
- package/dist/core/handlers/authRouteHandlers.d.ts +1 -1
- package/dist/core/handlers/authRouteHandlers.js +57 -21
- package/dist/core/handlers/csrfMiddleware.d.ts +23 -0
- package/dist/core/handlers/csrfMiddleware.js +31 -7
- package/dist/core/handlers/csrfMiddleware.test.d.ts +1 -0
- package/dist/core/handlers/oauthRouteHandlers.js +5 -3
- package/dist/core/handlers/socialRouteHandlers.d.ts +1 -1
- package/dist/core/handlers/socialRouteHandlers.js +2 -1
- package/dist/core/http/dispatcher.d.ts +9 -1
- package/dist/core/http/dispatcher.js +38 -11
- package/dist/core/http/navigation.d.ts +5 -2
- package/dist/core/http/navigation.js +7 -4
- package/dist/core/http/socketResponse.d.ts +18 -0
- package/dist/core/http/socketResponse.js +59 -0
- package/dist/core/http/sse.d.ts +26 -0
- package/dist/core/http/sse.js +78 -0
- package/dist/core/http/stages/authRoutes.js +3 -1
- package/dist/core/http/stages/exchangeRoute.test.d.ts +1 -0
- package/dist/core/http/stages/middlewares.d.ts +1 -1
- package/dist/core/http/stages/middlewares.js +16 -10
- package/dist/core/http/stages/middlewares.test.d.ts +1 -0
- package/dist/core/http/stages/oauth.js +2 -2
- package/dist/core/http/stages/sessionRenewal.d.ts +16 -0
- package/dist/core/http/stages/sessionRenewal.js +81 -0
- package/dist/core/http/stages/sessionRenewal.test.d.ts +1 -0
- package/dist/core/http/stages/signInRoute.d.ts +2 -0
- package/dist/core/http/stages/signInRoute.js +139 -0
- package/dist/core/http/stages/signInRoute.test.d.ts +1 -0
- package/dist/core/http/types.d.ts +9 -0
- package/dist/core/http/webExchange.d.ts +18 -0
- package/dist/core/http/webExchange.js +53 -0
- package/dist/core/oauth/authorize.js +37 -13
- package/dist/core/oauth/consentPage.js +11 -0
- package/dist/core/oauth/issueContext.d.ts +14 -0
- package/dist/core/oauth/issueContext.js +32 -0
- package/dist/core/oauth/records.d.ts +7 -0
- package/dist/core/oauth/register.d.ts +2 -0
- package/dist/core/oauth/register.js +32 -11
- package/dist/core/oauth/register.test.d.ts +1 -0
- package/dist/core/oauth/token.js +21 -6
- package/dist/core/oauth/token.test.d.ts +1 -0
- package/dist/core/requestParser.d.ts +4 -0
- package/dist/core/requestParser.js +14 -7
- package/dist/core/sdkAssets.js +1 -1
- package/dist/core/server/baseServer.d.ts +10 -0
- package/dist/core/server/baseServer.js +42 -11
- package/dist/core/server/closeOnSignals.js +4 -3
- package/dist/core/server/drain.d.ts +18 -0
- package/dist/core/server/drain.js +62 -0
- package/dist/core/server/drain.test.d.ts +1 -0
- package/dist/core/server/fleet/channel.d.ts +103 -0
- package/dist/core/server/fleet/channel.js +191 -0
- package/dist/core/server/fleet/channel.test.d.ts +1 -0
- package/dist/core/server/fleet/fixtures/fleetServer.d.ts +1 -0
- package/dist/core/server/fleet/fleet.integration.test.d.ts +1 -0
- package/dist/core/server/fleet/link.d.ts +23 -0
- package/dist/core/server/fleet/link.js +70 -0
- package/dist/core/server/fleet/relay.d.ts +11 -0
- package/dist/core/server/fleet/relay.js +104 -0
- package/dist/core/server/fleet/relay.test.d.ts +1 -0
- package/dist/core/server/fleet/role.d.ts +46 -0
- package/dist/core/server/fleet/role.js +59 -0
- package/dist/core/server/fleet/role.test.d.ts +1 -0
- package/dist/core/server/fleet/stores.d.ts +7 -0
- package/dist/core/server/fleet/stores.js +19 -0
- package/dist/core/server/fleet/supervisor.d.ts +26 -0
- package/dist/core/server/fleet/supervisor.js +108 -0
- package/dist/core/server/fleet/supervisor.test.d.ts +1 -0
- package/dist/core/server/pageServer.js +34 -5
- package/dist/core/services/action.js +10 -2
- package/dist/core/services/functionRoutes.d.ts +9 -0
- package/dist/core/services/functionRoutes.js +47 -0
- package/dist/core/services/realtime.d.ts +8 -0
- package/dist/core/services/realtime.js +57 -0
- package/dist/core/services/registry.js +6 -2
- package/dist/core/staticFiles.js +12 -4
- package/dist/core/staticFiles.test.d.ts +1 -0
- package/dist/core/transports.d.ts +13 -3
- package/dist/core/transports.js +14 -5
- package/dist/functions-api.d.ts +281 -0
- package/dist/functions.d.ts +19 -0
- package/dist/functions.js +5 -0
- package/dist/functionsRunner.d.ts +15 -0
- package/dist/functionsRunner.js +6 -0
- package/dist/handlers.d.ts +2 -2
- package/dist/handlers.js +2 -2
- package/dist/helpers/atomicFile.d.ts +10 -0
- package/dist/helpers/atomicFile.js +33 -0
- package/dist/helpers/atomicFile.test.d.ts +1 -0
- package/dist/helpers/buildResponseHelpers.js +35 -11
- package/dist/helpers/buildServerInfo.js +4 -2
- package/dist/helpers/buildServerInfo.test.d.ts +1 -0
- package/dist/helpers/cache/CompressedFileCache.d.ts +18 -0
- package/dist/helpers/cache/CompressedFileCache.js +44 -0
- package/dist/helpers/cache/CompressedFileCache.test.d.ts +1 -0
- package/dist/helpers/cache/cacheManager.d.ts +1 -1
- package/dist/helpers/cache/cacheManager.js +2 -1
- package/dist/helpers/cache/cacheManager.test.d.ts +1 -0
- package/dist/helpers/cache/index.d.ts +1 -1
- package/dist/helpers/cache/keys.d.ts +15 -1
- package/dist/helpers/cache/keys.js +47 -5
- package/dist/helpers/cache/serverCaches.d.ts +7 -1
- package/dist/helpers/compress.d.ts +3 -1
- package/dist/helpers/compress.js +11 -3
- package/dist/helpers/hydrationPayload.d.ts +11 -0
- package/dist/helpers/hydrationPayload.js +51 -0
- package/dist/helpers/hydrationPayload.test.d.ts +1 -0
- package/dist/helpers/metrics.d.ts +1 -1
- package/dist/helpers/metrics.js +3 -2
- package/dist/helpers/outboundGuard.d.ts +14 -6
- package/dist/helpers/outboundGuard.js +132 -8
- package/dist/helpers/paintedCookie.d.ts +8 -0
- package/dist/helpers/paintedCookie.js +13 -0
- package/dist/helpers/reportReactBuild.d.ts +9 -0
- package/dist/helpers/reportReactBuild.js +18 -0
- package/dist/helpers/reportReactBuild.test.d.ts +1 -0
- package/dist/helpers/serverLog.d.ts +25 -1
- package/dist/helpers/serverLog.js +68 -3
- package/dist/helpers/serverLog.test.d.ts +1 -0
- package/dist/helpers/wsFrame.d.ts +3 -0
- package/dist/helpers/wsFrame.js +5 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -2
- package/dist/kernel.d.ts +2 -1
- package/dist/kernel.js +3 -2
- package/dist/modules/actions/connectorWrite.js +2 -1
- package/dist/modules/actions/index.d.ts +29 -10
- package/dist/modules/actions/index.js +43 -6
- package/dist/modules/actions/jobs/failureStreak.d.ts +17 -0
- package/dist/modules/actions/jobs/failureStreak.js +47 -0
- package/dist/modules/actions/jobs/failureStreak.test.d.ts +1 -0
- package/dist/modules/actions/jobs/index.d.ts +2 -1
- package/dist/modules/actions/jobs/index.js +5 -3
- package/dist/modules/actions/jobs/memoryQueue.d.ts +3 -0
- package/dist/modules/actions/jobs/memoryQueue.js +17 -1
- package/dist/modules/actions/jobs/scheduler.d.ts +2 -0
- package/dist/modules/actions/jobs/scheduler.js +12 -7
- package/dist/modules/actions/jobs/worker.d.ts +3 -1
- package/dist/modules/actions/jobs/worker.js +9 -4
- package/dist/modules/actions/moduleFor.js +5 -2
- package/dist/modules/actions/runtime/errors.d.ts +12 -0
- package/dist/modules/actions/runtime/errors.js +16 -1
- package/dist/modules/actions/runtime/kvChange.d.ts +27 -0
- package/dist/modules/actions/runtime/kvChange.js +23 -0
- package/dist/modules/actions/runtime/kvList.d.ts +51 -0
- package/dist/modules/actions/runtime/kvList.js +52 -0
- package/dist/modules/actions/runtime/kvStore.d.ts +8 -0
- package/dist/modules/actions/runtime/kvStore.js +46 -20
- package/dist/modules/actions/runtime/memoryKv.d.ts +3 -0
- package/dist/modules/actions/runtime/memoryKv.js +19 -1
- package/dist/modules/actions/runtime/namespaceKv.d.ts +4 -6
- package/dist/modules/actions/runtime/namespaceKv.js +15 -3
- package/dist/modules/actions/runtime/namespaceKv.test.d.ts +1 -0
- package/dist/modules/actions/runtime/precheck.js +4 -8
- package/dist/modules/actions/runtime/rateLimit.d.ts +19 -0
- package/dist/modules/actions/runtime/rateLimit.js +19 -0
- package/dist/modules/actions/runtime/redisKv.d.ts +27 -0
- package/dist/modules/actions/runtime/redisKv.integration.test.d.ts +1 -0
- package/dist/modules/actions/runtime/redisKv.js +46 -0
- package/dist/modules/actions/runtime/report.js +2 -1
- package/dist/modules/actions/runtime/runAction.d.ts +10 -2
- package/dist/modules/actions/runtime/runAction.js +52 -116
- package/dist/modules/actions/runtime/runLogger.d.ts +5 -0
- package/dist/modules/actions/runtime/runLogger.js +8 -2
- package/dist/modules/actions/runtime/signing.d.ts +20 -0
- package/dist/modules/actions/runtime/signing.js +25 -0
- package/dist/modules/actions/runtime/signing.test.d.ts +1 -0
- package/dist/modules/actions/runtime/taskContext.d.ts +33 -0
- package/dist/modules/actions/runtime/taskContext.js +124 -0
- package/dist/modules/actions/tasks/builtins.js +5 -1
- package/dist/modules/actions/tasks/flow.js +81 -10
- package/dist/modules/actions/tasks/helpers.js +2 -2
- package/dist/modules/actions/tasks/http.js +2 -3
- package/dist/modules/actions/tasks/kv.js +39 -0
- package/dist/modules/actions/tasks/list.d.ts +2 -0
- package/dist/modules/actions/tasks/list.js +145 -0
- package/dist/modules/actions/tasks/realtime.d.ts +2 -0
- package/dist/modules/actions/tasks/realtime.js +104 -0
- package/dist/modules/actions/tasks/registry.d.ts +10 -1
- package/dist/modules/actions/tasks/registry.js +17 -6
- package/dist/modules/actions/transport/callHandler.js +5 -2
- package/dist/modules/actions/transport/catalogHandler.d.ts +5 -2
- package/dist/modules/actions/transport/catalogHandler.js +3 -2
- package/dist/modules/actions/transport/stream.d.ts +4 -5
- package/dist/modules/actions/transport/stream.js +12 -76
- package/dist/modules/actions/transport/webhookHandler.js +4 -3
- package/dist/modules/actions/types.d.ts +86 -3
- package/dist/modules/connectors/engine.js +3 -5
- package/dist/modules/functions/build.d.ts +22 -0
- package/dist/modules/functions/build.js +122 -0
- package/dist/modules/functions/build.test.d.ts +1 -0
- package/dist/modules/functions/capabilities.d.ts +22 -0
- package/dist/modules/functions/capabilities.js +228 -0
- package/dist/modules/functions/config.d.ts +34 -0
- package/dist/modules/functions/config.js +24 -0
- package/dist/modules/functions/context.d.ts +17 -0
- package/dist/modules/functions/context.js +88 -0
- package/dist/modules/functions/contract.d.ts +121 -0
- package/dist/modules/functions/contract.js +9 -0
- package/dist/modules/functions/driver.d.ts +36 -0
- package/dist/modules/functions/driver.js +150 -0
- package/dist/modules/functions/load.d.ts +15 -0
- package/dist/modules/functions/load.js +45 -0
- package/dist/modules/functions/load.test.d.ts +1 -0
- package/dist/modules/functions/local.d.ts +29 -0
- package/dist/modules/functions/local.js +61 -0
- package/dist/modules/functions/local.test.d.ts +1 -0
- package/dist/modules/functions/manifest.d.ts +24 -0
- package/dist/modules/functions/manifest.js +163 -0
- package/dist/modules/functions/manifest.test.d.ts +1 -0
- package/dist/modules/functions/native.d.ts +8 -0
- package/dist/modules/functions/native.js +21 -0
- package/dist/modules/functions/native.test.d.ts +1 -0
- package/dist/modules/functions/protocol.d.ts +216 -0
- package/dist/modules/functions/protocol.js +18 -0
- package/dist/modules/functions/routes.d.ts +36 -0
- package/dist/modules/functions/routes.js +181 -0
- package/dist/modules/functions/routes.test.d.ts +1 -0
- package/dist/modules/functions/runner/messages.d.ts +5 -0
- package/dist/modules/functions/runner/messages.js +188 -0
- package/dist/modules/functions/runner/remote.d.ts +19 -0
- package/dist/modules/functions/runner/remote.js +116 -0
- package/dist/modules/functions/runner/remote.test.d.ts +1 -0
- package/dist/modules/functions/runner/service.d.ts +32 -0
- package/dist/modules/functions/runner/service.js +209 -0
- package/dist/modules/functions/sandbox/guest.d.ts +45 -0
- package/dist/modules/functions/sandbox/guest.js +660 -0
- package/dist/modules/functions/sandbox/isolate.d.ts +37 -0
- package/dist/modules/functions/sandbox/isolate.js +347 -0
- package/dist/modules/functions/sandbox/isolate.test.d.ts +1 -0
- package/dist/modules/functions/sandbox/prelude.d.ts +2 -0
- package/dist/modules/functions/sandbox/prelude.js +49 -0
- package/dist/modules/functions/space.d.ts +40 -0
- package/dist/modules/functions/space.js +162 -0
- package/dist/modules/functions/testing/tasksOf.d.ts +4 -0
- package/dist/modules/functions/tryEntry.d.ts +9 -0
- package/dist/modules/functions/tryEntry.js +49 -0
- package/dist/modules/mongo/kv.js +31 -0
- package/dist/modules/mysql/accounts.js +19 -9
- package/dist/modules/mysql/index.d.ts +2 -1
- package/dist/modules/mysql/jobs/index.d.ts +1 -1
- package/dist/modules/mysql/jobs/kv.js +30 -0
- package/dist/modules/mysql/jobs/schema.d.ts +9 -0
- package/dist/modules/mysql/jobs/schema.js +21 -4
- package/dist/modules/mysql/schema.js +25 -0
- package/dist/modules/mysql/sessions.d.ts +1 -11
- package/dist/modules/mysql/sessions.js +6 -1
- package/dist/modules/realtime/declarations.d.ts +11 -0
- package/dist/modules/realtime/declarations.js +26 -0
- package/dist/modules/realtime/grants.d.ts +23 -0
- package/dist/modules/realtime/grants.js +61 -0
- package/dist/modules/realtime/handlers.d.ts +74 -0
- package/dist/modules/realtime/handlers.js +224 -0
- package/dist/modules/realtime/hub.d.ts +62 -0
- package/dist/modules/realtime/hub.js +141 -0
- package/dist/modules/realtime/index.d.ts +51 -0
- package/dist/modules/realtime/index.js +62 -0
- package/dist/modules/realtime/memoryPubSub.d.ts +10 -0
- package/dist/modules/realtime/memoryPubSub.js +43 -0
- package/dist/modules/realtime/realtime.test.d.ts +1 -0
- package/dist/modules/realtime/redisPubSub.d.ts +24 -0
- package/dist/modules/realtime/redisPubSub.js +36 -0
- package/dist/modules/realtime/redisPubSub.test.d.ts +1 -0
- package/dist/modules/realtime/socket.d.ts +25 -0
- package/dist/modules/realtime/socket.js +122 -0
- package/dist/modules/realtime/socket.test.d.ts +1 -0
- package/dist/modules/rsc/handler.d.ts +16 -0
- package/dist/modules/rsc/handler.js +17 -10
- package/dist/modules/rsc/handler.test.d.ts +1 -0
- package/dist/modules/rsc/resolveRscData.js +2 -1
- package/dist/modules/runtime/bundle.d.ts +24 -0
- package/dist/modules/runtime/bundle.js +102 -0
- package/dist/modules/runtime/contract.d.ts +37 -0
- package/dist/modules/runtime/contract.js +21 -0
- package/dist/modules/runtime/entry.test.d.ts +1 -0
- package/dist/modules/runtime/host.d.ts +38 -0
- package/dist/modules/runtime/host.js +68 -0
- package/dist/modules/runtime/inside.d.ts +15 -0
- package/dist/modules/runtime/inside.js +47 -0
- package/dist/modules/runtime/inside.test.d.ts +1 -0
- package/dist/modules/runtime/runner.d.ts +11 -0
- package/dist/modules/runtime/runner.js +38 -0
- package/dist/modules/runtime/runtime.test.d.ts +1 -0
- package/dist/modules/runtime/stages.d.ts +50 -0
- package/dist/modules/runtime/stages.js +117 -0
- package/dist/modules/ssr/Component.d.ts +3 -1
- package/dist/modules/ssr/Component.js +2 -1
- package/dist/modules/ssr/handler.js +10 -2
- package/dist/modules/ssr/loadPluginComponents.js +2 -1
- package/dist/modules/ssr/prepareRender.js +39 -10
- package/dist/modules/ssr/preview.d.ts +3 -0
- package/dist/modules/ssr/preview.js +17 -7
- package/dist/modules/ssr/redirect.test.d.ts +1 -0
- package/dist/modules/ssr/registerExternalPlugins.d.ts +6 -1
- package/dist/modules/ssr/registerExternalPlugins.js +19 -16
- package/dist/modules/ssr/registerExternalPlugins.test.d.ts +1 -0
- package/dist/modules/ssr/reportMissingPlugins.d.ts +9 -0
- package/dist/modules/ssr/reportMissingPlugins.js +24 -0
- package/dist/modules/ssr/reportMissingPlugins.test.d.ts +1 -0
- package/dist/modules/ssr/streamBody.d.ts +2 -2
- package/dist/modules/ssr/streamBody.js +4 -1
- package/dist/modules/ssr/template.js +1 -1
- package/dist/modules/ssr/views/template.ejs +8 -5
- package/dist/mysql.d.ts +1 -1
- package/dist/mysql.js +2 -2
- package/dist/oauth.js +2 -2
- package/dist/plugins/compile.js +9 -4
- package/dist/plugins/copy.js +3 -3
- package/dist/plugins/manager.d.ts +35 -3
- package/dist/plugins/manager.js +109 -38
- package/dist/runtime.d.ts +15 -0
- package/dist/runtime.js +7 -0
- package/package.json +39 -8
- package/skills/plitzi-authoring/SKILL.md +98 -14
- package/skills/plitzi-authoring/reference/accessibility.md +111 -0
- package/skills/plitzi-authoring/reference/authoring-errors.md +70 -0
- package/skills/plitzi-authoring/reference/data-and-visibility.md +84 -18
- package/skills/plitzi-authoring/reference/elements-and-styles.md +61 -7
- package/skills/plitzi-authoring/reference/flows.md +108 -17
- package/skills/plitzi-authoring/reference/lists.md +74 -0
- package/skills/plitzi-authoring/reference/performance.md +67 -0
- package/skills/plitzi-authoring/reference/plugins.md +167 -0
- package/skills/plitzi-authoring/reference/review-checklist.md +19 -2
- package/skills/plitzi-authoring/reference/templates.md +67 -27
- package/skills/plitzi-authoring/reference/testing.md +72 -5
- package/skills/plitzi-authoring/reference/validation.md +73 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ChannelResolver } from './declarations';
|
|
2
|
+
import { RealtimeGrants } from './grants';
|
|
3
|
+
import { RealtimeHub } from './hub';
|
|
4
|
+
import { UpgradeRequest } from '../../core/http/socketResponse';
|
|
5
|
+
import { SSRRequest, SSRResponseHelpers } from '@plitzi/sdk-shared';
|
|
6
|
+
import { IncomingMessage } from 'node:http';
|
|
7
|
+
export type SocketDeps = {
|
|
8
|
+
req: SSRRequest;
|
|
9
|
+
res: SSRResponseHelpers;
|
|
10
|
+
raw: IncomingMessage;
|
|
11
|
+
upgrade: UpgradeRequest;
|
|
12
|
+
hub: RealtimeHub;
|
|
13
|
+
resolveChannels: ChannelResolver;
|
|
14
|
+
grants: RealtimeGrants;
|
|
15
|
+
allowedOrigins: readonly string[];
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* A page's realtime connection as a WebSocket: the same topics, rules and messages as the Server-Sent Events one, in
|
|
19
|
+
* both directions over one socket. A publish is a frame, not a request — no headers, no second connection, no token
|
|
20
|
+
* to present: the socket is the credential. Its answer arrives as an `ack` carrying the publish's `id`.
|
|
21
|
+
*
|
|
22
|
+
* Refused the way the stream is — a `403` or a `422`, in HTTP, before anything switches — so a page falls back or
|
|
23
|
+
* reports it exactly as it would there.
|
|
24
|
+
*/
|
|
25
|
+
export declare const handleRealtimeSocket: ({ req, res, raw, upgrade, hub, resolveChannels, grants, allowedOrigins }: SocketDeps) => Promise<void>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { admit, answer, connectionFor, publishFrom } from "./handlers.js";
|
|
2
|
+
import { requestOrigin } from "../../core/requestParser.js";
|
|
3
|
+
import { frameText } from "../../helpers/wsFrame.js";
|
|
4
|
+
//#region src/modules/realtime/socket.ts
|
|
5
|
+
/** The largest frame a page may send: well above the largest message a channel may declare, and bounded. */
|
|
6
|
+
var MAX_FRAME_BYTES = 262144;
|
|
7
|
+
/**
|
|
8
|
+
* What may wait in a socket's buffer for a page that stopped reading. Past it the page is gone in all but name, and
|
|
9
|
+
* holding everything it has not read is how one stalled tab grows a process.
|
|
10
|
+
*/
|
|
11
|
+
var MAX_BUFFERED_BYTES = 1048576;
|
|
12
|
+
/** How often the server asks a socket whether the page behind it is still there — and gives up on one that did not answer the last time. */
|
|
13
|
+
var PING_MS = 25e3;
|
|
14
|
+
var server;
|
|
15
|
+
/**
|
|
16
|
+
* Loaded the first time a page asks for a socket: a server whose pages never do — or that runs behind HTTP/2, where
|
|
17
|
+
* none can — never pays for the module.
|
|
18
|
+
*/
|
|
19
|
+
var socketServer = () => server ??= import("ws").then(({ WebSocketServer: Server }) => new Server({
|
|
20
|
+
noServer: true,
|
|
21
|
+
maxPayload: MAX_FRAME_BYTES
|
|
22
|
+
}));
|
|
23
|
+
/**
|
|
24
|
+
* A browser opens a WebSocket to any address, carrying that address's cookies, from any page — CORS does not apply to
|
|
25
|
+
* it. So a socket is refused unless the page that opened it is on this origin, or on one the deployment lists: without
|
|
26
|
+
* this, another site could open a channel restricted to signed-in visitors with a visitor's session and read it.
|
|
27
|
+
* A client that is not a browser sends no `Origin`, and has no visitor's cookies to borrow.
|
|
28
|
+
*/
|
|
29
|
+
var originAllowed = (raw, req, allowed) => {
|
|
30
|
+
const origin = raw.headers.origin;
|
|
31
|
+
return origin === void 0 || origin === requestOrigin(req) || allowed.includes(origin);
|
|
32
|
+
};
|
|
33
|
+
/** A frame's text, whichever of the shapes `ws` hands a frame over in. */
|
|
34
|
+
var parse = (frame) => {
|
|
35
|
+
try {
|
|
36
|
+
const value = JSON.parse(frameText(frame));
|
|
37
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
38
|
+
} catch {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
/** A publish's id, echoed on its `ack`: what the page matches the answer to. Short, and never anything else. */
|
|
43
|
+
var ackId = (id) => typeof id === "number" && Number.isInteger(id) || typeof id === "string" && id.length <= 64 ? id : void 0;
|
|
44
|
+
/**
|
|
45
|
+
* A page's realtime connection as a WebSocket: the same topics, rules and messages as the Server-Sent Events one, in
|
|
46
|
+
* both directions over one socket. A publish is a frame, not a request — no headers, no second connection, no token
|
|
47
|
+
* to present: the socket is the credential. Its answer arrives as an `ack` carrying the publish's `id`.
|
|
48
|
+
*
|
|
49
|
+
* Refused the way the stream is — a `403` or a `422`, in HTTP, before anything switches — so a page falls back or
|
|
50
|
+
* reports it exactly as it would there.
|
|
51
|
+
*/
|
|
52
|
+
var handleRealtimeSocket = async ({ req, res, raw, upgrade, hub, resolveChannels, grants, allowedOrigins }) => {
|
|
53
|
+
if (!originAllowed(raw, req, allowedOrigins)) {
|
|
54
|
+
answer(res, {
|
|
55
|
+
status: 403,
|
|
56
|
+
error: "A page on another site may not open this",
|
|
57
|
+
reason: "foreign"
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const admission = await admit(req, resolveChannels, grants);
|
|
62
|
+
if (!admission.ok) {
|
|
63
|
+
answer(res, admission.answer);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const sockets = await socketServer();
|
|
67
|
+
upgrade.taken = true;
|
|
68
|
+
res.setStatus(101);
|
|
69
|
+
sockets.handleUpgrade(raw, upgrade.socket, upgrade.head, (socket) => {
|
|
70
|
+
const send = (event, data) => {
|
|
71
|
+
if (socket.readyState !== socket.OPEN) return;
|
|
72
|
+
if (socket.bufferedAmount > MAX_BUFFERED_BYTES) {
|
|
73
|
+
socket.terminate();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
socket.send(JSON.stringify({
|
|
77
|
+
event,
|
|
78
|
+
data
|
|
79
|
+
}));
|
|
80
|
+
};
|
|
81
|
+
const connection = connectionFor(admission, send, () => socket.close(1001, "The server is going away"));
|
|
82
|
+
let alive = true;
|
|
83
|
+
const heartbeat = setInterval(() => {
|
|
84
|
+
if (!alive) {
|
|
85
|
+
socket.terminate();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
alive = false;
|
|
89
|
+
socket.ping();
|
|
90
|
+
}, PING_MS);
|
|
91
|
+
socket.on("pong", () => {
|
|
92
|
+
alive = true;
|
|
93
|
+
});
|
|
94
|
+
socket.on("message", (frame, binary) => {
|
|
95
|
+
alive = true;
|
|
96
|
+
const message = binary ? void 0 : parse(frame);
|
|
97
|
+
if (!message) return;
|
|
98
|
+
const id = ackId(message.id);
|
|
99
|
+
publishFrom(hub, connection, message).then((result) => {
|
|
100
|
+
if (id !== void 0) send("ack", {
|
|
101
|
+
id,
|
|
102
|
+
ok: result.status === 204,
|
|
103
|
+
status: result.status,
|
|
104
|
+
...result.reason ? { reason: result.reason } : {}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
socket.on("close", () => {
|
|
109
|
+
clearInterval(heartbeat);
|
|
110
|
+
hub.disconnect(connection);
|
|
111
|
+
});
|
|
112
|
+
socket.on("error", () => socket.terminate());
|
|
113
|
+
send("ready", {
|
|
114
|
+
connection: connection.id,
|
|
115
|
+
topics: [...admission.accepted.keys()],
|
|
116
|
+
refused: admission.refused
|
|
117
|
+
});
|
|
118
|
+
hub.connect(connection);
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
//#endregion
|
|
122
|
+
export { handleRealtimeSocket };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { TtlCache } from '../../helpers/cache';
|
|
2
2
|
import { PluginManager } from '../../plugins/manager';
|
|
3
3
|
import { SSRPageServerConfig, SSRRequest, SSRResponseHelpers } from '@plitzi/sdk-shared';
|
|
4
|
+
/**
|
|
5
|
+
* Rewrites the request to the page the browser is actually on.
|
|
6
|
+
*
|
|
7
|
+
* A refresh is issued against `/_rsc`, not against `/blog/my-post`, so resolving straight from `req.path` matches
|
|
8
|
+
* nothing in `schema.pages` and every client-side refresh silently returns no data. The SDK sends the visitor's
|
|
9
|
+
* location as `?location=`, and route params plus the page parameter are read from that instead.
|
|
10
|
+
*
|
|
11
|
+
* What a refresh asks for besides — the next page of a "load more", a search a provider was reloaded with — rides on
|
|
12
|
+
* the query string beside `location`, and is read over the location's own query. It used to be dropped with the rest
|
|
13
|
+
* of the endpoint's query, so a provider paged in place was answered its first page every time.
|
|
14
|
+
*
|
|
15
|
+
* Reflecting both is safe by construction: the location is matched against this space's own page list, and the rest
|
|
16
|
+
* is what the same visitor could have put in the page's own address; the cache key is the whole request, so no answer
|
|
17
|
+
* is served for a query it was not made for.
|
|
18
|
+
*/
|
|
19
|
+
export declare const withPageLocation: (req: SSRRequest) => SSRRequest;
|
|
4
20
|
/**
|
|
5
21
|
* Handles GET /_rsc requests.
|
|
6
22
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { serverLog } from "../../helpers/serverLog.js";
|
|
1
2
|
import { resolveDebugAuthorization } from "../../helpers/debugAuthorization.js";
|
|
2
3
|
import { DEFAULT_TTL_MS } from "../../helpers/cache/defaults.js";
|
|
3
4
|
import { buildRscCacheKey } from "../../helpers/cache/keys.js";
|
|
@@ -19,6 +20,8 @@ var meterRsc = async (req, config, spaceId, environment, revision, cached) => {
|
|
|
19
20
|
});
|
|
20
21
|
} catch {}
|
|
21
22
|
};
|
|
23
|
+
/** The endpoint's own parameters: what it is asked about, never input to what it resolves. */
|
|
24
|
+
var ENDPOINT_PARAMS = /* @__PURE__ */ new Set(["location", "ids"]);
|
|
22
25
|
/**
|
|
23
26
|
* Rewrites the request to the page the browser is actually on.
|
|
24
27
|
*
|
|
@@ -26,22 +29,26 @@ var meterRsc = async (req, config, spaceId, environment, revision, cached) => {
|
|
|
26
29
|
* nothing in `schema.pages` and every client-side refresh silently returns no data. The SDK sends the visitor's
|
|
27
30
|
* location as `?location=`, and route params plus the page parameter are read from that instead.
|
|
28
31
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
32
|
+
* What a refresh asks for besides — the next page of a "load more", a search a provider was reloaded with — rides on
|
|
33
|
+
* the query string beside `location`, and is read over the location's own query. It used to be dropped with the rest
|
|
34
|
+
* of the endpoint's query, so a provider paged in place was answered its first page every time.
|
|
35
|
+
*
|
|
36
|
+
* Reflecting both is safe by construction: the location is matched against this space's own page list, and the rest
|
|
37
|
+
* is what the same visitor could have put in the page's own address; the cache key is the whole request, so no answer
|
|
38
|
+
* is served for a query it was not made for.
|
|
31
39
|
*/
|
|
32
40
|
var withPageLocation = (req) => {
|
|
33
41
|
const location = req.query.location;
|
|
34
42
|
if (!location || !location.startsWith("/") || location.length > MAX_PAGE_LOCATION) return req;
|
|
35
43
|
const url = new URL(location, `${req.protocol}://${req.hostname}`);
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}, {});
|
|
44
|
+
const asked = Object.entries(req.query).filter(([key]) => !ENDPOINT_PARAMS.has(key));
|
|
45
|
+
const query = Object.fromEntries([...url.searchParams.entries(), ...asked]);
|
|
46
|
+
const search = new URLSearchParams(query).toString();
|
|
40
47
|
return {
|
|
41
48
|
...req,
|
|
42
49
|
path: url.pathname,
|
|
43
|
-
search:
|
|
44
|
-
url: `${url.pathname}${
|
|
50
|
+
search: search ? `?${search}` : "",
|
|
51
|
+
url: `${url.pathname}${search ? `?${search}` : ""}`,
|
|
45
52
|
query
|
|
46
53
|
};
|
|
47
54
|
};
|
|
@@ -112,7 +119,7 @@ var handleRsc = async (req, res, config, _pluginManager, cache) => {
|
|
|
112
119
|
loadOfflineData
|
|
113
120
|
});
|
|
114
121
|
} catch (err) {
|
|
115
|
-
|
|
122
|
+
serverLog.error("RSC", "getRscData error", err);
|
|
116
123
|
res.setStatus(500);
|
|
117
124
|
res.send(JSON.stringify({ error: "RSC data fetch failed" }));
|
|
118
125
|
return;
|
|
@@ -135,4 +142,4 @@ var handleRsc = async (req, res, config, _pluginManager, cache) => {
|
|
|
135
142
|
res.send(payloadStr);
|
|
136
143
|
};
|
|
137
144
|
//#endregion
|
|
138
|
-
export { handleRsc };
|
|
145
|
+
export { handleRsc, withPageLocation };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { serverLog } from "../../helpers/serverLog.js";
|
|
1
2
|
import { matchRscPage } from "./matchRscPage.js";
|
|
2
3
|
import { collectServerElements } from "@plitzi/sdk-shared/schema/serverElements";
|
|
3
4
|
//#region src/modules/rsc/resolveRscData.ts
|
|
@@ -52,7 +53,7 @@ var resolveRscData = async ({ schema, req, spaceId, environment, user, ids, reso
|
|
|
52
53
|
}), timeoutMs, `element ${element.id}`)
|
|
53
54
|
})))).reduce((acum, result, index) => {
|
|
54
55
|
if (result.status === "rejected") {
|
|
55
|
-
|
|
56
|
+
serverLog.error("RSC", `element ${targets[index].id} failed to resolve`, result.reason);
|
|
56
57
|
return acum;
|
|
57
58
|
}
|
|
58
59
|
if (result.value.data !== void 0) acum[result.value.id] = result.value.data;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { SpaceRuntime } from './contract';
|
|
2
|
+
/** A packed runtime at most this big: its code and every dependency, not its data. */
|
|
3
|
+
export declare const MAX_RUNTIME_BUNDLE_BYTES: number;
|
|
4
|
+
/** A bundle's id: what its bytes are, so the same code packed twice is one bundle. */
|
|
5
|
+
export declare const runtimeBundleId: (bytes: Uint8Array) => string;
|
|
6
|
+
/**
|
|
7
|
+
* A runtime's module packed to travel: bundled for Node with every dependency it imports — but the platform's packages
|
|
8
|
+
* and React, which the host provides, so there is one copy of each — and gzipped. `entry` is the module whose default
|
|
9
|
+
* export is `defineRuntime(…)`.
|
|
10
|
+
*/
|
|
11
|
+
export declare const packRuntime: (entry: string) => Promise<Uint8Array>;
|
|
12
|
+
/**
|
|
13
|
+
* What a packed runtime holds, read without running any of it — what a platform checks a bundle with before keeping it.
|
|
14
|
+
* Throws, saying why, for bytes that are not one.
|
|
15
|
+
*/
|
|
16
|
+
export declare const inspectRuntime: (bytes: Uint8Array) => {
|
|
17
|
+
entry: string;
|
|
18
|
+
files: string[];
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* A packed runtime written into `dir` and imported: its module's default export. `dir` must be inside the host's own
|
|
22
|
+
* project, so the packages the bundle leaves to the host resolve to the host's — one copy of each.
|
|
23
|
+
*/
|
|
24
|
+
export declare const loadRuntime: (bytes: Uint8Array, dir: string) => Promise<SpaceRuntime>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import esbuild from "esbuild";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import { gunzipSync, gzipSync } from "node:zlib";
|
|
7
|
+
//#region src/modules/runtime/bundle.ts
|
|
8
|
+
/** What the runtime's host provides, and a bundle must not carry a copy of: the platform's packages and React. */
|
|
9
|
+
var PROVIDED = [
|
|
10
|
+
"@plitzi/*",
|
|
11
|
+
"react",
|
|
12
|
+
"react-dom",
|
|
13
|
+
"react/*",
|
|
14
|
+
"react-dom/*"
|
|
15
|
+
];
|
|
16
|
+
/** A packed runtime at most this big: its code and every dependency, not its data. */
|
|
17
|
+
var MAX_RUNTIME_BUNDLE_BYTES = 33554432;
|
|
18
|
+
var FORMAT = 1;
|
|
19
|
+
var ENTRY = "runtime.mjs";
|
|
20
|
+
/** A bundle's id: what its bytes are, so the same code packed twice is one bundle. */
|
|
21
|
+
var runtimeBundleId = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
22
|
+
/**
|
|
23
|
+
* A runtime's module packed to travel: bundled for Node with every dependency it imports — but the platform's packages
|
|
24
|
+
* and React, which the host provides, so there is one copy of each — and gzipped. `entry` is the module whose default
|
|
25
|
+
* export is `defineRuntime(…)`.
|
|
26
|
+
*/
|
|
27
|
+
var packRuntime = async (entry) => {
|
|
28
|
+
const result = await esbuild.build({
|
|
29
|
+
entryPoints: [entry],
|
|
30
|
+
bundle: true,
|
|
31
|
+
platform: "node",
|
|
32
|
+
format: "esm",
|
|
33
|
+
target: "node22",
|
|
34
|
+
external: PROVIDED,
|
|
35
|
+
outdir: "runtime",
|
|
36
|
+
entryNames: "runtime",
|
|
37
|
+
outExtension: { ".js": ".mjs" },
|
|
38
|
+
banner: { js: "import { createRequire as __plitziRequire } from \"node:module\"; const require = __plitziRequire(import.meta.url);" },
|
|
39
|
+
splitting: false,
|
|
40
|
+
logLevel: "warning",
|
|
41
|
+
write: false
|
|
42
|
+
});
|
|
43
|
+
const files = Object.fromEntries(result.outputFiles.map((output) => [path.basename(output.path), Buffer.from(output.contents).toString("base64")]));
|
|
44
|
+
if (!(ENTRY in files)) throw new Error(`Packing ${entry} produced no ${ENTRY}`);
|
|
45
|
+
const bytes = gzipSync(JSON.stringify({
|
|
46
|
+
format: FORMAT,
|
|
47
|
+
entry: ENTRY,
|
|
48
|
+
files
|
|
49
|
+
}));
|
|
50
|
+
if (bytes.byteLength > 33554432) throw new Error(`A runtime is at most ${String(MAX_RUNTIME_BUNDLE_BYTES / 1024 / 1024)} MB packed; this one is ${(bytes.byteLength / 1024 / 1024).toFixed(1)} MB`);
|
|
51
|
+
return new Uint8Array(bytes);
|
|
52
|
+
};
|
|
53
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
54
|
+
/** A packed runtime read back — refused whole when it is not one. */
|
|
55
|
+
var unpack = (bytes) => {
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = JSON.parse(gunzipSync(bytes).toString("utf8"));
|
|
59
|
+
} catch {
|
|
60
|
+
throw new Error("Not a packed runtime: it is not gzipped JSON");
|
|
61
|
+
}
|
|
62
|
+
if (!isRecord(parsed) || parsed.format !== FORMAT || typeof parsed.entry !== "string" || !isRecord(parsed.files)) throw new Error(`Not a packed runtime of format ${String(FORMAT)}`);
|
|
63
|
+
const files = {};
|
|
64
|
+
Object.entries(parsed.files).forEach(([name, content]) => {
|
|
65
|
+
if (typeof content !== "string" || name !== path.basename(name) || name.startsWith(".")) throw new Error(`"${name}" is not a file a packed runtime can hold`);
|
|
66
|
+
files[name] = content;
|
|
67
|
+
});
|
|
68
|
+
if (!(parsed.entry in files)) throw new Error(`A packed runtime's entry, ${parsed.entry}, is not among its files`);
|
|
69
|
+
return {
|
|
70
|
+
format: FORMAT,
|
|
71
|
+
entry: parsed.entry,
|
|
72
|
+
files
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* What a packed runtime holds, read without running any of it — what a platform checks a bundle with before keeping it.
|
|
77
|
+
* Throws, saying why, for bytes that are not one.
|
|
78
|
+
*/
|
|
79
|
+
var inspectRuntime = (bytes) => {
|
|
80
|
+
if (bytes.byteLength > 33554432) throw new Error(`A runtime is at most ${String(MAX_RUNTIME_BUNDLE_BYTES / 1024 / 1024)} MB packed`);
|
|
81
|
+
const { entry, files } = unpack(bytes);
|
|
82
|
+
return {
|
|
83
|
+
entry,
|
|
84
|
+
files: Object.keys(files)
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
var isRuntime = (value) => isRecord(value) && typeof value.start === "function";
|
|
88
|
+
/**
|
|
89
|
+
* A packed runtime written into `dir` and imported: its module's default export. `dir` must be inside the host's own
|
|
90
|
+
* project, so the packages the bundle leaves to the host resolve to the host's — one copy of each.
|
|
91
|
+
*/
|
|
92
|
+
var loadRuntime = async (bytes, dir) => {
|
|
93
|
+
const { entry, files } = unpack(bytes);
|
|
94
|
+
await fs.mkdir(dir, { recursive: true });
|
|
95
|
+
await Promise.all(Object.entries(files).map(([name, content]) => fs.writeFile(path.join(dir, name), Buffer.from(content, "base64"))));
|
|
96
|
+
const loaded = await import(pathToFileURL(path.join(dir, entry)).href);
|
|
97
|
+
const runtime = isRecord(loaded) ? loaded.default : void 0;
|
|
98
|
+
if (!isRuntime(runtime)) throw new Error("A runtime module exports its runtime by default: export default defineRuntime({ start: … })");
|
|
99
|
+
return runtime;
|
|
100
|
+
};
|
|
101
|
+
//#endregion
|
|
102
|
+
export { MAX_RUNTIME_BUNDLE_BYTES, inspectRuntime, loadRuntime, packRuntime, runtimeBundleId };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { FunctionsDefinition } from '../functions/contract';
|
|
2
|
+
/** What a runtime starts with. */
|
|
3
|
+
export type SpaceRuntimeContext = {
|
|
4
|
+
/** Its variables: those the platform keeps for it (`REDIS_URL`…), or a self-hosted server's own environment. */
|
|
5
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
6
|
+
/** Where people reach the space — what a link written for a person, or an agent, is handed. */
|
|
7
|
+
publicUrl: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* A path of the space's own answered by the runtime: a web handler, streaming if it likes — an agent's endpoint, a
|
|
11
|
+
* webhook that speaks a protocol of its own. The visitor's cookies and `Authorization` never reach it: the host's are
|
|
12
|
+
* the platform's.
|
|
13
|
+
*/
|
|
14
|
+
export type SpaceRuntimeEndpoint = (request: Request) => Response | Promise<Response>;
|
|
15
|
+
/** What a started runtime is made of. */
|
|
16
|
+
export type SpaceRuntimeParts = {
|
|
17
|
+
/** Its tasks and `/api/` routes, run with a `ctx` that is the platform's — the space's `kv`, its channels, its key. */
|
|
18
|
+
functions?: FunctionsDefinition;
|
|
19
|
+
/** Paths it answers itself, each with everything beneath it (`/mcp` answers `/mcp/…`) — see {@link endpointProblem}. */
|
|
20
|
+
endpoints?: Record<string, SpaceRuntimeEndpoint>;
|
|
21
|
+
/** Called when it is stopped: close what `start` opened. */
|
|
22
|
+
close?: () => Promise<void>;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* A space's own server code, run as code of its own — Node, its dependencies, its connections — beside the platform
|
|
26
|
+
* that serves the space: a runtime of the space, where the sandbox's functions are not enough. `start` is handed its
|
|
27
|
+
* variables and answers what it serves.
|
|
28
|
+
*/
|
|
29
|
+
export type SpaceRuntime = {
|
|
30
|
+
start: (context: SpaceRuntimeContext) => SpaceRuntimeParts | Promise<SpaceRuntimeParts>;
|
|
31
|
+
};
|
|
32
|
+
/** Declares a space runtime — an identity, for the types: `export default defineRuntime({ start: … })`. */
|
|
33
|
+
export declare const defineRuntime: (runtime: SpaceRuntime) => SpaceRuntime;
|
|
34
|
+
/** Why a path cannot be an endpoint — or nothing, for one that can. */
|
|
35
|
+
export declare const endpointProblem: (path: string) => string | undefined;
|
|
36
|
+
/** The endpoint a request's path belongs to: exactly it, or beneath it — the longest that does. */
|
|
37
|
+
export declare const endpointFor: (paths: readonly string[], path: string) => string | undefined;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { FUNCTION_ROUTES_PREFIX } from "@plitzi/sdk-shared/actions";
|
|
2
|
+
//#region src/modules/runtime/contract.ts
|
|
3
|
+
/** Declares a space runtime — an identity, for the types: `export default defineRuntime({ start: … })`. */
|
|
4
|
+
var defineRuntime = (runtime) => runtime;
|
|
5
|
+
/** The paths the server answers itself, which no endpoint may take: its routes, its own endpoints and sign-in. */
|
|
6
|
+
var RESERVED = [
|
|
7
|
+
FUNCTION_ROUTES_PREFIX,
|
|
8
|
+
"/_",
|
|
9
|
+
"/auth",
|
|
10
|
+
"/.well-known"
|
|
11
|
+
];
|
|
12
|
+
/** Why a path cannot be an endpoint — or nothing, for one that can. */
|
|
13
|
+
var endpointProblem = (path) => {
|
|
14
|
+
if (!/^\/[A-Za-z0-9._~-]+(\/[A-Za-z0-9._~-]+)*$/.test(path)) return `"${path}" is not an endpoint path: it starts with "/" and is made of plain segments`;
|
|
15
|
+
const taken = RESERVED.find((prefix) => path === prefix || path.startsWith(prefix.endsWith("_") ? prefix : `${prefix}/`));
|
|
16
|
+
if (taken) return `"${path}" is the server's own (${taken}): an endpoint takes a path of the space's`;
|
|
17
|
+
};
|
|
18
|
+
/** The endpoint a request's path belongs to: exactly it, or beneath it — the longest that does. */
|
|
19
|
+
var endpointFor = (paths, path) => paths.filter((candidate) => path === candidate || path.startsWith(`${candidate}/`)).sort((a, b) => b.length - a.length)[0];
|
|
20
|
+
//#endregion
|
|
21
|
+
export { defineRuntime, endpointFor, endpointProblem };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { SpaceRuntime } from './contract';
|
|
2
|
+
import { AddressInfo } from 'node:net';
|
|
3
|
+
/** What the platform reads of a started runtime: its functions' manifest, and the paths it answers itself. */
|
|
4
|
+
export type SpaceRuntimeDescription = {
|
|
5
|
+
functions: Record<string, unknown>;
|
|
6
|
+
endpoints: string[];
|
|
7
|
+
};
|
|
8
|
+
/** Where a started runtime says what it serves — under `/_`, which no endpoint may take. */
|
|
9
|
+
export declare const RUNTIME_DESCRIBE_PATH = "/_describe";
|
|
10
|
+
/** The headers of the platform's, which a runtime's endpoint never sees. */
|
|
11
|
+
export declare const PLATFORM_HEADERS: string[];
|
|
12
|
+
export type SpaceRuntimeHostOptions = {
|
|
13
|
+
runtime: SpaceRuntime;
|
|
14
|
+
/** What the platform presents — `Authorization: Bearer <secret>` — on its calls and on the requests it forwards. */
|
|
15
|
+
secret: string;
|
|
16
|
+
port?: number;
|
|
17
|
+
host?: string;
|
|
18
|
+
/** The runtime's variables. */
|
|
19
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
20
|
+
/** Where people reach the space. */
|
|
21
|
+
publicUrl: string;
|
|
22
|
+
/**
|
|
23
|
+
* Where the space is reached from inside the network this runtime runs in — the platform's ingress, over plain HTTP.
|
|
24
|
+
* Given, the runtime's requests to its own space go there rather than out to the edge and back (`reachSpaceInside`).
|
|
25
|
+
*/
|
|
26
|
+
insideUrl?: string | undefined;
|
|
27
|
+
};
|
|
28
|
+
export type SpaceRuntimeHost = {
|
|
29
|
+
address: () => AddressInfo;
|
|
30
|
+
close: () => Promise<void>;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* A space runtime, started and served: its tasks and routes over the functions runners' protocol (the platform invokes
|
|
34
|
+
* them and answers every `ctx` call), and its endpoints as plain HTTP the platform forwards — both behind one secret,
|
|
35
|
+
* since the only caller is the platform. The process is the space's alone: what its code can reach is this process
|
|
36
|
+
* and the network it is given, never the platform's database or keys.
|
|
37
|
+
*/
|
|
38
|
+
export declare const startSpaceRuntime: ({ runtime, secret, port, host, env, publicUrl, insideUrl }: SpaceRuntimeHostOptions) => Promise<SpaceRuntimeHost>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describeFunctions } from "../functions/driver.js";
|
|
2
|
+
import { startFunctionsRunnerService } from "../functions/runner/service.js";
|
|
3
|
+
import { endpointFor, endpointProblem } from "./contract.js";
|
|
4
|
+
import { reachSpaceInside } from "./inside.js";
|
|
5
|
+
import { createRuntimeRunner } from "./runner.js";
|
|
6
|
+
import { webRequestOf, writeWebResponse } from "../../core/http/webExchange.js";
|
|
7
|
+
//#region src/modules/runtime/host.ts
|
|
8
|
+
/** Where a started runtime says what it serves — under `/_`, which no endpoint may take. */
|
|
9
|
+
var RUNTIME_DESCRIBE_PATH = "/_describe";
|
|
10
|
+
/** The headers of the platform's, which a runtime's endpoint never sees. */
|
|
11
|
+
var PLATFORM_HEADERS = ["authorization", "cookie"];
|
|
12
|
+
/**
|
|
13
|
+
* A space runtime, started and served: its tasks and routes over the functions runners' protocol (the platform invokes
|
|
14
|
+
* them and answers every `ctx` call), and its endpoints as plain HTTP the platform forwards — both behind one secret,
|
|
15
|
+
* since the only caller is the platform. The process is the space's alone: what its code can reach is this process
|
|
16
|
+
* and the network it is given, never the platform's database or keys.
|
|
17
|
+
*/
|
|
18
|
+
var startSpaceRuntime = async ({ runtime, secret, port = 8791, host = "0.0.0.0", env, publicUrl, insideUrl }) => {
|
|
19
|
+
if (insideUrl) await reachSpaceInside({
|
|
20
|
+
publicUrl,
|
|
21
|
+
insideUrl
|
|
22
|
+
});
|
|
23
|
+
const parts = await runtime.start({
|
|
24
|
+
env,
|
|
25
|
+
publicUrl
|
|
26
|
+
});
|
|
27
|
+
const endpoints = parts.endpoints ?? {};
|
|
28
|
+
const paths = Object.keys(endpoints);
|
|
29
|
+
const problems = paths.flatMap((path) => endpointProblem(path) ?? []);
|
|
30
|
+
if (problems.length > 0) {
|
|
31
|
+
await parts.close?.();
|
|
32
|
+
throw new Error(problems.join("; "));
|
|
33
|
+
}
|
|
34
|
+
const description = {
|
|
35
|
+
functions: describeFunctions(parts.functions ?? {}),
|
|
36
|
+
endpoints: paths
|
|
37
|
+
};
|
|
38
|
+
const service = await startFunctionsRunnerService({
|
|
39
|
+
secret,
|
|
40
|
+
port,
|
|
41
|
+
host,
|
|
42
|
+
runner: createRuntimeRunner(parts.functions),
|
|
43
|
+
http: async (req, res) => {
|
|
44
|
+
const url = new URL(req.url ?? "/", publicUrl);
|
|
45
|
+
if (req.method === "GET" && url.pathname === "/_describe") {
|
|
46
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
47
|
+
res.end(JSON.stringify(description));
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
const endpoint = endpointFor(paths, url.pathname);
|
|
51
|
+
const answer = endpoint ? endpoints[endpoint] : void 0;
|
|
52
|
+
if (!answer) return false;
|
|
53
|
+
const left = new AbortController();
|
|
54
|
+
res.once("close", () => left.abort());
|
|
55
|
+
await writeWebResponse(res, await answer(await webRequestOf(req, url.href, PLATFORM_HEADERS)), left.signal);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
address: service.address,
|
|
61
|
+
close: async () => {
|
|
62
|
+
await service.close();
|
|
63
|
+
await parts.close?.();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
//#endregion
|
|
68
|
+
export { PLATFORM_HEADERS, RUNTIME_DESCRIBE_PATH, startSpaceRuntime };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The space's own public address, reached from inside the network this runtime runs in: every request this process
|
|
3
|
+
* makes to the space's host — `fetch` and `WebSocket` alike, which share Node's dispatcher — connects to `insideUrl`
|
|
4
|
+
* instead, over plain HTTP, the space's host and the public protocol kept (`X-Forwarded-Proto`). It is the path a
|
|
5
|
+
* request takes once past the edge, without leaving to come back through it; the space's code keeps writing its public
|
|
6
|
+
* address, and nothing else it reaches changes.
|
|
7
|
+
*
|
|
8
|
+
* `undici` is imported here, when a runtime asks, and never with the module: loading the package installs a global
|
|
9
|
+
* dispatcher of its own, and this entry is loaded by the platform's page server too (`createRuntimeProxyStage`), whose
|
|
10
|
+
* every `fetch` must stay Node's.
|
|
11
|
+
*/
|
|
12
|
+
export declare const reachSpaceInside: ({ publicUrl, insideUrl }: {
|
|
13
|
+
publicUrl: string;
|
|
14
|
+
insideUrl: string;
|
|
15
|
+
}) => Promise<void>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
//#region src/modules/runtime/inside.ts
|
|
3
|
+
/** Headers as a dispatch carries them — a record, name/value pairs flattened into one list, or pairs — plus one more. */
|
|
4
|
+
var withHeader = (headers, name, value) => {
|
|
5
|
+
const entries = [];
|
|
6
|
+
if (Array.isArray(headers)) for (let index = 0; index < headers.length; index += 2) entries.push([headers[index], headers[index + 1]]);
|
|
7
|
+
else if (headers && Symbol.iterator in headers) entries.push(...headers);
|
|
8
|
+
else if (headers) entries.push(...Object.entries(headers));
|
|
9
|
+
return {
|
|
10
|
+
...Object.fromEntries(entries),
|
|
11
|
+
[name]: value
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The space's own public address, reached from inside the network this runtime runs in: every request this process
|
|
16
|
+
* makes to the space's host — `fetch` and `WebSocket` alike, which share Node's dispatcher — connects to `insideUrl`
|
|
17
|
+
* instead, over plain HTTP, the space's host and the public protocol kept (`X-Forwarded-Proto`). It is the path a
|
|
18
|
+
* request takes once past the edge, without leaving to come back through it; the space's code keeps writing its public
|
|
19
|
+
* address, and nothing else it reaches changes.
|
|
20
|
+
*
|
|
21
|
+
* `undici` is imported here, when a runtime asks, and never with the module: loading the package installs a global
|
|
22
|
+
* dispatcher of its own, and this entry is loaded by the platform's page server too (`createRuntimeProxyStage`), whose
|
|
23
|
+
* every `fetch` must stay Node's.
|
|
24
|
+
*/
|
|
25
|
+
var reachSpaceInside = async ({ publicUrl, insideUrl }) => {
|
|
26
|
+
const outside = new URL(publicUrl);
|
|
27
|
+
const inside = new URL(insideUrl);
|
|
28
|
+
if (inside.protocol !== "http:") throw new Error(`A space is reached from inside over plain HTTP, not ${inside.protocol} (${insideUrl})`);
|
|
29
|
+
const { Agent, buildConnector, setGlobalDispatcher } = await import("undici");
|
|
30
|
+
const port = Number(inside.port || 80);
|
|
31
|
+
const protocol = outside.protocol.replace(":", "");
|
|
32
|
+
const connect = buildConnector({});
|
|
33
|
+
setGlobalDispatcher(new Agent({ connect: (options, callback) => {
|
|
34
|
+
if (options.hostname !== outside.hostname) {
|
|
35
|
+
connect(options, callback);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const socket = net.connect(port, inside.hostname);
|
|
39
|
+
socket.once("connect", () => callback(null, socket));
|
|
40
|
+
socket.once("error", (error) => callback(error, null));
|
|
41
|
+
} }).compose((dispatch) => (options, handler) => dispatch(new URL(String(options.origin)).hostname === outside.hostname ? {
|
|
42
|
+
...options,
|
|
43
|
+
headers: withHeader(options.headers, "x-forwarded-proto", protocol)
|
|
44
|
+
} : options, handler)));
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { reachSpaceInside };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FunctionsDefinition } from '../functions/contract';
|
|
2
|
+
import { FunctionRunner } from '../functions/protocol';
|
|
3
|
+
/**
|
|
4
|
+
* A runtime's functions as a runner: the same protocol as the sandbox's, answered by code loaded in this process. The
|
|
5
|
+
* driver is the sandbox's own (`createFunctionsDriver`), so a task behaves the same in both — its `ctx` is calls to the
|
|
6
|
+
* platform, `ctx.kv.change` the same loop, a refusal the same answer. What it does not share is the isolate: there are
|
|
7
|
+
* no limits here but the platform's clock, since this process is the space's alone.
|
|
8
|
+
*
|
|
9
|
+
* The bundle a request names is not asked for: the code is the one this runtime was started with.
|
|
10
|
+
*/
|
|
11
|
+
export declare const createRuntimeRunner: (definition?: FunctionsDefinition) => FunctionRunner;
|