@plitzi/sdk-server 0.33.2 → 0.34.1
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 +54 -0
- package/README.md +283 -8
- package/dist/actions.d.ts +62 -0
- package/dist/actions.js +12 -0
- package/dist/adapters/cloudAdapters.d.ts +72 -0
- package/dist/adapters/cloudAdapters.js +303 -0
- package/dist/adapters/space/index.d.ts +93 -0
- package/dist/adapters/space/index.js +122 -0
- package/dist/adapters/space/resolvers.d.ts +69 -0
- package/dist/adapters/space/resolvers.js +79 -0
- package/dist/adapters/space/space.test.d.ts +1 -0
- package/dist/adapters/space/types.d.ts +64 -0
- package/dist/adapters/space/types.js +41 -0
- package/dist/auth.d.ts +50 -0
- package/dist/auth.js +19 -0
- package/dist/core/auth/api.d.ts +400 -11
- package/dist/core/auth/api.js +1044 -50
- package/dist/core/auth/createAuth.d.ts +135 -30
- package/dist/core/auth/createAuth.js +96 -24
- package/dist/core/auth/credentials.d.ts +9 -3
- package/dist/core/auth/credentials.js +10 -4
- package/dist/core/auth/csrf.d.ts +89 -0
- package/dist/core/auth/csrf.js +234 -0
- package/dist/core/auth/csrf.test.d.ts +1 -0
- package/dist/core/auth/identity.d.ts +6 -0
- package/dist/core/auth/identity.js +37 -8
- package/dist/core/auth/lifecycle.test.d.ts +1 -0
- package/dist/core/auth/oauth/index.d.ts +8 -0
- package/dist/core/auth/oauth/index.js +13 -15
- package/dist/core/auth/passwords.d.ts +15 -0
- package/dist/core/auth/passwords.js +32 -0
- package/dist/core/auth/redirects.d.ts +18 -0
- package/dist/core/auth/redirects.js +31 -0
- package/dist/core/auth/redirects.test.d.ts +1 -0
- package/dist/core/auth/routes.d.ts +4 -2
- package/dist/core/auth/routes.js +234 -9
- package/dist/core/auth/session.d.ts +11 -0
- package/dist/core/auth/session.js +1 -1
- package/dist/core/auth/spaceTokens.d.ts +38 -2
- package/dist/core/auth/spaceTokens.js +54 -2
- package/dist/core/auth/throttle.d.ts +5 -0
- package/dist/core/auth/throttle.js +81 -0
- package/dist/core/auth/throttle.test.d.ts +1 -0
- package/dist/core/auth/tokens.d.ts +83 -12
- package/dist/core/auth/tokens.js +51 -9
- package/dist/core/auth/totp.d.ts +48 -0
- package/dist/core/auth/totp.js +118 -0
- package/dist/core/auth/totp.test.d.ts +1 -0
- package/dist/core/createServer.d.ts +0 -6
- package/dist/core/createServer.js +35 -3
- package/dist/core/fontStore.d.ts +33 -0
- package/dist/core/fontStore.js +53 -0
- package/dist/core/fontStore.test.d.ts +1 -0
- package/dist/core/handlers/authMiddleware.js +1 -1
- package/dist/core/handlers/authRouteHandlers.d.ts +9 -1
- package/dist/core/handlers/authRouteHandlers.js +4 -3
- package/dist/core/handlers/csrfMiddleware.d.ts +20 -0
- package/dist/core/handlers/csrfMiddleware.js +32 -0
- package/dist/core/handlers/oauthRouteHandlers.d.ts +46 -0
- package/dist/core/handlers/oauthRouteHandlers.js +159 -0
- package/dist/core/handlers/socialRouteHandlers.d.ts +40 -0
- package/dist/core/handlers/socialRouteHandlers.js +129 -0
- package/dist/core/handlers/socialRouteHandlers.test.d.ts +1 -0
- package/dist/core/handlers/types.d.ts +22 -5
- package/dist/core/health.d.ts +11 -1
- package/dist/core/health.js +27 -3
- package/dist/core/http/dispatcher.d.ts +1 -1
- package/dist/core/http/dispatcher.js +20 -1
- package/dist/core/http/stages/authApi.js +1 -1
- package/dist/core/http/stages/fontAssets.d.ts +9 -0
- package/dist/core/http/stages/fontAssets.js +27 -0
- package/dist/core/http/stages/oauth.d.ts +19 -0
- package/dist/core/http/stages/oauth.js +118 -0
- package/dist/core/http/types.d.ts +12 -0
- package/dist/core/oauth/authorize.d.ts +22 -0
- package/dist/core/oauth/authorize.js +254 -0
- package/dist/core/oauth/authorize.test.d.ts +1 -0
- package/dist/core/oauth/challenge.d.ts +11 -0
- package/dist/core/oauth/challenge.js +33 -0
- package/dist/core/oauth/consentPage.d.ts +12 -0
- package/dist/core/oauth/consentPage.js +131 -0
- package/dist/core/oauth/metadata.d.ts +41 -0
- package/dist/core/oauth/metadata.js +71 -0
- package/dist/core/oauth/params.d.ts +5 -0
- package/dist/core/oauth/params.js +5 -0
- package/dist/core/oauth/pkce.d.ts +6 -0
- package/dist/core/oauth/pkce.js +15 -0
- package/dist/core/oauth/records.d.ts +72 -0
- package/dist/core/oauth/records.js +49 -0
- package/dist/core/oauth/register.d.ts +5 -0
- package/dist/core/oauth/register.js +55 -0
- package/dist/core/oauth/respond.d.ts +21 -0
- package/dist/core/oauth/respond.js +59 -0
- package/dist/core/oauth/token.d.ts +14 -0
- package/dist/core/oauth/token.js +130 -0
- package/dist/core/previewToken.d.ts +20 -0
- package/dist/core/previewToken.js +29 -1
- package/dist/core/server/pageServer.js +4 -1
- package/dist/core/services/action.d.ts +6 -4
- package/dist/core/services/action.js +103 -10
- package/dist/core/services/registry.js +2 -0
- package/dist/core/services/resolve.d.ts +8 -0
- package/dist/core/services/resolve.js +12 -1
- package/dist/core/staticFiles.js +1 -2
- package/dist/handlers.d.ts +7 -1
- package/dist/handlers.js +4 -1
- package/dist/helpers/buildResponseHelpers.d.ts +8 -0
- package/dist/helpers/buildResponseHelpers.js +9 -1
- package/dist/helpers/buildServerInfo.d.ts +8 -3
- package/dist/helpers/buildServerInfo.js +18 -14
- package/dist/helpers/cache/keys.d.ts +13 -0
- package/dist/helpers/cache/keys.js +12 -1
- package/dist/helpers/onAbort.d.ts +12 -0
- package/dist/helpers/onAbort.js +22 -0
- package/dist/helpers/onAbort.test.d.ts +1 -0
- package/dist/helpers/outboundGuard.d.ts +27 -0
- package/dist/helpers/outboundGuard.js +49 -0
- package/dist/helpers/outboundGuard.test.d.ts +1 -0
- package/dist/helpers/serverLog.d.ts +6 -3
- package/dist/helpers/serverLog.js +20 -3
- package/dist/index.d.ts +6 -0
- package/dist/index.js +9 -4
- package/dist/kernel.d.ts +5 -22
- package/dist/kernel.js +1 -14
- package/dist/modules/actions/{handler.js → connectorWrite.js} +5 -2
- package/dist/modules/actions/connectorWrite.test.d.ts +1 -0
- package/dist/modules/actions/index.d.ts +40 -0
- package/dist/modules/actions/index.js +46 -0
- package/dist/modules/actions/moduleFor.d.ts +3 -0
- package/dist/modules/actions/moduleFor.js +35 -0
- package/dist/modules/actions/moduleFor.test.d.ts +1 -0
- package/dist/modules/actions/runtime/check.d.ts +24 -0
- package/dist/modules/actions/runtime/check.js +111 -0
- package/dist/modules/actions/runtime/check.test.d.ts +1 -0
- package/dist/modules/actions/runtime/errors.d.ts +11 -0
- package/dist/modules/actions/runtime/errors.js +17 -0
- package/dist/modules/actions/runtime/guards.d.ts +126 -0
- package/dist/modules/actions/runtime/guards.js +246 -0
- package/dist/modules/actions/runtime/guards.test.d.ts +1 -0
- package/dist/modules/actions/runtime/kvStore.d.ts +27 -0
- package/dist/modules/actions/runtime/kvStore.js +41 -0
- package/dist/modules/actions/runtime/kvStore.test.d.ts +1 -0
- package/dist/modules/actions/runtime/limits.d.ts +11 -0
- package/dist/modules/actions/runtime/limits.js +35 -0
- package/dist/modules/actions/runtime/memoryKv.d.ts +2 -0
- package/dist/modules/actions/runtime/memoryKv.js +77 -0
- package/dist/modules/actions/runtime/namespaceKv.d.ts +9 -0
- package/dist/modules/actions/runtime/namespaceKv.js +19 -0
- package/dist/modules/actions/runtime/precheck.d.ts +27 -0
- package/dist/modules/actions/runtime/precheck.js +45 -0
- package/dist/modules/actions/runtime/renderResolver.d.ts +15 -0
- package/dist/modules/actions/runtime/renderResolver.js +130 -0
- package/dist/modules/actions/runtime/renderResolver.test.d.ts +1 -0
- package/dist/modules/actions/runtime/renderShare.d.ts +29 -0
- package/dist/modules/actions/runtime/renderShare.js +36 -0
- package/dist/modules/actions/runtime/renderShare.test.d.ts +1 -0
- package/dist/modules/actions/runtime/report.d.ts +14 -0
- package/dist/modules/actions/runtime/report.js +22 -0
- package/dist/modules/actions/runtime/runAction.d.ts +12 -0
- package/dist/modules/actions/runtime/runAction.js +405 -0
- package/dist/modules/actions/runtime/runAction.test.d.ts +1 -0
- package/dist/modules/actions/runtime/runLogger.d.ts +31 -0
- package/dist/modules/actions/runtime/runLogger.js +66 -0
- package/dist/modules/actions/runtime/runLogger.test.d.ts +1 -0
- package/dist/modules/actions/runtime/schedule.d.ts +35 -0
- package/dist/modules/actions/runtime/schedule.js +76 -0
- package/dist/modules/actions/runtime/schedule.test.d.ts +1 -0
- package/dist/modules/actions/runtime/scope.d.ts +40 -0
- package/dist/modules/actions/runtime/scope.js +103 -0
- package/dist/modules/actions/runtime/triggers.d.ts +12 -0
- package/dist/modules/actions/runtime/triggers.js +14 -0
- package/dist/modules/actions/taskCatalog.d.ts +26 -0
- package/dist/modules/actions/taskCatalog.js +24 -0
- package/dist/modules/actions/taskCatalog.test.d.ts +1 -0
- package/dist/modules/actions/tasks/auth.d.ts +2 -0
- package/dist/modules/actions/tasks/auth.js +40 -0
- package/dist/modules/actions/tasks/builtins.d.ts +8 -0
- package/dist/modules/actions/tasks/builtins.js +24 -0
- package/dist/modules/actions/tasks/connector.d.ts +2 -0
- package/dist/modules/actions/tasks/connector.js +141 -0
- package/dist/modules/actions/tasks/db.d.ts +2 -0
- package/dist/modules/actions/tasks/db.js +59 -0
- package/dist/modules/actions/tasks/db.test.d.ts +1 -0
- package/dist/modules/actions/tasks/flow.d.ts +3 -0
- package/dist/modules/actions/tasks/flow.js +109 -0
- package/dist/modules/actions/tasks/helpers.d.ts +16 -0
- package/dist/modules/actions/tasks/helpers.js +30 -0
- package/dist/modules/actions/tasks/http.d.ts +2 -0
- package/dist/modules/actions/tasks/http.js +111 -0
- package/dist/modules/actions/tasks/http.test.d.ts +1 -0
- package/dist/modules/actions/tasks/kv.d.ts +2 -0
- package/dist/modules/actions/tasks/kv.js +102 -0
- package/dist/modules/actions/tasks/kv.test.d.ts +1 -0
- package/dist/modules/actions/tasks/registry.d.ts +10 -0
- package/dist/modules/actions/tasks/registry.js +36 -0
- package/dist/modules/actions/tasks/transform.d.ts +2 -0
- package/dist/modules/actions/tasks/transform.js +34 -0
- package/dist/modules/actions/transport/callHandler.d.ts +24 -0
- package/dist/modules/actions/transport/callHandler.js +233 -0
- package/dist/modules/actions/transport/callHandler.test.d.ts +1 -0
- package/dist/modules/actions/transport/cancelHandler.d.ts +23 -0
- package/dist/modules/actions/transport/cancelHandler.js +23 -0
- package/dist/modules/actions/transport/catalogHandler.d.ts +18 -0
- package/dist/modules/actions/transport/catalogHandler.js +21 -0
- package/dist/modules/actions/transport/stream.d.ts +23 -0
- package/dist/modules/actions/transport/stream.js +100 -0
- package/dist/modules/actions/transport/verifySignature.d.ts +16 -0
- package/dist/modules/actions/transport/verifySignature.js +72 -0
- package/dist/modules/actions/transport/webhookHandler.d.ts +25 -0
- package/dist/modules/actions/transport/webhookHandler.js +206 -0
- package/dist/modules/actions/transport/webhookHandler.test.d.ts +1 -0
- package/dist/modules/actions/types.d.ts +234 -0
- package/dist/modules/connectors/engine.js +4 -1
- package/dist/modules/connectors/resolver.d.ts +10 -2
- package/dist/modules/connectors/resolver.js +8 -3
- package/dist/modules/mysql/accounts.d.ts +5 -0
- package/dist/modules/mysql/accounts.js +365 -0
- package/dist/modules/mysql/accounts.test.d.ts +1 -0
- package/dist/modules/mysql/admin.d.ts +58 -0
- package/dist/modules/mysql/admin.js +133 -0
- package/dist/modules/mysql/config.d.ts +82 -0
- package/dist/modules/mysql/config.js +30 -0
- package/dist/modules/mysql/identities.d.ts +35 -0
- package/dist/modules/mysql/identities.js +43 -0
- package/dist/modules/mysql/index.d.ts +67 -0
- package/dist/modules/mysql/index.js +62 -0
- package/dist/modules/mysql/pool.d.ts +3 -0
- package/dist/modules/mysql/pool.js +88 -0
- package/dist/modules/mysql/query.d.ts +12 -0
- package/dist/modules/mysql/query.js +21 -0
- package/dist/modules/mysql/schema.d.ts +43 -0
- package/dist/modules/mysql/schema.js +340 -0
- package/dist/modules/mysql/schema.test.d.ts +1 -0
- package/dist/modules/mysql/sessions.d.ts +33 -0
- package/dist/modules/mysql/sessions.js +46 -0
- package/dist/modules/mysql/spaceTokens.d.ts +11 -0
- package/dist/modules/mysql/spaceTokens.js +59 -0
- package/dist/modules/mysql/store.integration.test.d.ts +1 -0
- package/dist/modules/rsc/connectorRscData.d.ts +25 -0
- package/dist/modules/rsc/connectorRscData.js +51 -0
- package/dist/modules/rsc/connectorRscData.test.d.ts +1 -0
- package/dist/modules/rsc/handler.js +16 -3
- package/dist/modules/rsc/matchRscPage.d.ts +14 -0
- package/dist/modules/rsc/matchRscPage.js +26 -0
- package/dist/modules/rsc/resolveRscData.d.ts +14 -1
- package/dist/modules/rsc/resolveRscData.js +23 -34
- package/dist/modules/ssr/Component.d.ts +6 -2
- package/dist/modules/ssr/Component.js +3 -1
- package/dist/modules/ssr/draftSession.test.d.ts +1 -0
- package/dist/modules/ssr/handler.js +15 -2
- package/dist/modules/ssr/loadPluginComponents.js +22 -5
- package/dist/modules/ssr/prepareRender.js +87 -8
- package/dist/modules/ssr/prepareRender.test.d.ts +1 -0
- package/dist/modules/ssr/preview.d.ts +21 -8
- package/dist/modules/ssr/preview.js +38 -14
- package/dist/modules/ssr/registerExternalPlugins.js +2 -2
- package/dist/modules/ssr/resolvePageSeo.d.ts +18 -0
- package/dist/modules/ssr/resolvePageSeo.js +26 -0
- package/dist/modules/ssr/resolvePageSeo.test.d.ts +1 -0
- package/dist/modules/ssr/template.test.d.ts +1 -0
- package/dist/modules/ssr/views/template.ejs +71 -5
- package/dist/mysql.d.ts +26 -0
- package/dist/mysql.js +4 -0
- package/dist/oauth.d.ts +24 -0
- package/dist/oauth.js +10 -0
- package/dist/plugins/compile.d.ts +1 -0
- package/dist/plugins/compile.js +37 -3
- package/dist/plugins/manager.d.ts +40 -0
- package/dist/plugins/manager.js +113 -13
- package/dist/plugins/manager.test.d.ts +1 -0
- package/package.json +43 -14
- package/skills/plitzi-authoring/SKILL.md +259 -0
- /package/dist/{modules/actions/handler.test.d.ts → adapters/cloudAdapters.test.d.ts} +0 -0
- /package/dist/modules/actions/{handler.d.ts → connectorWrite.d.ts} +0 -0
package/dist/core/auth/routes.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { csrfFailureMessage } from "./csrf.js";
|
|
1
2
|
//#region src/core/auth/routes.ts
|
|
2
3
|
var body = (req) => req.body ?? {};
|
|
3
4
|
/** A body field as a string, whatever arrived. Handlers validate what they need; this only stops `[object Object]`. */
|
|
@@ -5,6 +6,32 @@ var field = (req, name) => {
|
|
|
5
6
|
const value = body(req)[name];
|
|
6
7
|
return typeof value === "string" ? value : "";
|
|
7
8
|
};
|
|
9
|
+
/** A body field as a whole number. `NaN` for anything else, which every handler refuses as a missing id. */
|
|
10
|
+
var numberField = (req, name) => {
|
|
11
|
+
const value = body(req)[name];
|
|
12
|
+
if (typeof value === "number") return value;
|
|
13
|
+
return typeof value === "string" ? Number.parseInt(value, 10) : NaN;
|
|
14
|
+
};
|
|
15
|
+
var queryText = (req, name) => {
|
|
16
|
+
const value = req.query?.[name];
|
|
17
|
+
return typeof value === "string" && value ? value : void 0;
|
|
18
|
+
};
|
|
19
|
+
var queryNumber = (req, name) => {
|
|
20
|
+
const parsed = Number.parseInt(queryText(req, name) ?? "", 10);
|
|
21
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Whatever arrived as `roles`, unexamined.
|
|
25
|
+
*
|
|
26
|
+
* Deliberately NOT coerced to a list here. It was, and a client sending `roles: "editor"` — the obvious mistake —
|
|
27
|
+
* got an empty list, which `setRoles` faithfully applied: every role removed, and a 200 saying it worked. The
|
|
28
|
+
* handler validates instead, so the wrong shape is a 400.
|
|
29
|
+
*/
|
|
30
|
+
var rolesOf = (req) => body(req).roles;
|
|
31
|
+
var SUBJECT_OF = {
|
|
32
|
+
signIn: "signIn",
|
|
33
|
+
grant: "delegated"
|
|
34
|
+
};
|
|
8
35
|
/**
|
|
9
36
|
* The whole `/auth` surface as data: which method and path each flow answers on, what a caller must present, and what
|
|
10
37
|
* to call.
|
|
@@ -24,6 +51,17 @@ var FLOWS = [
|
|
|
24
51
|
requirement: "public",
|
|
25
52
|
run: (api) => api.describe()
|
|
26
53
|
},
|
|
54
|
+
(
|
|
55
|
+
/**
|
|
56
|
+
* A token to write with, for a page that has none. Public because signing IN needs one: without a token before
|
|
57
|
+
* anybody is authenticated, another site can log a visitor into an account it controls.
|
|
58
|
+
*/
|
|
59
|
+
{
|
|
60
|
+
method: "GET",
|
|
61
|
+
path: "/csrf",
|
|
62
|
+
requirement: "public",
|
|
63
|
+
run: (api, cookies, req) => api.issueCsrf(cookies.resolveSessionToken(req))
|
|
64
|
+
}),
|
|
27
65
|
{
|
|
28
66
|
method: "GET",
|
|
29
67
|
path: "/session",
|
|
@@ -34,18 +72,21 @@ var FLOWS = [
|
|
|
34
72
|
method: "POST",
|
|
35
73
|
path: "/login",
|
|
36
74
|
requirement: "public",
|
|
37
|
-
|
|
75
|
+
csrf: "signIn",
|
|
76
|
+
run: (api, _cookies, req) => api.login(body(req), req)
|
|
38
77
|
},
|
|
39
78
|
{
|
|
40
79
|
method: "POST",
|
|
41
80
|
path: "/refresh",
|
|
42
81
|
requirement: "public",
|
|
43
|
-
|
|
82
|
+
csrf: "none",
|
|
83
|
+
run: (api, cookies, req) => api.refresh(cookies.resolveRefreshToken(req, req.body), req)
|
|
44
84
|
},
|
|
45
85
|
{
|
|
46
86
|
method: "POST",
|
|
47
87
|
path: "/logout",
|
|
48
88
|
requirement: "public",
|
|
89
|
+
csrf: "none",
|
|
49
90
|
run: (api, cookies, req) => api.logout({
|
|
50
91
|
accessToken: cookies.resolveSessionToken(req),
|
|
51
92
|
refreshToken: cookies.resolveRefreshToken(req, req.body)
|
|
@@ -61,44 +102,207 @@ var FLOWS = [
|
|
|
61
102
|
method: "POST",
|
|
62
103
|
path: "/exchange",
|
|
63
104
|
requirement: "grant",
|
|
105
|
+
csrf: "grant",
|
|
64
106
|
run: (api, _cookies, req) => api.exchange(field(req, "provider"), field(req, "token"), req)
|
|
65
107
|
},
|
|
66
108
|
{
|
|
67
109
|
method: "POST",
|
|
68
110
|
path: "/signup",
|
|
69
111
|
requirement: "public",
|
|
112
|
+
csrf: "signIn",
|
|
70
113
|
run: (api, _cookies, req) => api.signup(body(req))
|
|
71
114
|
},
|
|
72
115
|
{
|
|
73
116
|
method: "POST",
|
|
74
117
|
path: "/forgot-password",
|
|
75
118
|
requirement: "public",
|
|
119
|
+
csrf: "signIn",
|
|
76
120
|
run: (api, _cookies, req) => api.forgotPassword(field(req, "email"))
|
|
77
121
|
},
|
|
78
122
|
{
|
|
79
123
|
method: "POST",
|
|
80
124
|
path: "/reset-password",
|
|
81
125
|
requirement: "public",
|
|
126
|
+
csrf: "signIn",
|
|
82
127
|
run: (api, _cookies, req) => api.resetPassword(field(req, "token"), field(req, "password"))
|
|
83
128
|
},
|
|
84
129
|
{
|
|
85
130
|
method: "POST",
|
|
86
131
|
path: "/validate-account",
|
|
87
132
|
requirement: "public",
|
|
133
|
+
csrf: "signIn",
|
|
88
134
|
run: (api, _cookies, req) => api.validateAccount(field(req, "token"))
|
|
89
135
|
},
|
|
136
|
+
{
|
|
137
|
+
/**
|
|
138
|
+
* Confirms an address change the account asked for. Public and `signIn`-guarded because the link lands in a
|
|
139
|
+
* mail client, and the browser that opens it may be one that has never seen this deployment.
|
|
140
|
+
*/
|
|
141
|
+
method: "POST",
|
|
142
|
+
path: "/confirm-email",
|
|
143
|
+
requirement: "public",
|
|
144
|
+
csrf: "signIn",
|
|
145
|
+
run: (api, _cookies, req) => api.confirmEmailChange(field(req, "token"))
|
|
146
|
+
},
|
|
90
147
|
{
|
|
91
148
|
method: "POST",
|
|
92
149
|
path: "/resend-verification-email",
|
|
93
150
|
requirement: "public",
|
|
94
|
-
|
|
151
|
+
csrf: "signIn",
|
|
152
|
+
run: (api, _cookies, req) => api.resendVerification(field(req, "email"), field(req, "redirect"))
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
method: "POST",
|
|
156
|
+
path: "/profile",
|
|
157
|
+
requirement: "actor",
|
|
158
|
+
run: (api, _cookies, req) => api.updateProfile(req.actor, body(req))
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
method: "POST",
|
|
162
|
+
path: "/password",
|
|
163
|
+
requirement: "actor",
|
|
164
|
+
run: (api, _cookies, req) => api.changePassword(req.actor, field(req, "currentPassword"), field(req, "password"))
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
method: "POST",
|
|
168
|
+
path: "/delete-account",
|
|
169
|
+
requirement: "actor",
|
|
170
|
+
run: (api, _cookies, req) => api.deleteSelf(req.actor, field(req, "password"))
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
method: "GET",
|
|
174
|
+
path: "/sessions",
|
|
175
|
+
requirement: "actor",
|
|
176
|
+
run: (api, _cookies, req) => api.listSessions(req.actor)
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
method: "POST",
|
|
180
|
+
path: "/sessions/revoke-one",
|
|
181
|
+
requirement: "actor",
|
|
182
|
+
run: (api, _cookies, req) => api.revokeSession(req.actor, numberField(req, "sessionId"))
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
method: "POST",
|
|
186
|
+
path: "/sessions/revoke-others",
|
|
187
|
+
requirement: "actor",
|
|
188
|
+
run: (api, _cookies, req) => api.revokeOtherSessions(req.actor)
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
method: "POST",
|
|
192
|
+
path: "/passwordless/request",
|
|
193
|
+
requirement: "public",
|
|
194
|
+
csrf: "signIn",
|
|
195
|
+
run: (api, _cookies, req) => api.passwordless.request(field(req, "email"), req)
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
method: "POST",
|
|
199
|
+
path: "/passwordless/complete",
|
|
200
|
+
requirement: "public",
|
|
201
|
+
csrf: "signIn",
|
|
202
|
+
run: (api, _cookies, req) => api.passwordless.complete(field(req, "email"), field(req, "code"), req)
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
method: "POST",
|
|
206
|
+
path: "/mfa/complete",
|
|
207
|
+
requirement: "public",
|
|
208
|
+
csrf: "signIn",
|
|
209
|
+
run: (api, _cookies, req) => api.completeMfa(field(req, "mfaToken"), field(req, "code"), req)
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
method: "GET",
|
|
213
|
+
path: "/mfa",
|
|
214
|
+
requirement: "actor",
|
|
215
|
+
run: (api, _cookies, req) => api.mfa.status(req.actor)
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
method: "POST",
|
|
219
|
+
path: "/mfa/begin",
|
|
220
|
+
requirement: "actor",
|
|
221
|
+
run: (api, _cookies, req) => api.mfa.begin(req.actor)
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
method: "POST",
|
|
225
|
+
path: "/mfa/confirm",
|
|
226
|
+
requirement: "actor",
|
|
227
|
+
run: (api, _cookies, req) => api.mfa.confirm(req.actor, field(req, "code"))
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
method: "POST",
|
|
231
|
+
path: "/mfa/disable",
|
|
232
|
+
requirement: "actor",
|
|
233
|
+
run: (api, _cookies, req) => api.mfa.disable(req.actor, field(req, "password"))
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
method: "GET",
|
|
237
|
+
path: "/admin/accounts",
|
|
238
|
+
requirement: "actor",
|
|
239
|
+
run: (api, _cookies, req) => api.admin.list(req.actor, {
|
|
240
|
+
search: queryText(req, "search"),
|
|
241
|
+
status: queryText(req, "status"),
|
|
242
|
+
limit: queryNumber(req, "limit"),
|
|
243
|
+
offset: queryNumber(req, "offset")
|
|
244
|
+
})
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
method: "GET",
|
|
248
|
+
path: "/admin/account",
|
|
249
|
+
requirement: "actor",
|
|
250
|
+
run: (api, _cookies, req) => api.admin.get(req.actor, queryNumber(req, "id") ?? NaN)
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
method: "POST",
|
|
254
|
+
path: "/admin/account/status",
|
|
255
|
+
requirement: "actor",
|
|
256
|
+
run: (api, _cookies, req) => api.admin.setStatus(req.actor, numberField(req, "userId"), field(req, "status"))
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
method: "POST",
|
|
260
|
+
path: "/admin/account/roles",
|
|
261
|
+
requirement: "actor",
|
|
262
|
+
run: (api, _cookies, req) => api.admin.setRoles(req.actor, numberField(req, "userId"), rolesOf(req))
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
method: "POST",
|
|
266
|
+
path: "/admin/account/delete",
|
|
267
|
+
requirement: "actor",
|
|
268
|
+
run: (api, _cookies, req) => api.admin.remove(req.actor, numberField(req, "userId"))
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
method: "POST",
|
|
272
|
+
path: "/admin/impersonate",
|
|
273
|
+
requirement: "actor",
|
|
274
|
+
run: (api, _cookies, req) => api.admin.impersonate(req.actor, numberField(req, "userId"), req)
|
|
95
275
|
}
|
|
96
276
|
];
|
|
277
|
+
/** Whether this request has to present a token, which is `csrf`'s judgement — given what the flow does. */
|
|
278
|
+
var csrfNeeded = (csrf, policy, carrier) => policy === "none" ? false : csrf.required(carrier, policy ? SUBJECT_OF[policy] : "write");
|
|
97
279
|
/** The flows, ready to mount. Which of them actually answer is decided by the API itself — no adapter, no endpoint —
|
|
98
280
|
* so mounting all of them is correct even for a deployment that offers three. */
|
|
99
|
-
var authRoutes = ({ api, cookies }) => FLOWS.map(({ run, ...route }) => ({
|
|
281
|
+
var authRoutes = ({ api, cookies, csrf }) => FLOWS.map(({ run, csrf: policy, ...route }) => ({
|
|
100
282
|
...route,
|
|
101
|
-
handler: (req) =>
|
|
283
|
+
handler: async (req) => {
|
|
284
|
+
/**
|
|
285
|
+
* Checked before the flow runs. The session the token must be bound to is whatever this request carries — an
|
|
286
|
+
* unbound token satisfies a signed-out caller, which is how signing in is protected when a deployment asks
|
|
287
|
+
* for that.
|
|
288
|
+
*/
|
|
289
|
+
const carrier = {
|
|
290
|
+
...req,
|
|
291
|
+
method: route.method
|
|
292
|
+
};
|
|
293
|
+
if (csrf && csrfNeeded(csrf, policy, carrier)) {
|
|
294
|
+
const result = csrf.verify(carrier, cookies.resolveSessionToken(req));
|
|
295
|
+
if (!result.ok) return {
|
|
296
|
+
ok: false,
|
|
297
|
+
status: 403,
|
|
298
|
+
body: {
|
|
299
|
+
error: csrfFailureMessage[result.reason],
|
|
300
|
+
reason: result.reason
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
return run(api, cookies, req);
|
|
305
|
+
}
|
|
102
306
|
}));
|
|
103
307
|
/**
|
|
104
308
|
* The same flows as authorization rules, for the guard that runs in front of them.
|
|
@@ -120,10 +324,31 @@ var authPolicyRules = (prefix = "/auth") => {
|
|
|
120
324
|
* to do that, and because getting this half wrong — writing the session but not its readable hint, or clearing one of
|
|
121
325
|
* the three on sign-out — is the failure that looks like a session that exists and then does not.
|
|
122
326
|
*/
|
|
123
|
-
var applySessionOutcome = (req, res, outcome, cookies) => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
327
|
+
var applySessionOutcome = (req, res, outcome, cookies, csrf) => {
|
|
328
|
+
/**
|
|
329
|
+
* A refusal can still have something to say about the cookies — see `endSession` on the failing outcome. It is
|
|
330
|
+
* the only thing it may say: nothing is granted from a flow that failed.
|
|
331
|
+
*/
|
|
332
|
+
if (!outcome.ok) {
|
|
333
|
+
if (outcome.endSession) cookies.clear(req, res);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (outcome.session) {
|
|
337
|
+
cookies.write(req, res, outcome.session);
|
|
338
|
+
/**
|
|
339
|
+
* A fresh CSRF token, bound to the session that was just granted.
|
|
340
|
+
*
|
|
341
|
+
* Not optional housekeeping: the token a signed-out page was holding is bound to nobody, and every write it
|
|
342
|
+
* attempts after signing in would be refused as a mismatch. Re-issuing here is what makes signing in and then
|
|
343
|
+
* doing something work without the client knowing this exists.
|
|
344
|
+
*/
|
|
345
|
+
csrf?.write(req, res, csrf.issue(outcome.session.token));
|
|
346
|
+
}
|
|
347
|
+
if (outcome.endSession) {
|
|
348
|
+
cookies.clear(req, res);
|
|
349
|
+
csrf?.clear(req, res);
|
|
350
|
+
}
|
|
351
|
+
if (outcome.csrf !== void 0) csrf?.write(req, res, outcome.csrf);
|
|
127
352
|
};
|
|
128
353
|
//#endregion
|
|
129
354
|
export { applySessionOutcome, authPolicyRules, authRoutes };
|
|
@@ -22,6 +22,12 @@ export type SessionCookieParams = {
|
|
|
22
22
|
* refuse one on a single-label host) and lax SameSite, because Secure + SameSite=None over plain http is dropped.
|
|
23
23
|
*/
|
|
24
24
|
export declare const isLocalHost: (hostname: string) => boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The last two labels of the host, so a session established on one sub-domain is carried by its siblings — an app
|
|
27
|
+
* and the API it talks to are rarely the same host. Local and IP hosts get no domain at all: browsers refuse a
|
|
28
|
+
* Domain on a single-label host, and one written for an address would never be sent back to it.
|
|
29
|
+
*/
|
|
30
|
+
export declare const registrableDomain: (hostname: string) => string | undefined;
|
|
25
31
|
/**
|
|
26
32
|
* Everything about how a session cookie is written, derived from the request host unless the deployment says
|
|
27
33
|
* otherwise. The defaults are the ones a server on a real domain needs: a cookie shared across sub-domains, and
|
|
@@ -38,6 +44,11 @@ export declare const sessionCookieParams: (hostname: string, config?: SSRAuthCoo
|
|
|
38
44
|
* same question with `credentials: 'include'`.
|
|
39
45
|
*/
|
|
40
46
|
export declare const sessionHintValue: (expiresAt: number, refreshExpiresAt?: number) => string;
|
|
47
|
+
/**
|
|
48
|
+
* Adds to `Set-Cookie` rather than replacing it. A handler may already have written one — clearing an OAuth flow
|
|
49
|
+
* before granting the session it produced is exactly that — and replacing the header would silently drop it.
|
|
50
|
+
*/
|
|
51
|
+
export declare const appendCookies: (res: CookieSink, cookies: string[]) => void;
|
|
41
52
|
/**
|
|
42
53
|
* Writes a granted session onto the response: the credential itself, the refresh half confined to its own path, and
|
|
43
54
|
* the readable hint. One call, because the three only make sense together — a hint that outlives its session sends
|
|
@@ -162,4 +162,4 @@ var createSessionCookies = (config) => ({
|
|
|
162
162
|
}
|
|
163
163
|
});
|
|
164
164
|
//#endregion
|
|
165
|
-
export { clearFlowCookie, clearSessionCookies, createSessionCookies, isLocalHost, readFlowCookie, readRefreshToken, readSessionToken, sessionCookieParams, sessionHintValue, writeFlowCookie, writeSessionCookies };
|
|
165
|
+
export { appendCookies, clearFlowCookie, clearSessionCookies, createSessionCookies, isLocalHost, readFlowCookie, readRefreshToken, readSessionToken, registrableDomain, sessionCookieParams, sessionHintValue, writeFlowCookie, writeSessionCookies };
|
|
@@ -25,6 +25,18 @@ export interface SpaceTokenAdapters {
|
|
|
25
25
|
origins?: string[];
|
|
26
26
|
expiresAt?: number | null;
|
|
27
27
|
}) => Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Store a NEW credential beside the space's public one. Only the secret scopes are ever created this way — the
|
|
30
|
+
* public row is created with the space and rotated in place thereafter — so a deployment that issues none may
|
|
31
|
+
* leave it out and `issueHost` refuses.
|
|
32
|
+
*/
|
|
33
|
+
create?: (values: {
|
|
34
|
+
spaceId: number;
|
|
35
|
+
token: string;
|
|
36
|
+
scope: SpaceScope;
|
|
37
|
+
origins: string[];
|
|
38
|
+
expiresAt: number | null;
|
|
39
|
+
}) => Promise<SpaceTokenRecord>;
|
|
28
40
|
remove: (id: number) => Promise<void>;
|
|
29
41
|
/**
|
|
30
42
|
* The domain list is two policies at once — where the credential may be presented, and who may frame the space — so
|
|
@@ -91,6 +103,29 @@ export declare const createSpaceTokenApi: ({ tokens, adapters }: {
|
|
|
91
103
|
domains: string[];
|
|
92
104
|
warning: string;
|
|
93
105
|
}>>;
|
|
106
|
+
/**
|
|
107
|
+
* Issue a **host** credential: the space's read access for a SERVER that renders it as its own.
|
|
108
|
+
*
|
|
109
|
+
* A separate credential rather than letting the public one do it, because the two are protected by different
|
|
110
|
+
* things and only one of them can be published. A `render` token is safe in a page precisely because it is held
|
|
111
|
+
* to the origins it declares — a check that only means anything when a browser is the one making the claim. A
|
|
112
|
+
* server has no origin to state, so admitting the public token without one would make a key lifted from anybody's
|
|
113
|
+
* published page enough to serve a byte-identical clone of their site. This one is admitted without an origin
|
|
114
|
+
* because possessing it is itself the proof, which is only true while it stays secret: it is returned once, never
|
|
115
|
+
* embedded, and revocable on its own row.
|
|
116
|
+
*
|
|
117
|
+
* `label` is what the owner will read in the list when deciding which one to revoke — the deployment it was
|
|
118
|
+
* issued to. It is stored where a render token keeps its domains, since a host credential is bound to none.
|
|
119
|
+
*/
|
|
120
|
+
issueHost(context: SpaceTokenContext, label: unknown, { expiresAt }?: {
|
|
121
|
+
expiresAt?: number | null;
|
|
122
|
+
}): Promise<SpaceTokenOutcome<{
|
|
123
|
+
id: number;
|
|
124
|
+
token: string;
|
|
125
|
+
label: string;
|
|
126
|
+
expiresAt: number | null;
|
|
127
|
+
warning: string;
|
|
128
|
+
}>>;
|
|
94
129
|
readDomains(context: SpaceTokenContext): Promise<SpaceTokenOutcome<{
|
|
95
130
|
domains: string[];
|
|
96
131
|
}>>;
|
|
@@ -116,8 +151,9 @@ export declare const createSpaceTokenApi: ({ tokens, adapters }: {
|
|
|
116
151
|
}>>;
|
|
117
152
|
/**
|
|
118
153
|
* The space's credentials, so a leak can be seen and acted on. Never the secrets themselves: an `agent` grant
|
|
119
|
-
* writes
|
|
120
|
-
* construction and has its own
|
|
154
|
+
* writes and a `host` grant serves the space from somebody else's server, so listing either would turn
|
|
155
|
+
* permission to read into a way to obtain it. The public one is published by construction and has its own
|
|
156
|
+
* endpoint.
|
|
121
157
|
*/
|
|
122
158
|
list(context: SpaceTokenContext): Promise<SpaceTokenOutcome<{
|
|
123
159
|
tokens: SpaceTokenSummary[];
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { normalizeDomain } from "./domains.js";
|
|
2
2
|
//#region src/core/auth/spaceTokens.ts
|
|
3
3
|
var ROTATION_WARNING = "The previous token stopped working. Every site embedding it must be redeployed.";
|
|
4
|
+
/**
|
|
5
|
+
* Said once, at the only moment it can be acted on. A host credential is the space's read access WITHOUT the origin
|
|
6
|
+
* check that makes the public one safe to publish, so what keeps it from being a way to clone the space is that it
|
|
7
|
+
* stays secret — and a caller who does not know it is never shown again will paste it into a page.
|
|
8
|
+
*/
|
|
9
|
+
var HOST_SECRET_WARNING = "Store this now — it is not shown again. Unlike the public token it is NOT safe to embed in a page or commit to a public repository: anyone holding it can serve this space from their own server. Revoke it if it leaks.";
|
|
4
10
|
var DAY_SECONDS = 86400;
|
|
5
11
|
var now = () => Math.floor(Date.now() / 1e3);
|
|
6
12
|
var notFound = {
|
|
@@ -81,6 +87,51 @@ var createSpaceTokenApi = ({ tokens, adapters }) => {
|
|
|
81
87
|
}
|
|
82
88
|
};
|
|
83
89
|
},
|
|
90
|
+
/**
|
|
91
|
+
* Issue a **host** credential: the space's read access for a SERVER that renders it as its own.
|
|
92
|
+
*
|
|
93
|
+
* A separate credential rather than letting the public one do it, because the two are protected by different
|
|
94
|
+
* things and only one of them can be published. A `render` token is safe in a page precisely because it is held
|
|
95
|
+
* to the origins it declares — a check that only means anything when a browser is the one making the claim. A
|
|
96
|
+
* server has no origin to state, so admitting the public token without one would make a key lifted from anybody's
|
|
97
|
+
* published page enough to serve a byte-identical clone of their site. This one is admitted without an origin
|
|
98
|
+
* because possessing it is itself the proof, which is only true while it stays secret: it is returned once, never
|
|
99
|
+
* embedded, and revocable on its own row.
|
|
100
|
+
*
|
|
101
|
+
* `label` is what the owner will read in the list when deciding which one to revoke — the deployment it was
|
|
102
|
+
* issued to. It is stored where a render token keeps its domains, since a host credential is bound to none.
|
|
103
|
+
*/
|
|
104
|
+
async issueHost(context, label, { expiresAt = null } = {}) {
|
|
105
|
+
if (!adapters.create) return {
|
|
106
|
+
ok: false,
|
|
107
|
+
status: 501,
|
|
108
|
+
body: { error: "This deployment does not issue host tokens" }
|
|
109
|
+
};
|
|
110
|
+
if (typeof label !== "string" || label.trim() === "") return {
|
|
111
|
+
ok: false,
|
|
112
|
+
status: 400,
|
|
113
|
+
body: { error: "label is required — name the deployment this credential is for, so it can be revoked" }
|
|
114
|
+
};
|
|
115
|
+
const name = label.trim();
|
|
116
|
+
const token = tokens.generateSpaceToken(context.spaceId, [name], "host", { expiresAt });
|
|
117
|
+
return {
|
|
118
|
+
ok: true,
|
|
119
|
+
status: 201,
|
|
120
|
+
body: {
|
|
121
|
+
id: (await adapters.create({
|
|
122
|
+
spaceId: context.spaceId,
|
|
123
|
+
token,
|
|
124
|
+
scope: "host",
|
|
125
|
+
origins: [name],
|
|
126
|
+
expiresAt
|
|
127
|
+
})).id,
|
|
128
|
+
token,
|
|
129
|
+
label: name,
|
|
130
|
+
expiresAt,
|
|
131
|
+
warning: HOST_SECRET_WARNING
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
},
|
|
84
135
|
async readDomains(context) {
|
|
85
136
|
const record = await adapters.loadDefault(context.spaceId);
|
|
86
137
|
return record ? {
|
|
@@ -160,8 +211,9 @@ var createSpaceTokenApi = ({ tokens, adapters }) => {
|
|
|
160
211
|
},
|
|
161
212
|
/**
|
|
162
213
|
* The space's credentials, so a leak can be seen and acted on. Never the secrets themselves: an `agent` grant
|
|
163
|
-
* writes
|
|
164
|
-
* construction and has its own
|
|
214
|
+
* writes and a `host` grant serves the space from somebody else's server, so listing either would turn
|
|
215
|
+
* permission to read into a way to obtain it. The public one is published by construction and has its own
|
|
216
|
+
* endpoint.
|
|
165
217
|
*/
|
|
166
218
|
async list(context) {
|
|
167
219
|
return {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//#region src/core/auth/throttle.ts
|
|
2
|
+
/** Attempts allowed per window, per key, per action. */
|
|
3
|
+
var LIMITS = {
|
|
4
|
+
/** Credential stuffing is the thing being priced out. Ten wrong passwords in five minutes is already a lot. */
|
|
5
|
+
login: {
|
|
6
|
+
attempts: 10,
|
|
7
|
+
windowSeconds: 300
|
|
8
|
+
},
|
|
9
|
+
signup: {
|
|
10
|
+
attempts: 5,
|
|
11
|
+
windowSeconds: 3600
|
|
12
|
+
},
|
|
13
|
+
/** Mailing flows are throttled by address, or the endpoint is a way to send somebody a hundred emails. */
|
|
14
|
+
forgotPassword: {
|
|
15
|
+
attempts: 5,
|
|
16
|
+
windowSeconds: 3600
|
|
17
|
+
},
|
|
18
|
+
/** By token: without this a reset token is guessable by trying, however opaque it is. */
|
|
19
|
+
resetPassword: {
|
|
20
|
+
attempts: 10,
|
|
21
|
+
windowSeconds: 3600
|
|
22
|
+
},
|
|
23
|
+
changePassword: {
|
|
24
|
+
attempts: 10,
|
|
25
|
+
windowSeconds: 300
|
|
26
|
+
},
|
|
27
|
+
exchange: {
|
|
28
|
+
attempts: 30,
|
|
29
|
+
windowSeconds: 300
|
|
30
|
+
},
|
|
31
|
+
/** Six digits is a million guesses, so the limit is what makes a second factor worth anything. */
|
|
32
|
+
mfa: {
|
|
33
|
+
attempts: 5,
|
|
34
|
+
windowSeconds: 300
|
|
35
|
+
},
|
|
36
|
+
passwordless: {
|
|
37
|
+
attempts: 5,
|
|
38
|
+
windowSeconds: 900
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
/** Stop the map growing without bound on a server nobody is attacking politely. */
|
|
42
|
+
var SWEEP_EVERY = 1e3;
|
|
43
|
+
var createMemoryRateLimit = () => {
|
|
44
|
+
const hits = /* @__PURE__ */ new Map();
|
|
45
|
+
let sinceSweep = 0;
|
|
46
|
+
const sweep = (now) => {
|
|
47
|
+
for (const [key, times] of hits) {
|
|
48
|
+
const action = key.slice(0, key.indexOf(":"));
|
|
49
|
+
const live = times.filter((at) => at > now - LIMITS[action].windowSeconds);
|
|
50
|
+
if (live.length === 0) hits.delete(key);
|
|
51
|
+
else hits.set(key, live);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
return (attempt) => {
|
|
55
|
+
const { attempts, windowSeconds } = LIMITS[attempt.action];
|
|
56
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
57
|
+
const key = `${attempt.action}:${attempt.key}`;
|
|
58
|
+
/**
|
|
59
|
+
* It worked, so nothing is being guarded against any more. Without this the counter cannot tell ten failures
|
|
60
|
+
* from ten sign-ins — the check runs before the password is examined — and an app that signs the same account
|
|
61
|
+
* in repeatedly would lock it out by succeeding.
|
|
62
|
+
*/
|
|
63
|
+
if (attempt.succeeded) {
|
|
64
|
+
hits.delete(key);
|
|
65
|
+
return Promise.resolve({ allowed: true });
|
|
66
|
+
}
|
|
67
|
+
if (++sinceSweep >= SWEEP_EVERY) {
|
|
68
|
+
sinceSweep = 0;
|
|
69
|
+
sweep(now);
|
|
70
|
+
}
|
|
71
|
+
const recent = (hits.get(key) ?? []).filter((at) => at > now - windowSeconds);
|
|
72
|
+
if (recent.length >= attempts) return Promise.resolve({
|
|
73
|
+
allowed: false,
|
|
74
|
+
retryAfter: Math.max(1, recent[0] + windowSeconds - now)
|
|
75
|
+
});
|
|
76
|
+
hits.set(key, [...recent, now]);
|
|
77
|
+
return Promise.resolve({ allowed: true });
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
//#endregion
|
|
81
|
+
export { createMemoryRateLimit };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|