@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
package/dist/modules/mongo/kv.js
CHANGED
|
@@ -91,6 +91,37 @@ var createMongoKv = ({ db, collection = "action_kv" }) => {
|
|
|
91
91
|
_id: key,
|
|
92
92
|
$expr: LIVE
|
|
93
93
|
}, [{ $set: { expiresAt: expiryIn(ttlSeconds) } }]);
|
|
94
|
+
},
|
|
95
|
+
/**
|
|
96
|
+
* Each branch is one update Mongo applies to the document atomically. Expecting a value, the filter names it.
|
|
97
|
+
* Expecting nothing is two cases: a lapsed document, taken over by a filter that matches only a lapsed one; and no
|
|
98
|
+
* document, inserted by an upsert whose filter no stored document satisfies — so a live one makes it collide on
|
|
99
|
+
* `_id`, which is the refusal, not an error. (An upsert may not filter with `$expr`, or this would be one step.)
|
|
100
|
+
*/
|
|
101
|
+
swap: async (key, expected, next, ttlSeconds) => {
|
|
102
|
+
const keysNow = await store();
|
|
103
|
+
const write = [{ $set: {
|
|
104
|
+
value: next,
|
|
105
|
+
expiresAt: ttlSeconds === void 0 ? null : expiryIn(ttlSeconds)
|
|
106
|
+
} }];
|
|
107
|
+
if (expected !== void 0) return (await keysNow.updateOne({
|
|
108
|
+
_id: key,
|
|
109
|
+
value: expected,
|
|
110
|
+
$expr: LIVE
|
|
111
|
+
}, write)).matchedCount === 1;
|
|
112
|
+
if ((await keysNow.updateOne({
|
|
113
|
+
_id: key,
|
|
114
|
+
$expr: { $not: [LIVE] }
|
|
115
|
+
}, write)).matchedCount === 1) return true;
|
|
116
|
+
try {
|
|
117
|
+
return (await keysNow.updateOne({
|
|
118
|
+
_id: key,
|
|
119
|
+
value: { $exists: false }
|
|
120
|
+
}, write, { upsert: true })).upsertedCount === 1;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error.code === DUPLICATE_KEY) return false;
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
94
125
|
}
|
|
95
126
|
};
|
|
96
127
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { activityDue } from "../../core/auth/sessionActivity.js";
|
|
1
2
|
import { execute, selectOne, selectRows } from "./query.js";
|
|
2
3
|
import { createSessionStore } from "./sessions.js";
|
|
3
4
|
//#region src/modules/mysql/accounts.ts
|
|
@@ -50,12 +51,15 @@ var createAccountStore = (db, t) => {
|
|
|
50
51
|
* check — deactivating somebody has to end the session they are already holding, not just the next login.
|
|
51
52
|
*/
|
|
52
53
|
findAccountByToken: async (token) => {
|
|
53
|
-
const row = await selectOne(db, `SELECT ${ACCOUNT_COLUMNS}, s.expires_at AS session_expires_at
|
|
54
|
+
const row = await selectOne(db, `SELECT ${ACCOUNT_COLUMNS}, s.id AS session_id, s.expires_at AS session_expires_at,
|
|
55
|
+
s.last_active_at AS session_last_active_at
|
|
54
56
|
FROM ${t.session} s
|
|
55
57
|
JOIN ${t.account} a ON a.id = s.account_id
|
|
56
58
|
WHERE s.token = ? AND a.status = 'active'
|
|
57
59
|
LIMIT 1`, [token]);
|
|
58
60
|
if (!row) return;
|
|
61
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
62
|
+
if (row.session_id !== void 0 && activityDue(row.session_last_active_at, now)) execute(db, `UPDATE ${t.session} SET last_active_at = ? WHERE id = ?`, [now, row.session_id]).catch(() => void 0);
|
|
59
63
|
const { roles, permissions } = await loadAccess(row.id);
|
|
60
64
|
return {
|
|
61
65
|
id: row.id,
|
|
@@ -120,15 +124,18 @@ var createAccountStore = (db, t) => {
|
|
|
120
124
|
]);
|
|
121
125
|
return;
|
|
122
126
|
}
|
|
123
|
-
await execute(db, `INSERT INTO ${t.session}
|
|
124
|
-
|
|
127
|
+
await execute(db, `INSERT INTO ${t.session}
|
|
128
|
+
(account_id, token, expires_at, refresh_token, refresh_expires_at, user_agent, ip, app_name, app_id)
|
|
129
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
125
130
|
userId,
|
|
126
131
|
session.token,
|
|
127
132
|
session.expiresAt,
|
|
128
133
|
session.refreshToken ?? null,
|
|
129
134
|
session.refreshExpiresAt ?? null,
|
|
130
135
|
context?.client?.userAgent?.slice(0, 255) ?? null,
|
|
131
|
-
context?.client?.ip ?? null
|
|
136
|
+
context?.client?.ip ?? null,
|
|
137
|
+
context?.client?.app?.name.slice(0, 120) ?? null,
|
|
138
|
+
context?.client?.app?.softwareId?.slice(0, 64) ?? null
|
|
132
139
|
]);
|
|
133
140
|
},
|
|
134
141
|
/**
|
|
@@ -335,22 +342,25 @@ var createAccountStore = (db, t) => {
|
|
|
335
342
|
* read whole and written whole, and a text column that a human can read in a crisis beats one they cannot.
|
|
336
343
|
*/
|
|
337
344
|
loadMfa: async (userId) => {
|
|
338
|
-
const row = await selectOne(db, `SELECT secret, confirmed_at, recovery_codes FROM ${t.mfa} WHERE account_id = ? LIMIT 1`, [userId]);
|
|
345
|
+
const row = await selectOne(db, `SELECT secret, confirmed_at, recovery_codes, last_used_step FROM ${t.mfa} WHERE account_id = ? LIMIT 1`, [userId]);
|
|
339
346
|
if (!row) return;
|
|
340
347
|
return {
|
|
341
348
|
secret: row.secret,
|
|
342
349
|
...row.confirmed_at !== null ? { confirmedAt: row.confirmed_at } : {},
|
|
343
|
-
recoveryCodes: (row.recovery_codes ?? "").split("\n").filter(Boolean)
|
|
350
|
+
recoveryCodes: (row.recovery_codes ?? "").split("\n").filter(Boolean),
|
|
351
|
+
...row.last_used_step !== null ? { lastUsedStep: row.last_used_step } : {}
|
|
344
352
|
};
|
|
345
353
|
},
|
|
346
354
|
saveMfa: async (userId, mfaRecord) => {
|
|
347
|
-
await execute(db, `INSERT INTO ${t.mfa} (account_id, secret, confirmed_at, recovery_codes) VALUES (?, ?, ?, ?)
|
|
355
|
+
await execute(db, `INSERT INTO ${t.mfa} (account_id, secret, confirmed_at, recovery_codes, last_used_step) VALUES (?, ?, ?, ?, ?)
|
|
348
356
|
ON DUPLICATE KEY UPDATE
|
|
349
|
-
secret = VALUES(secret), confirmed_at = VALUES(confirmed_at), recovery_codes = VALUES(recovery_codes)
|
|
357
|
+
secret = VALUES(secret), confirmed_at = VALUES(confirmed_at), recovery_codes = VALUES(recovery_codes),
|
|
358
|
+
last_used_step = VALUES(last_used_step)`, [
|
|
350
359
|
userId,
|
|
351
360
|
mfaRecord.secret,
|
|
352
361
|
mfaRecord.confirmedAt ?? null,
|
|
353
|
-
(mfaRecord.recoveryCodes ?? []).join("\n")
|
|
362
|
+
(mfaRecord.recoveryCodes ?? []).join("\n"),
|
|
363
|
+
mfaRecord.lastUsedStep ?? null
|
|
354
364
|
]);
|
|
355
365
|
},
|
|
356
366
|
deleteMfa: async (userId) => {
|
|
@@ -63,5 +63,6 @@ export { TABLE_NAMES, tableNames } from './config';
|
|
|
63
63
|
export { SCHEMA_VERSION } from './schema';
|
|
64
64
|
export type { MysqlConfig, TableKey, Tables } from './config';
|
|
65
65
|
export type { AccountSeed, MysqlAdmin, SpaceTokenSeed } from './admin';
|
|
66
|
-
export type { SessionStore
|
|
66
|
+
export type { SessionStore } from './sessions';
|
|
67
|
+
export type { SessionSummary } from '../../core/auth/api';
|
|
67
68
|
export type { IdentityStore, LinkedIdentity } from './identities';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createMysqlJobQueue } from './jobQueue';
|
|
2
2
|
export { createMysqlKv } from './kv';
|
|
3
|
-
export { mysqlJobSchemaStatements } from './schema';
|
|
3
|
+
export { mysqlJobSchemaStatements, mysqlJobSchemaUpgrades } from './schema';
|
|
4
4
|
export type { MysqlJobQueue, MysqlJobQueueOptions } from './jobQueue';
|
|
5
5
|
export type { MysqlKvOptions } from './kv';
|
|
@@ -71,6 +71,36 @@ var createMysqlKv = ({ pool, tablePrefix = "", createTables = true }) => {
|
|
|
71
71
|
},
|
|
72
72
|
expire: async (key, ttlSeconds) => {
|
|
73
73
|
await execute(await db(), `UPDATE ${kv} SET expires_at = ${NOW_MS} + ? WHERE k = ? AND ${LIVE}`, [ttlSeconds * 1e3, key]);
|
|
74
|
+
},
|
|
75
|
+
/**
|
|
76
|
+
* Each branch is ONE statement MySQL applies under the row's lock, so two writers racing for the same key cannot
|
|
77
|
+
* both win. `affectedRows` counts the rows MATCHED because mysql2 connects with `FOUND_ROWS`: without it, writing
|
|
78
|
+
* back the value a key already holds would read as a refusal. The comparison is on bytes — the table's collation
|
|
79
|
+
* ignores case and trailing spaces, and "unchanged" has to mean the same bytes.
|
|
80
|
+
*/
|
|
81
|
+
swap: async (key, expected, next, ttlSeconds) => {
|
|
82
|
+
const expiry = ttlSeconds === void 0 ? "NULL" : `${NOW_MS} + ?`;
|
|
83
|
+
const lifetime = ttlSeconds === void 0 ? [] : [ttlSeconds * 1e3];
|
|
84
|
+
const pool = await db();
|
|
85
|
+
if (expected !== void 0) return (await execute(pool, `UPDATE ${kv} SET v = ?, expires_at = ${expiry}
|
|
86
|
+
WHERE k = ? AND ${LIVE} AND CAST(v AS BINARY) = CAST(? AS BINARY)`, [
|
|
87
|
+
next,
|
|
88
|
+
...lifetime,
|
|
89
|
+
key,
|
|
90
|
+
expected
|
|
91
|
+
])).affectedRows === 1;
|
|
92
|
+
if ((await execute(pool, `UPDATE ${kv} SET v = ?, expires_at = ${expiry} WHERE k = ? AND NOT ${LIVE}`, [
|
|
93
|
+
next,
|
|
94
|
+
...lifetime,
|
|
95
|
+
key
|
|
96
|
+
])).affectedRows === 1) return true;
|
|
97
|
+
const inserted = await execute(pool, `INSERT IGNORE INTO ${kv} (k, v, expires_at) VALUES (?, ?, ${expiry})`, [
|
|
98
|
+
key,
|
|
99
|
+
next,
|
|
100
|
+
...lifetime
|
|
101
|
+
]);
|
|
102
|
+
await sweep();
|
|
103
|
+
return inserted.affectedRows === 1;
|
|
74
104
|
}
|
|
75
105
|
};
|
|
76
106
|
};
|
|
@@ -16,8 +16,17 @@ export declare const jobTables: (prefix?: string) => JobTables;
|
|
|
16
16
|
* Instants are epoch milliseconds in `BIGINT`, read from the SERVER's clock; `history` and `input` are JSON kept as
|
|
17
17
|
* text, because they are only ever read whole. Separate from the account store's schema and its version: a deployment
|
|
18
18
|
* can use either without the other.
|
|
19
|
+
*
|
|
20
|
+
* A `kv` key is BYTES, as it is in Redis and in memory: under a collation `Board` and `board` — and `a` and `a ` — were
|
|
21
|
+
* one key here and two everywhere else. A value is a `MEDIUMTEXT`: a `TEXT` stops at 64 KB, and past it MySQL
|
|
22
|
+
* refuses the write, or cuts it short where it is not strict.
|
|
19
23
|
*/
|
|
20
24
|
export declare const mysqlJobSchemaStatements: (prefix?: string) => string[];
|
|
25
|
+
/**
|
|
26
|
+
* What brings a `kv` table made before the columns above up to them — for a deployment that runs its own migrations,
|
|
27
|
+
* once. `ensureJobTables` does it by itself, and only when the table needs it: the `ALTER` copies the table.
|
|
28
|
+
*/
|
|
29
|
+
export declare const mysqlJobSchemaUpgrades: (prefix?: string) => string[];
|
|
21
30
|
/** The server's now, in epoch ms, as an SQL expression — what every comparison reads instead of a caller's clock. */
|
|
22
31
|
export declare const NOW_MS = "CAST(ROUND(UNIX_TIMESTAMP(NOW(3)) * 1000) AS SIGNED)";
|
|
23
32
|
/** Creates the tables once per process, on first use; a failure is retried by the next call rather than remembered. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execute } from "../query.js";
|
|
1
|
+
import { execute, selectRows } from "../query.js";
|
|
2
2
|
//#region src/modules/mysql/jobs/schema.ts
|
|
3
3
|
var PREFIX = /^[A-Za-z0-9_]*$/;
|
|
4
4
|
/**
|
|
@@ -20,6 +20,10 @@ var CHARSET = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
|
|
20
20
|
* Instants are epoch milliseconds in `BIGINT`, read from the SERVER's clock; `history` and `input` are JSON kept as
|
|
21
21
|
* text, because they are only ever read whole. Separate from the account store's schema and its version: a deployment
|
|
22
22
|
* can use either without the other.
|
|
23
|
+
*
|
|
24
|
+
* A `kv` key is BYTES, as it is in Redis and in memory: under a collation `Board` and `board` — and `a` and `a ` — were
|
|
25
|
+
* one key here and two everywhere else. A value is a `MEDIUMTEXT`: a `TEXT` stops at 64 KB, and past it MySQL
|
|
26
|
+
* refuses the write, or cuts it short where it is not strict.
|
|
23
27
|
*/
|
|
24
28
|
var mysqlJobSchemaStatements = (prefix = "") => {
|
|
25
29
|
const t = jobTables(prefix);
|
|
@@ -68,14 +72,26 @@ var mysqlJobSchemaStatements = (prefix = "") => {
|
|
|
68
72
|
KEY action_schedules_due (enabled, next_run_at)
|
|
69
73
|
) ${CHARSET}`,
|
|
70
74
|
`CREATE TABLE IF NOT EXISTS ${t.kv} (
|
|
71
|
-
k
|
|
72
|
-
v
|
|
75
|
+
k VARBINARY(764) NOT NULL,
|
|
76
|
+
v MEDIUMTEXT NOT NULL,
|
|
73
77
|
expires_at BIGINT NULL,
|
|
74
78
|
PRIMARY KEY (k),
|
|
75
79
|
KEY action_kv_expires (expires_at)
|
|
76
80
|
) ${CHARSET}`
|
|
77
81
|
];
|
|
78
82
|
};
|
|
83
|
+
/**
|
|
84
|
+
* What brings a `kv` table made before the columns above up to them — for a deployment that runs its own migrations,
|
|
85
|
+
* once. `ensureJobTables` does it by itself, and only when the table needs it: the `ALTER` copies the table.
|
|
86
|
+
*/
|
|
87
|
+
var mysqlJobSchemaUpgrades = (prefix = "") => [`ALTER TABLE ${jobTables(prefix).kv} MODIFY k VARBINARY(764) NOT NULL, MODIFY v MEDIUMTEXT NOT NULL`];
|
|
88
|
+
/** Whether the `kv` table still has the columns it was first made with. */
|
|
89
|
+
var kvNeedsUpgrade = async (db, prefix) => {
|
|
90
|
+
const columns = await selectRows(db, `SELECT COLUMN_NAME AS name, DATA_TYPE AS type FROM INFORMATION_SCHEMA.COLUMNS
|
|
91
|
+
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME IN ('k', 'v')`, [`${prefix}action_kv`]);
|
|
92
|
+
const typeOf = (name) => columns.find((column) => column.name === name)?.type.toLowerCase();
|
|
93
|
+
return typeOf("k") !== "varbinary" || typeOf("v") !== "mediumtext";
|
|
94
|
+
};
|
|
79
95
|
/** The server's now, in epoch ms, as an SQL expression — what every comparison reads instead of a caller's clock. */
|
|
80
96
|
var NOW_MS = "CAST(ROUND(UNIX_TIMESTAMP(NOW(3)) * 1000) AS SIGNED)";
|
|
81
97
|
/** Creates the tables once per process, on first use; a failure is retried by the next call rather than remembered. */
|
|
@@ -85,6 +101,7 @@ var ensureJobTables = (db, prefix, create) => {
|
|
|
85
101
|
if (!create) return;
|
|
86
102
|
ready ??= (async () => {
|
|
87
103
|
for (const statement of mysqlJobSchemaStatements(prefix)) await execute(db, statement);
|
|
104
|
+
if (await kvNeedsUpgrade(db, prefix)) for (const statement of mysqlJobSchemaUpgrades(prefix)) await execute(db, statement);
|
|
88
105
|
})().catch((error) => {
|
|
89
106
|
ready = void 0;
|
|
90
107
|
throw error;
|
|
@@ -93,4 +110,4 @@ var ensureJobTables = (db, prefix, create) => {
|
|
|
93
110
|
};
|
|
94
111
|
};
|
|
95
112
|
//#endregion
|
|
96
|
-
export { NOW_MS, ensureJobTables, jobTables, mysqlJobSchemaStatements };
|
|
113
|
+
export { NOW_MS, ensureJobTables, jobTables, mysqlJobSchemaStatements, mysqlJobSchemaUpgrades };
|
|
@@ -196,6 +196,23 @@ var step3 = (t) => {
|
|
|
196
196
|
...addIndex(account, "account_pending_email_token", "(pending_email_token)")
|
|
197
197
|
];
|
|
198
198
|
};
|
|
199
|
+
/**
|
|
200
|
+
* What tells one device from another in the account's session list: the application holding it, when it is not a
|
|
201
|
+
* browser (`Plitzi CLI on carlos-mbp`, software `plitzi-cli`), and when it was last used — see `sessionActivity`.
|
|
202
|
+
*/
|
|
203
|
+
var step4 = (t) => {
|
|
204
|
+
const session = unquote(t.session);
|
|
205
|
+
return [
|
|
206
|
+
...addColumn(session, "app_name", "VARCHAR(120) NULL AFTER ip"),
|
|
207
|
+
...addColumn(session, "app_id", "VARCHAR(64) NULL AFTER app_name"),
|
|
208
|
+
...addColumn(session, "last_active_at", "BIGINT NULL AFTER created_at")
|
|
209
|
+
];
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* The time step of the last second-factor code accepted, so the same code cannot sign in twice within the window it
|
|
213
|
+
* stays valid for (RFC 6238 §5.2) — see `MfaRecord.lastUsedStep`.
|
|
214
|
+
*/
|
|
215
|
+
var step5 = (t) => [...addColumn(unquote(t.mfa), "last_used_step", "BIGINT NULL AFTER recovery_codes")];
|
|
199
216
|
/** Append only. A released version is never edited — the deployments that already ran it would never see the change. */
|
|
200
217
|
var STEPS = [
|
|
201
218
|
{
|
|
@@ -209,6 +226,14 @@ var STEPS = [
|
|
|
209
226
|
{
|
|
210
227
|
version: 3,
|
|
211
228
|
statements: step3
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
version: 4,
|
|
232
|
+
statements: step4
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
version: 5,
|
|
236
|
+
statements: step5
|
|
212
237
|
}
|
|
213
238
|
];
|
|
214
239
|
var SCHEMA_VERSION = STEPS[STEPS.length - 1].version;
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
import { Tables } from './config';
|
|
2
2
|
import { Queryable } from './query';
|
|
3
|
-
|
|
4
|
-
export interface SessionSummary {
|
|
5
|
-
id: number;
|
|
6
|
-
userAgent?: string;
|
|
7
|
-
ip?: string;
|
|
8
|
-
/** Unix seconds. */
|
|
9
|
-
createdAt: number;
|
|
10
|
-
expiresAt: number;
|
|
11
|
-
/** This is the session asking. A device list without it invites someone to revoke the one they are using. */
|
|
12
|
-
current: boolean;
|
|
13
|
-
}
|
|
3
|
+
import { SessionSummary } from '../../core/auth/api';
|
|
14
4
|
/**
|
|
15
5
|
* The sessions of one account, as a thing that can be looked at and taken away one at a time.
|
|
16
6
|
*
|
|
@@ -9,14 +9,19 @@ import { execute, selectRows } from "./query.js";
|
|
|
9
9
|
*/
|
|
10
10
|
var createSessionStore = (db, t) => {
|
|
11
11
|
const list = async (accountId, currentToken) => {
|
|
12
|
-
return (await selectRows(db, `SELECT id, token, user_agent, ip, created_at, expires_at
|
|
12
|
+
return (await selectRows(db, `SELECT id, token, user_agent, ip, app_name, app_id, created_at, last_active_at, expires_at
|
|
13
13
|
FROM ${t.session}
|
|
14
14
|
WHERE account_id = ?
|
|
15
15
|
ORDER BY created_at DESC`, [accountId])).map((row) => ({
|
|
16
16
|
id: row.id,
|
|
17
17
|
...row.user_agent ? { userAgent: row.user_agent } : {},
|
|
18
18
|
...row.ip ? { ip: row.ip } : {},
|
|
19
|
+
...row.app_name ? { app: {
|
|
20
|
+
name: row.app_name,
|
|
21
|
+
...row.app_id ? { softwareId: row.app_id } : {}
|
|
22
|
+
} } : {},
|
|
19
23
|
createdAt: Math.floor(row.created_at.getTime() / 1e3),
|
|
24
|
+
...row.last_active_at ? { lastActiveAt: row.last_active_at } : {},
|
|
20
25
|
expiresAt: row.expires_at,
|
|
21
26
|
current: currentToken !== void 0 && row.token === currentToken
|
|
22
27
|
}));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ChannelDeclarations, OfflineDataRaw } from '@plitzi/sdk-shared';
|
|
2
|
+
type GetOfflineData = (spaceId: number, environment: string, revision?: number) => Promise<OfflineDataRaw | undefined>;
|
|
3
|
+
/**
|
|
4
|
+
* The channels a space declares, read from its schema and remembered for a few seconds.
|
|
5
|
+
*
|
|
6
|
+
* A publish is checked against them on every message; reading the space's documents for each one would cost more
|
|
7
|
+
* than the message. A space that changes its channels is heard within the TTL.
|
|
8
|
+
*/
|
|
9
|
+
export declare const createChannelResolver: (getOfflineData: GetOfflineData) => (spaceId: number, environment: string, revision?: number) => Promise<ChannelDeclarations | undefined>;
|
|
10
|
+
export type ChannelResolver = ReturnType<typeof createChannelResolver>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/modules/realtime/declarations.ts
|
|
2
|
+
/** How long a space's channels are remembered: a publish is frequent, and a space's settings change rarely. */
|
|
3
|
+
var TTL_MS = 5e3;
|
|
4
|
+
/**
|
|
5
|
+
* The channels a space declares, read from its schema and remembered for a few seconds.
|
|
6
|
+
*
|
|
7
|
+
* A publish is checked against them on every message; reading the space's documents for each one would cost more
|
|
8
|
+
* than the message. A space that changes its channels is heard within the TTL.
|
|
9
|
+
*/
|
|
10
|
+
var createChannelResolver = (getOfflineData) => {
|
|
11
|
+
const cache = /* @__PURE__ */ new Map();
|
|
12
|
+
return (spaceId, environment, revision) => {
|
|
13
|
+
const key = `${spaceId}:${environment}:${revision ?? ""}`;
|
|
14
|
+
const hit = cache.get(key);
|
|
15
|
+
if (hit && Date.now() - hit.at < TTL_MS) return hit.channels;
|
|
16
|
+
const channels = getOfflineData(spaceId, environment, revision).then((data) => data?.schema.settings.channels);
|
|
17
|
+
cache.set(key, {
|
|
18
|
+
at: Date.now(),
|
|
19
|
+
channels
|
|
20
|
+
});
|
|
21
|
+
channels.catch(() => cache.delete(key));
|
|
22
|
+
return channels;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { createChannelResolver };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { RealtimeSpace } from './hub';
|
|
2
|
+
import { ActionKvAdapter } from '../actions/types';
|
|
3
|
+
/** How long a grant opens its topic when the flow does not say: a day, which outlives any one visit. */
|
|
4
|
+
export declare const DEFAULT_GRANT_SECONDS = 86400;
|
|
5
|
+
/** The longest a grant may last: thirty days. A page that stays open longer asks its action for a new one. */
|
|
6
|
+
export declare const MAX_GRANT_SECONDS: number;
|
|
7
|
+
export type RealtimeGrants = {
|
|
8
|
+
/** A grant for `topic`, good for `ttlSeconds` — what `realtime.grant` hands the page. */
|
|
9
|
+
issue: (space: RealtimeSpace, topic: string, ttlSeconds?: number) => Promise<string>;
|
|
10
|
+
/** Whether `grant` was issued for exactly `topic` of this space, and has neither run out nor been revoked. */
|
|
11
|
+
opens: (space: RealtimeSpace, topic: string, grant: string) => Promise<boolean>;
|
|
12
|
+
/** `grant` no longer opens `topic` — or, with none named, no grant issued for it until now does. */
|
|
13
|
+
revoke: (space: RealtimeSpace, topic: string, grant?: string) => Promise<void>;
|
|
14
|
+
};
|
|
15
|
+
export declare const grantSeconds: (ttlSeconds: number | undefined) => number;
|
|
16
|
+
/**
|
|
17
|
+
* The grants a server has issued, kept in the store its actions' `kv` uses — so a grant one replica issued opens the
|
|
18
|
+
* topic on whichever replica the page connects to, and nothing new has to be configured for it. A random token rather
|
|
19
|
+
* than a signature: there is no secret to share between replicas, and a grant is only ever as old as its lifetime.
|
|
20
|
+
*
|
|
21
|
+
* Keyed under `realtime-grant:`, a prefix no key a flow writes can reach (theirs are `kv:action:<space>:…`).
|
|
22
|
+
*/
|
|
23
|
+
export declare const createRealtimeGrants: (store: ActionKvAdapter) => RealtimeGrants;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
//#region src/modules/realtime/grants.ts
|
|
3
|
+
/** How long a grant opens its topic when the flow does not say: a day, which outlives any one visit. */
|
|
4
|
+
var DEFAULT_GRANT_SECONDS = 86400;
|
|
5
|
+
/** The longest a grant may last: thirty days. A page that stays open longer asks its action for a new one. */
|
|
6
|
+
var MAX_GRANT_SECONDS = 2592e3;
|
|
7
|
+
/** What a grant looks like on the wire: long enough to be unguessable, and nothing a query string has to escape. */
|
|
8
|
+
var TOKEN = /^[A-Za-z0-9_-]{24,64}$/;
|
|
9
|
+
var parseGrant = (raw) => {
|
|
10
|
+
try {
|
|
11
|
+
const value = JSON.parse(raw ?? "");
|
|
12
|
+
return typeof value === "object" && value !== null && "topic" in value && typeof value.topic === "string" && "generation" in value && typeof value.generation === "number" ? {
|
|
13
|
+
topic: value.topic,
|
|
14
|
+
generation: value.generation
|
|
15
|
+
} : void 0;
|
|
16
|
+
} catch {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var grantSeconds = (ttlSeconds) => ttlSeconds === void 0 || !Number.isFinite(ttlSeconds) ? DEFAULT_GRANT_SECONDS : Math.min(MAX_GRANT_SECONDS, Math.max(60, Math.floor(ttlSeconds)));
|
|
21
|
+
/**
|
|
22
|
+
* The grants a server has issued, kept in the store its actions' `kv` uses — so a grant one replica issued opens the
|
|
23
|
+
* topic on whichever replica the page connects to, and nothing new has to be configured for it. A random token rather
|
|
24
|
+
* than a signature: there is no secret to share between replicas, and a grant is only ever as old as its lifetime.
|
|
25
|
+
*
|
|
26
|
+
* Keyed under `realtime-grant:`, a prefix no key a flow writes can reach (theirs are `kv:action:<space>:…`).
|
|
27
|
+
*/
|
|
28
|
+
var createRealtimeGrants = (store) => {
|
|
29
|
+
const key = ({ spaceId, environment }, grant) => `realtime-grant:${String(spaceId)}:${environment}:${grant}`;
|
|
30
|
+
/**
|
|
31
|
+
* How many times every grant of a topic was revoked at once. A grant carries the number it was issued under, so
|
|
32
|
+
* raising it voids them all — the ones in use and the ones not used yet — without a list of them to walk.
|
|
33
|
+
*/
|
|
34
|
+
const generationKey = ({ spaceId, environment }, topic) => `realtime-grant-generation:${String(spaceId)}:${environment}:${topic}`;
|
|
35
|
+
const generationOf = async (space, topic) => Number(await store.get(generationKey(space, topic))) || 0;
|
|
36
|
+
return {
|
|
37
|
+
issue: async (space, topic, ttlSeconds) => {
|
|
38
|
+
const grant = randomBytes(18).toString("base64url");
|
|
39
|
+
const stored = {
|
|
40
|
+
topic,
|
|
41
|
+
generation: await generationOf(space, topic)
|
|
42
|
+
};
|
|
43
|
+
await store.set(key(space, grant), JSON.stringify(stored), grantSeconds(ttlSeconds));
|
|
44
|
+
return grant;
|
|
45
|
+
},
|
|
46
|
+
opens: async (space, topic, grant) => {
|
|
47
|
+
if (!TOKEN.test(grant)) return false;
|
|
48
|
+
const stored = parseGrant(await store.get(key(space, grant)));
|
|
49
|
+
return stored?.topic === topic && stored.generation === await generationOf(space, topic);
|
|
50
|
+
},
|
|
51
|
+
revoke: async (space, topic, grant) => {
|
|
52
|
+
if (grant === void 0) {
|
|
53
|
+
await store.increment(generationKey(space, topic), 1);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (TOKEN.test(grant) && parseGrant(await store.get(key(space, grant)))?.topic === topic) await store.delete(key(space, grant));
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
//#endregion
|
|
61
|
+
export { DEFAULT_GRANT_SECONDS, MAX_GRANT_SECONDS, createRealtimeGrants, grantSeconds };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ChannelResolver } from './declarations';
|
|
2
|
+
import { RealtimeGrants } from './grants';
|
|
3
|
+
import { RealtimeConnection, RealtimeHub, RealtimeSpace } from './hub';
|
|
4
|
+
import { RawResponse } from '../../helpers/buildResponseHelpers';
|
|
5
|
+
import { ChannelDeclaration, RealtimeSender, SSRRequest, SSRResponseHelpers } from '@plitzi/sdk-shared';
|
|
6
|
+
export type RealtimeRefusal = {
|
|
7
|
+
topic: string;
|
|
8
|
+
/** `ungranted`: the channel is declared `grant: true`, and no grant the page sent opens this topic. */
|
|
9
|
+
reason: 'undeclared' | 'unauthenticated' | 'forbidden' | 'ungranted';
|
|
10
|
+
};
|
|
11
|
+
/** An answer in HTTP terms, whichever transport carries it: a status, and what went wrong when something did. */
|
|
12
|
+
export type RealtimeAnswer = {
|
|
13
|
+
status: number;
|
|
14
|
+
error?: string;
|
|
15
|
+
reason?: string;
|
|
16
|
+
refused?: RealtimeRefusal[];
|
|
17
|
+
};
|
|
18
|
+
export declare const answer: (res: SSRResponseHelpers, { status, ...payload }: RealtimeAnswer) => void;
|
|
19
|
+
/** What a page may open, decided once for either transport. */
|
|
20
|
+
export type Admission = {
|
|
21
|
+
ok: false;
|
|
22
|
+
answer: RealtimeAnswer;
|
|
23
|
+
} | {
|
|
24
|
+
ok: true;
|
|
25
|
+
space: RealtimeSpace;
|
|
26
|
+
user?: RealtimeSender['user'];
|
|
27
|
+
accepted: Map<string, ChannelDeclaration>;
|
|
28
|
+
/** The grant each accepted topic of a `grant: true` channel was opened with. */
|
|
29
|
+
granted: Map<string, string>;
|
|
30
|
+
refused: RealtimeRefusal[];
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Which of the topics a page asked for (`?topics=a,b`) it may open: each checked against the channels the space
|
|
34
|
+
* declares and against the visitor's access. One that fails is reported, not a reason to refuse the rest; all failing
|
|
35
|
+
* is a `403`.
|
|
36
|
+
*/
|
|
37
|
+
export declare const admit: (req: SSRRequest, resolveChannels: ChannelResolver, grants: RealtimeGrants) => Promise<Admission>;
|
|
38
|
+
/** A new connection for what was admitted, speaking through `send`. */
|
|
39
|
+
export declare const connectionFor: (admission: Extract<Admission, {
|
|
40
|
+
ok: true;
|
|
41
|
+
}>, send: RealtimeConnection["send"], end: RealtimeConnection["end"]) => RealtimeConnection;
|
|
42
|
+
/**
|
|
43
|
+
* A page publishing, whichever way it arrived: only on a topic its connection subscribed to, only on a channel pages
|
|
44
|
+
* may send on, within its size and its rate — and stamped by the server with who sent it and when, whatever the
|
|
45
|
+
* message said.
|
|
46
|
+
*/
|
|
47
|
+
export declare const publishFrom: (hub: RealtimeHub, connection: RealtimeConnection, { topic, type, data }: {
|
|
48
|
+
topic?: unknown;
|
|
49
|
+
type?: unknown;
|
|
50
|
+
data?: unknown;
|
|
51
|
+
}) => Promise<RealtimeAnswer>;
|
|
52
|
+
export type SubscribeDeps = {
|
|
53
|
+
req: SSRRequest;
|
|
54
|
+
res: SSRResponseHelpers;
|
|
55
|
+
raw: RawResponse;
|
|
56
|
+
signal: AbortSignal;
|
|
57
|
+
hub: RealtimeHub;
|
|
58
|
+
resolveChannels: ChannelResolver;
|
|
59
|
+
grants: RealtimeGrants;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* `GET /_realtime?topics=a,b` (and `&grants=g1,g2` for the topics of a `grant: true` channel) — one Server-Sent Events connection for every topic a page listens to.
|
|
63
|
+
*
|
|
64
|
+
* The first event, `ready`, tells the page its public name (`from`) and the secret its publishes carry. Resolves when
|
|
65
|
+
* the page goes.
|
|
66
|
+
*/
|
|
67
|
+
export declare const handleRealtimeSubscribe: ({ req, res, raw, signal, hub, resolveChannels, grants }: SubscribeDeps) => Promise<void>;
|
|
68
|
+
export type PublishDeps = {
|
|
69
|
+
req: SSRRequest;
|
|
70
|
+
res: SSRResponseHelpers;
|
|
71
|
+
hub: RealtimeHub;
|
|
72
|
+
};
|
|
73
|
+
/** `POST /_realtime` `{ token, topic, type, data }` — a page on a Server-Sent Events connection, publishing. */
|
|
74
|
+
export declare const handleRealtimePublish: ({ req, res, hub }: PublishDeps) => Promise<void>;
|