@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
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createSessionCookies, sessionCookieParams } from "./session.js";
|
|
2
|
+
import { createCsrf } from "./csrf.js";
|
|
3
|
+
import { applySessionOutcome, authPolicyRules, authRoutes } from "./routes.js";
|
|
4
|
+
import { createCarriers, presentedOrigin } from "./credentials.js";
|
|
2
5
|
import { createAuthorizer } from "./authorize.js";
|
|
3
6
|
import { createTokens } from "./tokens.js";
|
|
4
7
|
import { createAuthApi } from "./api.js";
|
|
5
|
-
import { createCarriers, presentedOrigin } from "./credentials.js";
|
|
6
|
-
import { createCsrf } from "./csrf.js";
|
|
7
8
|
import { createIdentity } from "./identity.js";
|
|
8
|
-
import { applySessionOutcome, authPolicyRules, authRoutes } from "./routes.js";
|
|
9
9
|
//#region src/core/auth/createAuth.ts
|
|
10
10
|
/**
|
|
11
11
|
* The whole auth cycle, wired.
|
|
@@ -9,7 +9,10 @@ export interface CredentialCarrier {
|
|
|
9
9
|
query?: Record<string, unknown>;
|
|
10
10
|
/** Populated by cookie-parser on Express; absent elsewhere, where the header is parsed. */
|
|
11
11
|
cookies?: Record<string, string>;
|
|
12
|
+
/** The client's address as the host resolved it (Express's `req.ip`, honouring its `trust proxy`). */
|
|
13
|
+
ip?: string;
|
|
12
14
|
}
|
|
15
|
+
export declare const readCookies: (carrier: CredentialCarrier) => Record<string, string>;
|
|
13
16
|
/**
|
|
14
17
|
* Where a credential of each kind is allowed to ride.
|
|
15
18
|
*
|
package/dist/core/auth/csrf.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface CsrfConfig {
|
|
|
26
26
|
* space declared. Applying the check below on top would refuse every legitimate embed.
|
|
27
27
|
*/
|
|
28
28
|
export type CsrfSubject = 'write' | 'signIn' | 'delegated';
|
|
29
|
-
export type CsrfFailure = 'missing' | 'malformed' | 'expired' | 'mismatch';
|
|
29
|
+
export type CsrfFailure = 'missing' | 'malformed' | 'expired' | 'mismatch' | 'foreign';
|
|
30
30
|
export declare const csrfFailureMessage: Record<CsrfFailure, string>;
|
|
31
31
|
export type CsrfResult = {
|
|
32
32
|
ok: true;
|
|
@@ -42,6 +42,20 @@ export interface CsrfCarrier {
|
|
|
42
42
|
cookies?: Record<string, string>;
|
|
43
43
|
body?: unknown;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The carrier, read off whatever request a host hands over — named field by field, never spread.
|
|
47
|
+
*
|
|
48
|
+
* A spread copies OWN properties, and on Node's `IncomingMessage` (which an Express request is) `headers` is a getter on
|
|
49
|
+
* the prototype: `{ ...req }` arrives with no headers at all, and the first header read throws. Every guard built on a
|
|
50
|
+
* spread failed that way on every request.
|
|
51
|
+
*/
|
|
52
|
+
export declare const carrierOf: (req: {
|
|
53
|
+
method?: string;
|
|
54
|
+
headers: CsrfCarrier["headers"];
|
|
55
|
+
hostname: string;
|
|
56
|
+
cookies?: unknown;
|
|
57
|
+
body?: unknown;
|
|
58
|
+
}, method?: string | undefined) => CsrfCarrier;
|
|
45
59
|
export declare const createCsrf: (config: CsrfConfig) => {
|
|
46
60
|
issue: (sessionToken?: string) => string;
|
|
47
61
|
/** The name the cookie lands under, for a client that reads it by hand. */
|
|
@@ -79,6 +93,16 @@ export declare const createCsrf: (config: CsrfConfig) => {
|
|
|
79
93
|
* protected by the session cookie's presence, a sign-in by where the request came from. See {@link CsrfSubject}.
|
|
80
94
|
*/
|
|
81
95
|
required: (carrier: CsrfCarrier, subject?: CsrfSubject) => boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Whether a browser sent this from a site this deployment does not recognise — its own origins, and
|
|
98
|
+
* `alsoAllowed` (the origins a SPACE declared, for a request that carries that space's credential).
|
|
99
|
+
*
|
|
100
|
+
* The rule `signIn` flows are guarded by, offered on its own for a router whose writes cannot all carry a token:
|
|
101
|
+
* a cookie-carried write that a foreign page caused is refused by where it came from — see
|
|
102
|
+
* `createOriginGuardMiddleware`. A request with neither Fetch Metadata nor `Origin` is not a browser's, and is
|
|
103
|
+
* never foreign: there is no victim's session in a script.
|
|
104
|
+
*/
|
|
105
|
+
crossSite: (carrier: CsrfCarrier, alsoAllowed?: readonly string[]) => boolean;
|
|
82
106
|
/**
|
|
83
107
|
* The whole check. The echoed token must verify against the session the request carries, and — when the
|
|
84
108
|
* cookie is also present — match it. The second half costs nothing and blocks a token that leaked somewhere
|
package/dist/core/auth/csrf.js
CHANGED
|
@@ -40,8 +40,23 @@ var csrfFailureMessage = {
|
|
|
40
40
|
missing: "A CSRF token is required",
|
|
41
41
|
malformed: "CSRF token malformed",
|
|
42
42
|
expired: "CSRF token expired",
|
|
43
|
-
mismatch: "CSRF token does not match this session"
|
|
43
|
+
mismatch: "CSRF token does not match this session",
|
|
44
|
+
foreign: "A request from another site may not act with this session"
|
|
44
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* The carrier, read off whatever request a host hands over — named field by field, never spread.
|
|
48
|
+
*
|
|
49
|
+
* A spread copies OWN properties, and on Node's `IncomingMessage` (which an Express request is) `headers` is a getter on
|
|
50
|
+
* the prototype: `{ ...req }` arrives with no headers at all, and the first header read throws. Every guard built on a
|
|
51
|
+
* spread failed that way on every request.
|
|
52
|
+
*/
|
|
53
|
+
var carrierOf = (req, method = req.method) => ({
|
|
54
|
+
method,
|
|
55
|
+
headers: req.headers,
|
|
56
|
+
hostname: req.hostname,
|
|
57
|
+
...req.cookies && typeof req.cookies === "object" ? { cookies: req.cookies } : {},
|
|
58
|
+
body: req.body
|
|
59
|
+
});
|
|
45
60
|
var equal = (a, b) => {
|
|
46
61
|
const left = Buffer.from(a);
|
|
47
62
|
const right = Buffer.from(b);
|
|
@@ -94,10 +109,10 @@ var createCsrf = (config) => {
|
|
|
94
109
|
* anything: there is no victim's session sitting in it. That is what keeps every API client, mobile app and
|
|
95
110
|
* script signing in with nothing extra to send.
|
|
96
111
|
*/
|
|
97
|
-
const foreign = (carrier) => {
|
|
112
|
+
const foreign = (carrier, alsoAllowed = []) => {
|
|
98
113
|
const site = header(carrier, "sec-fetch-site");
|
|
99
114
|
const origin = header(carrier, "origin");
|
|
100
|
-
const allowed = origin !== void 0 && permitted(origin);
|
|
115
|
+
const allowed = origin !== void 0 && (permitted(origin) || alsoAllowed.includes(origin));
|
|
101
116
|
if (site !== void 0) return site === "cross-site" && !allowed;
|
|
102
117
|
if (!origin) return false;
|
|
103
118
|
return originHost(origin) !== carrier.hostname && !allowed;
|
|
@@ -212,6 +227,16 @@ var createCsrf = (config) => {
|
|
|
212
227
|
return readCookieHeader(carrier, sessionCookieParams(carrier.hostname, cookie).name) !== void 0;
|
|
213
228
|
},
|
|
214
229
|
/**
|
|
230
|
+
* Whether a browser sent this from a site this deployment does not recognise — its own origins, and
|
|
231
|
+
* `alsoAllowed` (the origins a SPACE declared, for a request that carries that space's credential).
|
|
232
|
+
*
|
|
233
|
+
* The rule `signIn` flows are guarded by, offered on its own for a router whose writes cannot all carry a token:
|
|
234
|
+
* a cookie-carried write that a foreign page caused is refused by where it came from — see
|
|
235
|
+
* `createOriginGuardMiddleware`. A request with neither Fetch Metadata nor `Origin` is not a browser's, and is
|
|
236
|
+
* never foreign: there is no victim's session in a script.
|
|
237
|
+
*/
|
|
238
|
+
crossSite: (carrier, alsoAllowed) => foreign(carrier, alsoAllowed),
|
|
239
|
+
/**
|
|
215
240
|
* The whole check. The echoed token must verify against the session the request carries, and — when the
|
|
216
241
|
* cookie is also present — match it. The second half costs nothing and blocks a token that leaked somewhere
|
|
217
242
|
* the cookie did not.
|
|
@@ -231,4 +256,4 @@ var createCsrf = (config) => {
|
|
|
231
256
|
};
|
|
232
257
|
};
|
|
233
258
|
//#endregion
|
|
234
|
-
export { createCsrf, csrfFailureMessage };
|
|
259
|
+
export { carrierOf, createCsrf, csrfFailureMessage };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AuthApi } from './api';
|
|
2
|
+
import { CredentialCarrier } from './credentials';
|
|
3
|
+
import { Csrf } from './csrf';
|
|
4
|
+
import { CookieSink, SessionCookies } from './session';
|
|
5
|
+
/**
|
|
6
|
+
* A navigation of the whole tab — not a frame, not an image, not a script's fetch. The one kind of request another
|
|
7
|
+
* site cannot make on a visitor's behalf without the visitor watching it happen, which is what lets a renewal (it
|
|
8
|
+
* rotates the session) answer a `GET`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const isDocumentNavigation: (headers: Record<string, string | string[] | undefined>) => boolean;
|
|
11
|
+
export interface RenewalOptions {
|
|
12
|
+
api: AuthApi;
|
|
13
|
+
cookies: SessionCookies;
|
|
14
|
+
/** Re-issued for the renewed session, as every flow that grants one does. */
|
|
15
|
+
csrf?: Csrf;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* `GET <basePath>/refresh?redirect=<page>`: renews the session this browser's refresh cookie holds, and answers where
|
|
19
|
+
* to send the browser next — always back to the page, whether or not the renewal worked.
|
|
20
|
+
*
|
|
21
|
+
* It is where a page server sends a browser whose access cookie has died while its refresh cookie lives on, because
|
|
22
|
+
* this is the one path the refresh cookie is sent to. What it writes is what makes the page render right on the way
|
|
23
|
+
* back: the new session, or — for a refresh token that can never work again — no session and no hint, so the page
|
|
24
|
+
* renders a guest and does not send the browser here a second time.
|
|
25
|
+
*/
|
|
26
|
+
export declare const renewForNavigation: (req: CredentialCarrier, res: CookieSink, { api, cookies, csrf }: RenewalOptions) => Promise<string>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { applySessionOutcome } from "./routes.js";
|
|
2
|
+
//#region src/core/auth/renewal.ts
|
|
3
|
+
/**
|
|
4
|
+
* A navigation of the whole tab — not a frame, not an image, not a script's fetch. The one kind of request another
|
|
5
|
+
* site cannot make on a visitor's behalf without the visitor watching it happen, which is what lets a renewal (it
|
|
6
|
+
* rotates the session) answer a `GET`.
|
|
7
|
+
*/
|
|
8
|
+
var isDocumentNavigation = (headers) => headers["sec-fetch-mode"] === "navigate" && headers["sec-fetch-dest"] === "document";
|
|
9
|
+
/**
|
|
10
|
+
* `GET <basePath>/refresh?redirect=<page>`: renews the session this browser's refresh cookie holds, and answers where
|
|
11
|
+
* to send the browser next — always back to the page, whether or not the renewal worked.
|
|
12
|
+
*
|
|
13
|
+
* It is where a page server sends a browser whose access cookie has died while its refresh cookie lives on, because
|
|
14
|
+
* this is the one path the refresh cookie is sent to. What it writes is what makes the page render right on the way
|
|
15
|
+
* back: the new session, or — for a refresh token that can never work again — no session and no hint, so the page
|
|
16
|
+
* renders a guest and does not send the browser here a second time.
|
|
17
|
+
*/
|
|
18
|
+
var renewForNavigation = async (req, res, { api, cookies, csrf }) => {
|
|
19
|
+
const outcome = await api.refresh(cookies.resolveRefreshToken(req), req);
|
|
20
|
+
applySessionOutcome(req, res, outcome, cookies, csrf);
|
|
21
|
+
return cookies.returnTarget(req, req.query?.redirect);
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { isDocumentNavigation, renewForNavigation };
|
package/dist/core/auth/routes.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { csrfFailureMessage } from "./csrf.js";
|
|
1
|
+
import { carrierOf, csrfFailureMessage } from "./csrf.js";
|
|
2
2
|
//#region src/core/auth/routes.ts
|
|
3
3
|
var body = (req) => req.body ?? {};
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* A body field as text. A NUMBER counts — JSON carries an all-digit code or password as one, and a client that sent
|
|
6
|
+
* `123456` has sent a code: reading it as absent answered a right second-factor code with "Invalid code". Anything else
|
|
7
|
+
* (an object, a boolean) is not a value anybody typed, and is absent.
|
|
8
|
+
*/
|
|
5
9
|
var field = (req, name) => {
|
|
6
10
|
const value = body(req)[name];
|
|
11
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
7
12
|
return typeof value === "string" ? value : "";
|
|
8
13
|
};
|
|
9
14
|
/** A body field as a whole number. `NaN` for anything else, which every handler refuses as a missing id. */
|
|
@@ -286,10 +291,7 @@ var authRoutes = ({ api, cookies, csrf }) => FLOWS.map(({ run, csrf: policy, ...
|
|
|
286
291
|
* unbound token satisfies a signed-out caller, which is how signing in is protected when a deployment asks
|
|
287
292
|
* for that.
|
|
288
293
|
*/
|
|
289
|
-
const carrier =
|
|
290
|
-
...req,
|
|
291
|
-
method: route.method
|
|
292
|
-
};
|
|
294
|
+
const carrier = carrierOf(req, route.method);
|
|
293
295
|
if (csrf && csrfNeeded(csrf, policy, carrier)) {
|
|
294
296
|
const result = csrf.verify(carrier, cookies.resolveSessionToken(req));
|
|
295
297
|
if (!result.ok) return {
|
|
@@ -44,6 +44,14 @@ export declare const sessionCookieParams: (hostname: string, config?: SSRAuthCoo
|
|
|
44
44
|
* same question with `credentials: 'include'`.
|
|
45
45
|
*/
|
|
46
46
|
export declare const sessionHintValue: (expiresAt: number, refreshExpiresAt?: number) => string;
|
|
47
|
+
export type SessionHint = {
|
|
48
|
+
/** Unix seconds the access credential dies at. */
|
|
49
|
+
expiresAt: number;
|
|
50
|
+
/** Unix seconds renewal stops being possible at, when the session has a refresh half. */
|
|
51
|
+
refreshExpiresAt?: number;
|
|
52
|
+
};
|
|
53
|
+
/** {@link sessionHintValue} read back — the same reading `@plitzi/sdk-auth` gives it in the browser. */
|
|
54
|
+
export declare const parseSessionHint: (value: string | undefined) => SessionHint | undefined;
|
|
47
55
|
/**
|
|
48
56
|
* Adds to `Set-Cookie` rather than replacing it. A handler may already have written one — clearing an OAuth flow
|
|
49
57
|
* before granting the session it produced is exactly that — and replacing the header would silently drop it.
|
|
@@ -93,6 +101,40 @@ export declare const readRefreshToken: (req: SSRRequest | {
|
|
|
93
101
|
};
|
|
94
102
|
hostname: string;
|
|
95
103
|
}, config?: SSRAuthCookie) => string | undefined;
|
|
104
|
+
/** What the readable companion cookie says about this browser's session, without presenting any credential. */
|
|
105
|
+
export declare const readSessionHint: (req: SSRRequest | {
|
|
106
|
+
headers: {
|
|
107
|
+
cookie?: string;
|
|
108
|
+
};
|
|
109
|
+
hostname: string;
|
|
110
|
+
}, config?: SSRAuthCookie) => SessionHint | undefined;
|
|
111
|
+
/**
|
|
112
|
+
* How long a page server holds off sending a browser to renew again. Long enough to cover the round trip there and
|
|
113
|
+
* back, short enough that a renewal which failed for a passing reason is retried on a later visit.
|
|
114
|
+
*/
|
|
115
|
+
export declare const RENEWAL_GUARD_SECONDS = 30;
|
|
116
|
+
/**
|
|
117
|
+
* Marks a browser as having just been sent to renew its session, so the page it comes back to is rendered rather
|
|
118
|
+
* than bounced again. Every renewal that can fail without ending the session — the refresh cookie not reaching the
|
|
119
|
+
* endpoint, the store being down — would otherwise send the visitor round in a loop.
|
|
120
|
+
*/
|
|
121
|
+
export declare const writeRenewalGuard: (req: {
|
|
122
|
+
hostname: string;
|
|
123
|
+
}, res: CookieSink, config?: SSRAuthCookie) => void;
|
|
124
|
+
/**
|
|
125
|
+
* Where a renewal sends the browser back to: a path on this host, or a page on any host this session's cookies are
|
|
126
|
+
* scoped to — the only hosts that could have sent a browser here to renew, since the hint that triggers it rides on
|
|
127
|
+
* the same cookie domain. Anything else is an open redirect, and goes to `/` instead.
|
|
128
|
+
*/
|
|
129
|
+
export declare const sessionReturnTarget: (req: {
|
|
130
|
+
hostname: string;
|
|
131
|
+
}, target: unknown, config?: SSRAuthCookie) => string;
|
|
132
|
+
export declare const readRenewalGuard: (req: SSRRequest | {
|
|
133
|
+
headers: {
|
|
134
|
+
cookie?: string;
|
|
135
|
+
};
|
|
136
|
+
hostname: string;
|
|
137
|
+
}, config?: SSRAuthCookie) => boolean;
|
|
96
138
|
/** Anything a credential can be read off: this server's own request, an Express one, a bare `node:http` one. */
|
|
97
139
|
export type CookieCarrier = SSRRequest | {
|
|
98
140
|
headers: {
|
|
@@ -124,6 +166,9 @@ export declare const createSessionCookies: (config?: SSRAuthCookie) => {
|
|
|
124
166
|
hostname: string;
|
|
125
167
|
}, res: CookieSink) => void;
|
|
126
168
|
readFlow: (req: CookieCarrier) => string | undefined;
|
|
169
|
+
returnTarget: (req: {
|
|
170
|
+
hostname: string;
|
|
171
|
+
}, target: unknown) => string;
|
|
127
172
|
/**
|
|
128
173
|
* The session credential from `Authorization: Bearer` or the cookie, so one endpoint serves a bearer client and a
|
|
129
174
|
* browser. The header wins: a caller that went to the trouble of presenting one means it.
|
|
@@ -51,6 +51,18 @@ var sessionCookieParams = (hostname, config = {}) => {
|
|
|
51
51
|
* same question with `credentials: 'include'`.
|
|
52
52
|
*/
|
|
53
53
|
var sessionHintValue = (expiresAt, refreshExpiresAt) => `${Math.floor(expiresAt)}.${refreshExpiresAt === void 0 ? "" : Math.floor(refreshExpiresAt)}`;
|
|
54
|
+
/** {@link sessionHintValue} read back — the same reading `@plitzi/sdk-auth` gives it in the browser. */
|
|
55
|
+
var parseSessionHint = (value) => {
|
|
56
|
+
if (!value) return;
|
|
57
|
+
const [expiresAt, refreshExpiresAt] = value.split(".");
|
|
58
|
+
const expiry = Number(expiresAt);
|
|
59
|
+
if (!Number.isFinite(expiry) || expiry <= 0) return;
|
|
60
|
+
const refreshExpiry = Number(refreshExpiresAt);
|
|
61
|
+
return {
|
|
62
|
+
expiresAt: expiry,
|
|
63
|
+
refreshExpiresAt: Number.isFinite(refreshExpiry) && refreshExpiry > 0 ? refreshExpiry : void 0
|
|
64
|
+
};
|
|
65
|
+
};
|
|
54
66
|
var serializeCookie = (name, value, maxAgeSeconds, params, { httpOnly = true, path = "/" } = {}) => {
|
|
55
67
|
const parts = [
|
|
56
68
|
`${name}=${encodeURIComponent(value)}`,
|
|
@@ -127,6 +139,39 @@ var readCookie = (req, name) => {
|
|
|
127
139
|
var readSessionToken = (req, config) => readCookie(req, sessionCookieParams(req.hostname, config).name);
|
|
128
140
|
/** The renewal credential, which rides in its own cookie confined to the refresh path. */
|
|
129
141
|
var readRefreshToken = (req, config) => readCookie(req, `${sessionCookieParams(req.hostname, config).name}_refresh`);
|
|
142
|
+
/** What the readable companion cookie says about this browser's session, without presenting any credential. */
|
|
143
|
+
var readSessionHint = (req, config) => {
|
|
144
|
+
const params = sessionCookieParams(req.hostname, config);
|
|
145
|
+
return parseSessionHint(readCookie(req, `${params.name}${params.hintSuffix}`));
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Marks a browser as having just been sent to renew its session, so the page it comes back to is rendered rather
|
|
149
|
+
* than bounced again. Every renewal that can fail without ending the session — the refresh cookie not reaching the
|
|
150
|
+
* endpoint, the store being down — would otherwise send the visitor round in a loop.
|
|
151
|
+
*/
|
|
152
|
+
var writeRenewalGuard = (req, res, config) => {
|
|
153
|
+
const params = sessionCookieParams(req.hostname, config);
|
|
154
|
+
appendCookies(res, [serializeCookie(`${params.name}_renewing`, "1", 30, params)]);
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* Where a renewal sends the browser back to: a path on this host, or a page on any host this session's cookies are
|
|
158
|
+
* scoped to — the only hosts that could have sent a browser here to renew, since the hint that triggers it rides on
|
|
159
|
+
* the same cookie domain. Anything else is an open redirect, and goes to `/` instead.
|
|
160
|
+
*/
|
|
161
|
+
var sessionReturnTarget = (req, target, config) => {
|
|
162
|
+
if (typeof target !== "string" || target === "") return "/";
|
|
163
|
+
if (target.startsWith("/") && !target.startsWith("//") && !target.startsWith("/\\")) return target;
|
|
164
|
+
let url;
|
|
165
|
+
try {
|
|
166
|
+
url = new URL(target);
|
|
167
|
+
} catch {
|
|
168
|
+
return "/";
|
|
169
|
+
}
|
|
170
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return "/";
|
|
171
|
+
const { domain } = sessionCookieParams(req.hostname, config);
|
|
172
|
+
return url.hostname === req.hostname || domain !== void 0 && (url.hostname === domain.slice(1) || url.hostname.endsWith(domain)) ? url.toString() : "/";
|
|
173
|
+
};
|
|
174
|
+
var readRenewalGuard = (req, config) => readCookie(req, `${sessionCookieParams(req.hostname, config).name}_renewing`) !== void 0;
|
|
130
175
|
/**
|
|
131
176
|
* The cookie side of a session with this deployment's naming already bound in.
|
|
132
177
|
*
|
|
@@ -142,6 +187,7 @@ var createSessionCookies = (config) => ({
|
|
|
142
187
|
writeFlow: (req, res, value, ttlSeconds) => writeFlowCookie(req, res, value, ttlSeconds, config),
|
|
143
188
|
clearFlow: (req, res) => clearFlowCookie(req, res, config),
|
|
144
189
|
readFlow: (req) => readFlowCookie(req, config),
|
|
190
|
+
returnTarget: (req, target) => sessionReturnTarget(req, target, config),
|
|
145
191
|
/**
|
|
146
192
|
* The session credential from `Authorization: Bearer` or the cookie, so one endpoint serves a bearer client and a
|
|
147
193
|
* browser. The header wins: a caller that went to the trouble of presenting one means it.
|
|
@@ -162,4 +208,4 @@ var createSessionCookies = (config) => ({
|
|
|
162
208
|
}
|
|
163
209
|
});
|
|
164
210
|
//#endregion
|
|
165
|
-
export { appendCookies, clearFlowCookie, clearSessionCookies, createSessionCookies, isLocalHost, readFlowCookie, readRefreshToken, readSessionToken, registrableDomain, sessionCookieParams, sessionHintValue, writeFlowCookie, writeSessionCookies };
|
|
211
|
+
export { appendCookies, clearFlowCookie, clearSessionCookies, createSessionCookies, isLocalHost, parseSessionHint, readFlowCookie, readRefreshToken, readRenewalGuard, readSessionHint, readSessionToken, registrableDomain, sessionCookieParams, sessionHintValue, sessionReturnTarget, writeFlowCookie, writeRenewalGuard, writeSessionCookies };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When a session was last used, to the resolution a device list needs.
|
|
3
|
+
*
|
|
4
|
+
* "Active 3 minutes ago" against "active last March" is what tells somebody which of their devices is the one they do
|
|
5
|
+
* not recognise — and the time a session was CREATED says neither: a laptop signed in a month ago and used this morning
|
|
6
|
+
* reads as a month-old session. Recording every request would be a write per request on the hottest read in the
|
|
7
|
+
* system, so a store records it at most once per this many seconds, on the lookup it already makes.
|
|
8
|
+
*/
|
|
9
|
+
export declare const SESSION_ACTIVITY_RESOLUTION_SECONDS = 300;
|
|
10
|
+
/** Whether a session last seen at `lastActiveAt` (unix seconds, absent when never) is due a new record at `now`. */
|
|
11
|
+
export declare const activityDue: (lastActiveAt: number | null | undefined, now: number) => boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/core/auth/sessionActivity.ts
|
|
2
|
+
/**
|
|
3
|
+
* When a session was last used, to the resolution a device list needs.
|
|
4
|
+
*
|
|
5
|
+
* "Active 3 minutes ago" against "active last March" is what tells somebody which of their devices is the one they do
|
|
6
|
+
* not recognise — and the time a session was CREATED says neither: a laptop signed in a month ago and used this morning
|
|
7
|
+
* reads as a month-old session. Recording every request would be a write per request on the hottest read in the
|
|
8
|
+
* system, so a store records it at most once per this many seconds, on the lookup it already makes.
|
|
9
|
+
*/
|
|
10
|
+
var SESSION_ACTIVITY_RESOLUTION_SECONDS = 300;
|
|
11
|
+
/** Whether a session last seen at `lastActiveAt` (unix seconds, absent when never) is due a new record at `now`. */
|
|
12
|
+
var activityDue = (lastActiveAt, now) => lastActiveAt === null || lastActiveAt === void 0 || now - lastActiveAt >= 300;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { SESSION_ACTIVITY_RESOLUTION_SECONDS, activityDue };
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { ThrottleAttempt } from './api';
|
|
2
|
+
import { StoreMethods } from '../server/fleet/channel';
|
|
2
3
|
export declare const createMemoryRateLimit: () => ((attempt: ThrottleAttempt) => Promise<{
|
|
3
4
|
allowed: boolean;
|
|
4
5
|
retryAfter?: number;
|
|
5
6
|
}>);
|
|
7
|
+
type RateLimitStore = {
|
|
8
|
+
check: ReturnType<typeof createMemoryRateLimit>;
|
|
9
|
+
};
|
|
10
|
+
/** Every method of the store, for the copy a fleet's workers share (see `fleet/stores.ts`). */
|
|
11
|
+
export declare const RATE_LIMIT_METHODS: StoreMethods<RateLimitStore>;
|
|
12
|
+
/**
|
|
13
|
+
* In a worker of a fleet, the primary's {@link createMemoryRateLimit}, so an attacker spreading attempts over
|
|
14
|
+
* connections meets one counter rather than one per worker. Nothing anywhere else.
|
|
15
|
+
*/
|
|
16
|
+
export declare const fleetRateLimit: () => ((attempt: ThrottleAttempt) => Promise<{
|
|
17
|
+
allowed: boolean;
|
|
18
|
+
retryAfter?: number;
|
|
19
|
+
}>) | undefined;
|
|
20
|
+
export {};
|
|
@@ -1,4 +1,17 @@
|
|
|
1
|
+
import { fleetStore } from "../server/fleet/link.js";
|
|
1
2
|
//#region src/core/auth/throttle.ts
|
|
3
|
+
/**
|
|
4
|
+
* The rate limit a deployment gets without asking for one.
|
|
5
|
+
*
|
|
6
|
+
* This used to be a required-to-be-safe option: no `rateLimit`, no limit, and sign-in was an unmetered password
|
|
7
|
+
* oracle. Nobody sets an option they have not read about, so the effect of "there is no sensible default" was that
|
|
8
|
+
* the common deployment shipped without any — which is not a neutral outcome, it is the bad one.
|
|
9
|
+
*
|
|
10
|
+
* It counts in memory, and that is an honest limitation rather than a hidden one: several replicas each keep
|
|
11
|
+
* their own window, so four replicas tolerate four times the attempts. Four times a small number is still a small
|
|
12
|
+
* number, and it beats infinity. A deployment that wants one counter for all of them supplies `rateLimit` and puts
|
|
13
|
+
* it in Redis. The workers of one server are one replica: they count in the primary (`fleetRateLimit`).
|
|
14
|
+
*/
|
|
2
15
|
/** Attempts allowed per window, per key, per action. */
|
|
3
16
|
var LIMITS = {
|
|
4
17
|
/** Credential stuffing is the thing being priced out. Ten wrong passwords in five minutes is already a lot. */
|
|
@@ -77,5 +90,20 @@ var createMemoryRateLimit = () => {
|
|
|
77
90
|
return Promise.resolve({ allowed: true });
|
|
78
91
|
};
|
|
79
92
|
};
|
|
93
|
+
/** Every method of the store, for the copy a fleet's workers share (see `fleet/stores.ts`). */
|
|
94
|
+
var RATE_LIMIT_METHODS = { check: true };
|
|
95
|
+
/**
|
|
96
|
+
* In a worker of a fleet, the primary's {@link createMemoryRateLimit}, so an attacker spreading attempts over
|
|
97
|
+
* connections meets one counter rather than one per worker. Nothing anywhere else.
|
|
98
|
+
*/
|
|
99
|
+
var fleetRateLimit = () => {
|
|
100
|
+
const shared = fleetStore("auth.rateLimit", RATE_LIMIT_METHODS);
|
|
101
|
+
if (!shared) return;
|
|
102
|
+
return (attempt) => shared.check({
|
|
103
|
+
action: attempt.action,
|
|
104
|
+
key: attempt.key,
|
|
105
|
+
...attempt.succeeded ? { succeeded: true } : {}
|
|
106
|
+
});
|
|
107
|
+
};
|
|
80
108
|
//#endregion
|
|
81
|
-
export { createMemoryRateLimit };
|
|
109
|
+
export { RATE_LIMIT_METHODS, createMemoryRateLimit, fleetRateLimit };
|
package/dist/core/auth/totp.d.ts
CHANGED
|
@@ -3,15 +3,23 @@ export declare const generateTotpSecret: () => string;
|
|
|
3
3
|
/** The code an app would be showing right now. Exported so a test does not have to reimplement the algorithm. */
|
|
4
4
|
export declare const totpCode: (secret: string, at?: number) => string;
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Which time step this code belongs to, or `undefined` when it belongs to none near now.
|
|
7
7
|
*
|
|
8
8
|
* `window` steps either side are accepted, one by default — thirty seconds of tolerance for a clock that drifts
|
|
9
9
|
* and for a person who starts typing at second twenty-nine. Wider is friendlier and weaker; this is the usual
|
|
10
10
|
* trade and the same one Google Authenticator's own guidance makes.
|
|
11
11
|
*
|
|
12
|
+
* The step is the answer, not a yes: a code stays valid for its whole window, so a verifier that remembers nothing
|
|
13
|
+
* accepts the same code twice. RFC 6238 §5.2 forbids that — see `MfaRecord.lastUsedStep`.
|
|
14
|
+
*
|
|
12
15
|
* Compared in constant time: a comparison that returns on the first differing digit leaks how much of a guess was
|
|
13
16
|
* right, which turns a million-guess space into ten guesses per position.
|
|
14
17
|
*/
|
|
18
|
+
export declare const totpStep: (secret: string, code: string, options?: {
|
|
19
|
+
window?: number;
|
|
20
|
+
at?: number;
|
|
21
|
+
}) => number | undefined;
|
|
22
|
+
/** Is this the code? `totpStep`, for a caller that does not need to know which step it was. */
|
|
15
23
|
export declare const verifyTotp: (secret: string, code: string, options?: {
|
|
16
24
|
window?: number;
|
|
17
25
|
at?: number;
|
package/dist/core/auth/totp.js
CHANGED
|
@@ -55,28 +55,33 @@ var codeAt = (secret, counter) => {
|
|
|
55
55
|
/** The code an app would be showing right now. Exported so a test does not have to reimplement the algorithm. */
|
|
56
56
|
var totpCode = (secret, at = Date.now()) => codeAt(secret, Math.floor(at / 1e3 / STEP_SECONDS));
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
58
|
+
* Which time step this code belongs to, or `undefined` when it belongs to none near now.
|
|
59
59
|
*
|
|
60
60
|
* `window` steps either side are accepted, one by default — thirty seconds of tolerance for a clock that drifts
|
|
61
61
|
* and for a person who starts typing at second twenty-nine. Wider is friendlier and weaker; this is the usual
|
|
62
62
|
* trade and the same one Google Authenticator's own guidance makes.
|
|
63
63
|
*
|
|
64
|
+
* The step is the answer, not a yes: a code stays valid for its whole window, so a verifier that remembers nothing
|
|
65
|
+
* accepts the same code twice. RFC 6238 §5.2 forbids that — see `MfaRecord.lastUsedStep`.
|
|
66
|
+
*
|
|
64
67
|
* Compared in constant time: a comparison that returns on the first differing digit leaks how much of a guess was
|
|
65
68
|
* right, which turns a million-guess space into ten guesses per position.
|
|
66
69
|
*/
|
|
67
|
-
var
|
|
70
|
+
var totpStep = (secret, code, options = {}) => {
|
|
68
71
|
const { window = 1, at = Date.now() } = options;
|
|
69
72
|
const candidate = code.trim();
|
|
70
|
-
if (!/^\d{6}$/.test(candidate)) return
|
|
73
|
+
if (!/^\d{6}$/.test(candidate)) return;
|
|
71
74
|
const counter = Math.floor(at / 1e3 / STEP_SECONDS);
|
|
72
75
|
const supplied = Buffer.from(candidate);
|
|
73
|
-
let matched
|
|
76
|
+
let matched;
|
|
74
77
|
for (let drift = -window; drift <= window; drift++) {
|
|
75
78
|
const expected = Buffer.from(codeAt(secret, counter + drift));
|
|
76
|
-
|
|
79
|
+
if (timingSafeEqual(expected, supplied)) matched = counter + drift;
|
|
77
80
|
}
|
|
78
81
|
return matched;
|
|
79
82
|
};
|
|
83
|
+
/** Is this the code? `totpStep`, for a caller that does not need to know which step it was. */
|
|
84
|
+
var verifyTotp = (secret, code, options = {}) => totpStep(secret, code, options) !== void 0;
|
|
80
85
|
/**
|
|
81
86
|
* The URI an authenticator app scans. `issuer` appears twice by convention — in the label and as a parameter —
|
|
82
87
|
* because different apps read different ones, and an entry that says only an email address is unidentifiable in a
|
|
@@ -115,4 +120,4 @@ var generateRecoveryCodes = (count = 10) => Array.from({ length: count }, () =>
|
|
|
115
120
|
/** Normalised before hashing so that case and the hyphen a person types or omits do not decide whether it works. */
|
|
116
121
|
var normalizeRecoveryCode = (code) => code.trim().toUpperCase().replace(/[^A-Z0-9]/g, "");
|
|
117
122
|
//#endregion
|
|
118
|
-
export { generateRecoveryCodes, generateTotpSecret, normalizeRecoveryCode, randomCode, totpCode, totpUri, verifyTotp };
|
|
123
|
+
export { generateRecoveryCodes, generateTotpSecret, normalizeRecoveryCode, randomCode, totpCode, totpStep, totpUri, verifyTotp };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -19,5 +19,18 @@ export type ServerConfig = Omit<SSRServerConfig, 'adapters'> & {
|
|
|
19
19
|
* `adapters` or `authCookie` still wins, so a deployment can override one piece without giving up the rest.
|
|
20
20
|
*/
|
|
21
21
|
auth?: Auth;
|
|
22
|
+
/**
|
|
23
|
+
* Where a page request whose access cookie has died, while its refresh cookie lives on, is sent to renew before
|
|
24
|
+
* the page is rendered — so the server renders the visitor the browser is about to become, rather than a guest
|
|
25
|
+
* the browser then replaces. The endpoint is `GET <basePath>/refresh`, served by `auth` here or by
|
|
26
|
+
* `mountAuthRoutes` wherever `/auth` lives.
|
|
27
|
+
*
|
|
28
|
+
* On by default with `auth`, at its own `/refresh`. Without it, name the endpoint — an absolute URL when another
|
|
29
|
+
* host serves `/auth` (a host sharing the session's cookie domain, or the hint that triggers this never arrives).
|
|
30
|
+
* `false` turns it off.
|
|
31
|
+
*/
|
|
32
|
+
sessionRenewal?: {
|
|
33
|
+
url: string;
|
|
34
|
+
} | false;
|
|
22
35
|
};
|
|
23
|
-
export declare const createServer: ({ auth, ...config }: ServerConfig, extensions?: PipelineExtensions) => SSRServer;
|
|
36
|
+
export declare const createServer: ({ auth, sessionRenewal, ...config }: ServerConfig, extensions?: PipelineExtensions) => SSRServer;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { actionsModuleFor } from "../modules/actions/moduleFor.js";
|
|
2
2
|
import { connectorRscData } from "../modules/rsc/connectorRscData.js";
|
|
3
3
|
import { createAuthApiStage } from "./http/stages/authApi.js";
|
|
4
|
+
import { createRenewalEndpointStage, createSessionRenewalStage } from "./http/stages/sessionRenewal.js";
|
|
4
5
|
import { resolveServices } from "./services/resolve.js";
|
|
5
6
|
import { createPageServer } from "./server/pageServer.js";
|
|
6
7
|
//#region src/core/createServer.ts
|
|
@@ -24,23 +25,27 @@ import { createPageServer } from "./server/pageServer.js";
|
|
|
24
25
|
*/
|
|
25
26
|
var withConnectorRsc = (config) => {
|
|
26
27
|
if (config.adapters.getRscData || !config.connectors && !config.action?.lookups) return config;
|
|
27
|
-
const
|
|
28
|
+
const resolved = {
|
|
29
|
+
...config,
|
|
30
|
+
adapters: { ...config.adapters }
|
|
31
|
+
};
|
|
32
|
+
const module = actionsModuleFor(resolved);
|
|
28
33
|
const actions = module && config.action?.lookups ? {
|
|
29
34
|
lookups: config.action.lookups,
|
|
30
35
|
module
|
|
31
36
|
} : void 0;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
adapters: {
|
|
35
|
-
...config.adapters,
|
|
36
|
-
getRscData: connectorRscData(config.connectors, actions, config.rsc?.elementTimeoutMs)
|
|
37
|
-
}
|
|
38
|
-
};
|
|
37
|
+
resolved.adapters.getRscData = connectorRscData(config.connectors, actions, config.rsc?.elementTimeoutMs);
|
|
38
|
+
return resolved;
|
|
39
39
|
};
|
|
40
|
-
|
|
40
|
+
/** The renewal stage in front of whatever else gates itself, when there is anywhere to renew. */
|
|
41
|
+
var withRenewal = (extensions, url) => ({
|
|
42
|
+
...extensions,
|
|
43
|
+
preAuth: [...extensions?.preAuth ?? [], ...url ? [createSessionRenewalStage(url)] : []]
|
|
44
|
+
});
|
|
45
|
+
var createServer = ({ auth, sessionRenewal, ...config }, extensions) => {
|
|
41
46
|
if (!auth) {
|
|
42
47
|
const resolvedConfig = withConnectorRsc(config);
|
|
43
|
-
return createPageServer(resolvedConfig, resolveServices(resolvedConfig), extensions);
|
|
48
|
+
return createPageServer(resolvedConfig, resolveServices(resolvedConfig), withRenewal(extensions, sessionRenewal ? sessionRenewal.url : void 0));
|
|
44
49
|
}
|
|
45
50
|
const entries = Object.entries(config.adapters);
|
|
46
51
|
const supplied = Object.fromEntries(entries.filter(([, value]) => value !== void 0));
|
|
@@ -57,9 +62,14 @@ var createServer = ({ auth, ...config }, extensions) => {
|
|
|
57
62
|
});
|
|
58
63
|
const withAuthRoutes = {
|
|
59
64
|
...extensions,
|
|
60
|
-
preAuth: [
|
|
65
|
+
preAuth: [
|
|
66
|
+
...extensions?.preAuth ?? [],
|
|
67
|
+
createAuthApiStage(auth, auth.basePath),
|
|
68
|
+
createRenewalEndpointStage(auth)
|
|
69
|
+
]
|
|
61
70
|
};
|
|
62
|
-
|
|
71
|
+
const renewalUrl = sessionRenewal === false ? void 0 : sessionRenewal?.url ?? `${auth.basePath}/refresh`;
|
|
72
|
+
return createPageServer(resolved, resolveServices(resolved), withRenewal(withAuthRoutes, renewalUrl));
|
|
63
73
|
};
|
|
64
74
|
//#endregion
|
|
65
75
|
export { createServer, resolveServices };
|
package/dist/core/fontStore.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
3
|
//#region src/core/fontStore.ts
|
|
4
4
|
/** `<spaceId>/<name>` — the space segment is what keeps one space's files out of another's. */
|
|
5
5
|
var storePathOf = (spaceId, name) => `${spaceId}/${name}`;
|
|
@@ -12,7 +12,7 @@ export interface AuthRouteHandlersOptions {
|
|
|
12
12
|
* deployment that means to go without says so with `csrf: false` there rather than by forgetting here.
|
|
13
13
|
*/
|
|
14
14
|
csrf?: Csrf;
|
|
15
|
-
/** Reports a flow that threw. Without it the failure goes to `
|
|
15
|
+
/** Reports a flow that threw. Without it the failure goes to the server log at `error`; the caller still gets a 500. */
|
|
16
16
|
onError?: (error: unknown, context: {
|
|
17
17
|
method: string;
|
|
18
18
|
path: string;
|