@boopkit/cap-oauth-server 0.1.4 → 0.3.0
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/capability.json +14 -2
- package/dist/authorization-code-service.d.ts +40 -11
- package/dist/authorization-code-service.d.ts.map +1 -1
- package/dist/authorization-code-service.js +93 -41
- package/dist/authorization-code-service.js.map +1 -1
- package/dist/bearer.d.ts +27 -4
- package/dist/bearer.d.ts.map +1 -1
- package/dist/bearer.js +4 -4
- package/dist/bearer.js.map +1 -1
- package/dist/client-prune.d.ts +102 -0
- package/dist/client-prune.d.ts.map +1 -0
- package/dist/client-prune.js +51 -0
- package/dist/client-prune.js.map +1 -0
- package/dist/client-registry-service.d.ts +78 -7
- package/dist/client-registry-service.d.ts.map +1 -1
- package/dist/client-registry-service.js +200 -52
- package/dist/client-registry-service.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/connections-controller.js +1 -1
- package/dist/connections-controller.js.map +1 -1
- package/dist/consent-controller.d.ts +80 -6
- package/dist/consent-controller.d.ts.map +1 -1
- package/dist/consent-controller.js +134 -18
- package/dist/consent-controller.js.map +1 -1
- package/dist/discovery-controller.d.ts +2 -1
- package/dist/discovery-controller.d.ts.map +1 -1
- package/dist/discovery-controller.js +14 -4
- package/dist/discovery-controller.js.map +1 -1
- package/dist/discovery.d.ts +26 -0
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +55 -1
- package/dist/discovery.js.map +1 -1
- package/dist/errors.d.ts +38 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +59 -0
- package/dist/errors.js.map +1 -1
- package/dist/grant-service.d.ts +11 -4
- package/dist/grant-service.d.ts.map +1 -1
- package/dist/grant-service.js +6 -3
- package/dist/grant-service.js.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -3
- package/dist/index.js.map +1 -1
- package/dist/metadata-document.d.ts +22 -0
- package/dist/metadata-document.d.ts.map +1 -1
- package/dist/metadata-document.js +44 -2
- package/dist/metadata-document.js.map +1 -1
- package/dist/outbound-target.d.ts +48 -0
- package/dist/outbound-target.d.ts.map +1 -0
- package/dist/outbound-target.js +211 -0
- package/dist/outbound-target.js.map +1 -0
- package/dist/rate-limit.d.ts +53 -11
- package/dist/rate-limit.d.ts.map +1 -1
- package/dist/rate-limit.js +27 -4
- package/dist/rate-limit.js.map +1 -1
- package/dist/redirect-uri.d.ts +17 -9
- package/dist/redirect-uri.d.ts.map +1 -1
- package/dist/redirect-uri.js +26 -10
- package/dist/redirect-uri.js.map +1 -1
- package/dist/registration-controller.d.ts +10 -0
- package/dist/registration-controller.d.ts.map +1 -1
- package/dist/registration-controller.js +10 -0
- package/dist/registration-controller.js.map +1 -1
- package/dist/same-site.d.ts +54 -0
- package/dist/same-site.d.ts.map +1 -0
- package/dist/same-site.js +124 -0
- package/dist/same-site.js.map +1 -0
- package/dist/token-controller.d.ts +21 -0
- package/dist/token-controller.d.ts.map +1 -1
- package/dist/token-controller.js +42 -18
- package/dist/token-controller.js.map +1 -1
- package/package.json +2 -2
- package/scaffold/reference/oauth-server.ref.md +276 -10
- package/scaffold/src/middleware/oauth-server.ts +33 -17
- package/scaffold/src/scripts/prune-oauth-clients.ts +86 -0
- package/scaffold/src/services/oauth-server.service.ts +24 -0
- package/scaffold/views/oauth-consent-error.hbs +10 -3
package/dist/index.js
CHANGED
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
* that mechanically.
|
|
20
20
|
*/
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.
|
|
23
|
-
exports.
|
|
22
|
+
exports.DEFAULT_REGISTRATION_RATE_LIMIT = exports.DEFAULT_METADATA_RESOLUTION_RATE_LIMIT = exports.rateLimitAddress = exports.createRegistrationRateLimiter = exports.createFixedWindowRateLimiter = exports.toRegistrationResponse = exports.createRegistrationRouter = exports.createRegistrationHandlers = exports.createDiscoveryRouter = exports.createDiscoveryHandlers = exports.REFRESH_TOKEN_TTL_BOUNDS = exports.ACCESS_TOKEN_TTL_BOUNDS = exports.DEFAULT_REFRESH_TOKEN_TTL_SECONDS = exports.DEFAULT_ACCESS_TOKEN_TTL_SECONDS = exports.OauthServerConfigError = exports.clampTtlSeconds = exports.readServerConfig = exports.protectedResourceMetadataPaths = exports.PROTECTED_RESOURCE_METADATA_PATH = exports.DISCOVERY_MAX_AGE_SECONDS = exports.DEFAULT_SCOPES_SUPPORTED = exports.DEVICE_CODE_GRANT_TYPE = exports.CODE_CHALLENGE_METHODS_SUPPORTED = exports.buildProtectedResourceMetadata = exports.buildAuthorizationServerMetadata = exports.DEFAULT_METADATA_MAX_REDIRECTS = exports.DEFAULT_METADATA_MAX_BYTES = exports.DEFAULT_METADATA_TIMEOUT_MS = exports.isValidMetadataUri = exports.fetchMetadataDocument = exports.isBlockedAddress = exports.blockedHostReason = exports.DEFAULT_MAX_REGISTERED_CLIENTS = exports.DEFAULT_ABANDONED_CLIENT_TTL_MS = exports.DEFAULT_METADATA_MAX_AGE_MS = exports.DEFAULT_RESPONSE_TYPES = exports.DEFAULT_GRANT_TYPES = exports.STRANGER_CREATED_MODES = exports.validateRedirectUris = exports.createClientRegistryService = exports.isClientRegistryError = exports.ClientRegistryError = exports.CLIENT_PUBLIC_ID_LENGTH = exports.CLIENT_PUBLIC_ID_ALPHABET = exports.isValidClientPublicId = exports.generateClientPublicId = exports.isLoopbackHost = exports.matchesAnyRedirectUri = exports.matchRedirectUri = exports.classifyRedirectUri = void 0;
|
|
23
|
+
exports.REVOKED_BY_CLIENT = exports.createRevocationService = exports.createClientPruneJob = exports.createClientInUsePredicate = exports.DEFAULT_BEARER_CHALLENGE = exports.requireOAuth = exports.missingScopes = exports.createOAuthTokenResolver = exports.createOAuthResourceServer = exports.createOAuthBearerMiddleware = exports.OFFLINE_ACCESS_SCOPE = exports.parseScopes = exports.createTokenRouter = exports.createTokenHandlers = exports.isSameSiteRequest = exports.SCOPE_DESCRIPTIONS_LOCAL = exports.warnUndescribedScopes = exports.toScopeViews = exports.scopeDescriptionsFrom = exports.safeHttpsUri = exports.provenanceFor = exports.monogramFor = exports.defaultIsSignedIn = exports.createConsentRouter = exports.createConsentHandlers = exports.writeArray = exports.readArray = exports.isStaleWriteError = exports.isOAuthError = exports.OAuthError = exports.verifyCodeChallenge = exports.isValidCodeVerifier = exports.isValidCodeChallenge = exports.deriveCodeChallenge = exports.CODE_CHALLENGE_METHOD = exports.OAUTH_CODES_INFO_SCHEMA = exports.OAUTH_CODES_PROPERTY = exports.createAuthorizationCodeService = exports.OAUTH_GRANTS_INFO_SCHEMA = exports.OAUTH_GRANTS_PROPERTY = exports.isGrantActive = exports.createGrantService = exports.OAUTH_REFRESH_DOMAIN = exports.OAUTH_ACCESS_DOMAIN = exports.OAUTH_CODE_DOMAIN = exports.AUTHORIZATION_CODE_TTL_SECONDS = exports.buildIdentityKey = exports.createTokenService = exports.CLIENT_MODES = exports.DEFAULT_REGISTRATION_RATE_WINDOW_MS = void 0;
|
|
24
|
+
exports.ACCOUNT_COVERAGE_STATEMENT = exports.createConnectionsRouter = exports.createConnectionsHandlers = exports.createRevocationRouter = exports.createRevocationHandlers = exports.REVOKED_CLIENT_WIDE = exports.REVOKED_BY_USER = void 0;
|
|
24
25
|
// Side-effect import: the Express request augmentation this capability's
|
|
25
26
|
// middleware writes (`req.oauthGrant`) and its consent controller reads
|
|
26
27
|
// (`req.user`). Importing the package is what gives an application those
|
|
@@ -42,11 +43,15 @@ Object.defineProperty(exports, "isClientRegistryError", { enumerable: true, get:
|
|
|
42
43
|
var client_registry_service_js_1 = require("./client-registry-service.js");
|
|
43
44
|
Object.defineProperty(exports, "createClientRegistryService", { enumerable: true, get: function () { return client_registry_service_js_1.createClientRegistryService; } });
|
|
44
45
|
Object.defineProperty(exports, "validateRedirectUris", { enumerable: true, get: function () { return client_registry_service_js_1.validateRedirectUris; } });
|
|
46
|
+
Object.defineProperty(exports, "STRANGER_CREATED_MODES", { enumerable: true, get: function () { return client_registry_service_js_1.STRANGER_CREATED_MODES; } });
|
|
45
47
|
Object.defineProperty(exports, "DEFAULT_GRANT_TYPES", { enumerable: true, get: function () { return client_registry_service_js_1.DEFAULT_GRANT_TYPES; } });
|
|
46
48
|
Object.defineProperty(exports, "DEFAULT_RESPONSE_TYPES", { enumerable: true, get: function () { return client_registry_service_js_1.DEFAULT_RESPONSE_TYPES; } });
|
|
47
49
|
Object.defineProperty(exports, "DEFAULT_METADATA_MAX_AGE_MS", { enumerable: true, get: function () { return client_registry_service_js_1.DEFAULT_METADATA_MAX_AGE_MS; } });
|
|
48
50
|
Object.defineProperty(exports, "DEFAULT_ABANDONED_CLIENT_TTL_MS", { enumerable: true, get: function () { return client_registry_service_js_1.DEFAULT_ABANDONED_CLIENT_TTL_MS; } });
|
|
49
51
|
Object.defineProperty(exports, "DEFAULT_MAX_REGISTERED_CLIENTS", { enumerable: true, get: function () { return client_registry_service_js_1.DEFAULT_MAX_REGISTERED_CLIENTS; } });
|
|
52
|
+
var outbound_target_js_1 = require("./outbound-target.js");
|
|
53
|
+
Object.defineProperty(exports, "blockedHostReason", { enumerable: true, get: function () { return outbound_target_js_1.blockedHostReason; } });
|
|
54
|
+
Object.defineProperty(exports, "isBlockedAddress", { enumerable: true, get: function () { return outbound_target_js_1.isBlockedAddress; } });
|
|
50
55
|
var metadata_document_js_1 = require("./metadata-document.js");
|
|
51
56
|
Object.defineProperty(exports, "fetchMetadataDocument", { enumerable: true, get: function () { return metadata_document_js_1.fetchMetadataDocument; } });
|
|
52
57
|
Object.defineProperty(exports, "isValidMetadataUri", { enumerable: true, get: function () { return metadata_document_js_1.isValidMetadataUri; } });
|
|
@@ -60,6 +65,8 @@ Object.defineProperty(exports, "CODE_CHALLENGE_METHODS_SUPPORTED", { enumerable:
|
|
|
60
65
|
Object.defineProperty(exports, "DEVICE_CODE_GRANT_TYPE", { enumerable: true, get: function () { return discovery_js_1.DEVICE_CODE_GRANT_TYPE; } });
|
|
61
66
|
Object.defineProperty(exports, "DEFAULT_SCOPES_SUPPORTED", { enumerable: true, get: function () { return discovery_js_1.DEFAULT_SCOPES_SUPPORTED; } });
|
|
62
67
|
Object.defineProperty(exports, "DISCOVERY_MAX_AGE_SECONDS", { enumerable: true, get: function () { return discovery_js_1.DISCOVERY_MAX_AGE_SECONDS; } });
|
|
68
|
+
Object.defineProperty(exports, "PROTECTED_RESOURCE_METADATA_PATH", { enumerable: true, get: function () { return discovery_js_1.PROTECTED_RESOURCE_METADATA_PATH; } });
|
|
69
|
+
Object.defineProperty(exports, "protectedResourceMetadataPaths", { enumerable: true, get: function () { return discovery_js_1.protectedResourceMetadataPaths; } });
|
|
63
70
|
var config_js_1 = require("./config.js");
|
|
64
71
|
Object.defineProperty(exports, "readServerConfig", { enumerable: true, get: function () { return config_js_1.readServerConfig; } });
|
|
65
72
|
Object.defineProperty(exports, "clampTtlSeconds", { enumerable: true, get: function () { return config_js_1.clampTtlSeconds; } });
|
|
@@ -76,7 +83,10 @@ Object.defineProperty(exports, "createRegistrationHandlers", { enumerable: true,
|
|
|
76
83
|
Object.defineProperty(exports, "createRegistrationRouter", { enumerable: true, get: function () { return registration_controller_js_1.createRegistrationRouter; } });
|
|
77
84
|
Object.defineProperty(exports, "toRegistrationResponse", { enumerable: true, get: function () { return registration_controller_js_1.toRegistrationResponse; } });
|
|
78
85
|
var rate_limit_js_1 = require("./rate-limit.js");
|
|
86
|
+
Object.defineProperty(exports, "createFixedWindowRateLimiter", { enumerable: true, get: function () { return rate_limit_js_1.createFixedWindowRateLimiter; } });
|
|
79
87
|
Object.defineProperty(exports, "createRegistrationRateLimiter", { enumerable: true, get: function () { return rate_limit_js_1.createRegistrationRateLimiter; } });
|
|
88
|
+
Object.defineProperty(exports, "rateLimitAddress", { enumerable: true, get: function () { return rate_limit_js_1.rateLimitAddress; } });
|
|
89
|
+
Object.defineProperty(exports, "DEFAULT_METADATA_RESOLUTION_RATE_LIMIT", { enumerable: true, get: function () { return rate_limit_js_1.DEFAULT_METADATA_RESOLUTION_RATE_LIMIT; } });
|
|
80
90
|
Object.defineProperty(exports, "DEFAULT_REGISTRATION_RATE_LIMIT", { enumerable: true, get: function () { return rate_limit_js_1.DEFAULT_REGISTRATION_RATE_LIMIT; } });
|
|
81
91
|
Object.defineProperty(exports, "DEFAULT_REGISTRATION_RATE_WINDOW_MS", { enumerable: true, get: function () { return rate_limit_js_1.DEFAULT_REGISTRATION_RATE_WINDOW_MS; } });
|
|
82
92
|
var types_js_1 = require("./types.js");
|
|
@@ -117,11 +127,13 @@ Object.defineProperty(exports, "createConsentRouter", { enumerable: true, get: f
|
|
|
117
127
|
Object.defineProperty(exports, "defaultIsSignedIn", { enumerable: true, get: function () { return consent_controller_js_1.defaultIsSignedIn; } });
|
|
118
128
|
Object.defineProperty(exports, "monogramFor", { enumerable: true, get: function () { return consent_controller_js_1.monogramFor; } });
|
|
119
129
|
Object.defineProperty(exports, "provenanceFor", { enumerable: true, get: function () { return consent_controller_js_1.provenanceFor; } });
|
|
120
|
-
Object.defineProperty(exports, "
|
|
130
|
+
Object.defineProperty(exports, "safeHttpsUri", { enumerable: true, get: function () { return consent_controller_js_1.safeHttpsUri; } });
|
|
121
131
|
Object.defineProperty(exports, "scopeDescriptionsFrom", { enumerable: true, get: function () { return consent_controller_js_1.scopeDescriptionsFrom; } });
|
|
122
132
|
Object.defineProperty(exports, "toScopeViews", { enumerable: true, get: function () { return consent_controller_js_1.toScopeViews; } });
|
|
123
133
|
Object.defineProperty(exports, "warnUndescribedScopes", { enumerable: true, get: function () { return consent_controller_js_1.warnUndescribedScopes; } });
|
|
124
134
|
Object.defineProperty(exports, "SCOPE_DESCRIPTIONS_LOCAL", { enumerable: true, get: function () { return consent_controller_js_1.SCOPE_DESCRIPTIONS_LOCAL; } });
|
|
135
|
+
var same_site_js_1 = require("./same-site.js");
|
|
136
|
+
Object.defineProperty(exports, "isSameSiteRequest", { enumerable: true, get: function () { return same_site_js_1.isSameSiteRequest; } });
|
|
125
137
|
var token_controller_js_1 = require("./token-controller.js");
|
|
126
138
|
Object.defineProperty(exports, "createTokenHandlers", { enumerable: true, get: function () { return token_controller_js_1.createTokenHandlers; } });
|
|
127
139
|
Object.defineProperty(exports, "createTokenRouter", { enumerable: true, get: function () { return token_controller_js_1.createTokenRouter; } });
|
|
@@ -134,6 +146,9 @@ Object.defineProperty(exports, "createOAuthTokenResolver", { enumerable: true, g
|
|
|
134
146
|
Object.defineProperty(exports, "missingScopes", { enumerable: true, get: function () { return bearer_js_1.missingScopes; } });
|
|
135
147
|
Object.defineProperty(exports, "requireOAuth", { enumerable: true, get: function () { return bearer_js_1.requireOAuth; } });
|
|
136
148
|
Object.defineProperty(exports, "DEFAULT_BEARER_CHALLENGE", { enumerable: true, get: function () { return bearer_js_1.DEFAULT_BEARER_CHALLENGE; } });
|
|
149
|
+
var client_prune_js_1 = require("./client-prune.js");
|
|
150
|
+
Object.defineProperty(exports, "createClientInUsePredicate", { enumerable: true, get: function () { return client_prune_js_1.createClientInUsePredicate; } });
|
|
151
|
+
Object.defineProperty(exports, "createClientPruneJob", { enumerable: true, get: function () { return client_prune_js_1.createClientPruneJob; } });
|
|
137
152
|
var revocation_service_js_1 = require("./revocation-service.js");
|
|
138
153
|
Object.defineProperty(exports, "createRevocationService", { enumerable: true, get: function () { return revocation_service_js_1.createRevocationService; } });
|
|
139
154
|
Object.defineProperty(exports, "REVOKED_BY_CLIENT", { enumerable: true, get: function () { return revocation_service_js_1.REVOKED_BY_CLIENT; } });
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;GAkBG
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,6 +11,16 @@ import type { ClientIdMetadataDocument } from './types.js';
|
|
|
11
11
|
* drive it directly: what a failing fetch, an oversized body or a lying
|
|
12
12
|
* document do is behaviour worth pinning, and none of it is reachable through a
|
|
13
13
|
* real network in a test suite.
|
|
14
|
+
*
|
|
15
|
+
* ## Where the URL may point
|
|
16
|
+
*
|
|
17
|
+
* The bounds below — timeout, size, hop count — bound what a fetch can *cost*.
|
|
18
|
+
* They say nothing about where it goes, and for a while nothing did: an
|
|
19
|
+
* unauthenticated `GET /oauth/authorize` naming any https URL made this server
|
|
20
|
+
* issue a request to it, so the response time answered "is there something at
|
|
21
|
+
* `10.0.0.5:443`?" for anyone who asked (boop-jd4x). `outbound-target.ts` is
|
|
22
|
+
* that guard, and it runs **before every hop**, including the ones a redirect
|
|
23
|
+
* chooses.
|
|
14
24
|
*/
|
|
15
25
|
/** The `fetch` shape this module needs. Narrower than the global on purpose. */
|
|
16
26
|
export type FetchLike = (url: string, init: {
|
|
@@ -36,6 +46,18 @@ export interface MetadataFetchOptions {
|
|
|
36
46
|
maxBytes?: number;
|
|
37
47
|
/** How many `https`→`https` hops to follow before giving up. */
|
|
38
48
|
maxRedirects?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a hostname to the addresses it points at.
|
|
51
|
+
*
|
|
52
|
+
* Optional. Without it, every *literal* private, loopback or link-local
|
|
53
|
+
* target is still refused, and so are the names reserved for a local network;
|
|
54
|
+
* with it, a perfectly ordinary name that answers `10.0.0.5` is refused too.
|
|
55
|
+
* Injected rather than imported because a capability package has no business
|
|
56
|
+
* reaching for `node:dns`, and because a resolver in the module body is a
|
|
57
|
+
* resolver no test can stand in front of. The scaffold wires
|
|
58
|
+
* `dns.promises.lookup`.
|
|
59
|
+
*/
|
|
60
|
+
resolveHost?: (hostname: string) => Promise<string[]>;
|
|
39
61
|
}
|
|
40
62
|
export declare const DEFAULT_METADATA_TIMEOUT_MS = 3000;
|
|
41
63
|
export declare const DEFAULT_METADATA_MAX_BYTES: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata-document.d.ts","sourceRoot":"","sources":["../src/metadata-document.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"metadata-document.d.ts","sourceRoot":"","sources":["../src/metadata-document.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,gFAAgF;AAChF,MAAM,MAAM,SAAS,GAAG,CACtB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IACJ,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC,KACE,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAEhC,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE;QAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAC9C,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CACvD;AAED,eAAO,MAAM,2BAA2B,OAAQ,CAAC;AACjD,eAAO,MAAM,0BAA0B,QAAY,CAAC;AACpD,eAAO,MAAM,8BAA8B,IAAI,CAAC;AAKhD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAczD;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,SAAS,EACpB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CAoDnC"}
|
|
@@ -4,6 +4,7 @@ exports.DEFAULT_METADATA_MAX_REDIRECTS = exports.DEFAULT_METADATA_MAX_BYTES = ex
|
|
|
4
4
|
exports.isValidMetadataUri = isValidMetadataUri;
|
|
5
5
|
exports.fetchMetadataDocument = fetchMetadataDocument;
|
|
6
6
|
const errors_js_1 = require("./errors.js");
|
|
7
|
+
const outbound_target_js_1 = require("./outbound-target.js");
|
|
7
8
|
exports.DEFAULT_METADATA_TIMEOUT_MS = 3000;
|
|
8
9
|
exports.DEFAULT_METADATA_MAX_BYTES = 64 * 1024;
|
|
9
10
|
exports.DEFAULT_METADATA_MAX_REDIRECTS = 3;
|
|
@@ -44,7 +45,12 @@ async function fetchMetadataDocument(clientId, fetchImpl, options = {}) {
|
|
|
44
45
|
if (!isValidMetadataUri(clientId)) {
|
|
45
46
|
throw unusable(`client_id is not a usable https metadata document URL: ${clientId}`);
|
|
46
47
|
}
|
|
47
|
-
const body = await readDocument(clientId, fetchImpl, {
|
|
48
|
+
const body = await readDocument(clientId, fetchImpl, {
|
|
49
|
+
timeoutMs,
|
|
50
|
+
maxBytes,
|
|
51
|
+
maxRedirects,
|
|
52
|
+
resolveHost: options.resolveHost,
|
|
53
|
+
});
|
|
48
54
|
let parsed;
|
|
49
55
|
try {
|
|
50
56
|
parsed = JSON.parse(body);
|
|
@@ -78,10 +84,46 @@ async function fetchMetadataDocument(clientId, fetchImpl, options = {}) {
|
|
|
78
84
|
}
|
|
79
85
|
return doc;
|
|
80
86
|
}
|
|
81
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* Refuse a target that is not on the public internet.
|
|
89
|
+
*
|
|
90
|
+
* Called before the request rather than after it, because the request *is* the
|
|
91
|
+
* leak: the body is discarded on a refusal either way, and what an attacker
|
|
92
|
+
* reads is whether the connection was made and how long it took.
|
|
93
|
+
*/
|
|
94
|
+
async function assertPublicTarget(url, resolveHost) {
|
|
95
|
+
const { hostname } = new URL(url);
|
|
96
|
+
const reason = (0, outbound_target_js_1.blockedHostReason)(hostname);
|
|
97
|
+
if (reason !== null)
|
|
98
|
+
throw unusable(`metadata document at ${url} is not fetchable: ${reason}`);
|
|
99
|
+
if (!resolveHost)
|
|
100
|
+
return;
|
|
101
|
+
let addresses;
|
|
102
|
+
try {
|
|
103
|
+
addresses = await resolveHost(hostname);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
// Fail closed. A name we could not resolve is a name we cannot vouch for,
|
|
107
|
+
// and "could not check" must never read as "checked and fine".
|
|
108
|
+
throw unusable(`metadata document at ${url} could not be resolved: ${describe(err)}`);
|
|
109
|
+
}
|
|
110
|
+
// An empty answer is not a clean bill of health: it means nothing was
|
|
111
|
+
// checked, which is the same position as a resolver that threw.
|
|
112
|
+
if (addresses.length === 0) {
|
|
113
|
+
throw unusable(`metadata document at ${url} resolves to no address`);
|
|
114
|
+
}
|
|
115
|
+
const blocked = addresses.find((address) => (0, outbound_target_js_1.isBlockedAddress)(address));
|
|
116
|
+
if (blocked !== undefined) {
|
|
117
|
+
throw unusable(`metadata document at ${url} resolves to ${blocked}, which is not a public address`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** The transport half: target guard, redirects, timeout, size cap. */
|
|
82
121
|
async function readDocument(startUrl, fetchImpl, limits) {
|
|
83
122
|
let url = startUrl;
|
|
84
123
|
for (let hop = 0; hop <= limits.maxRedirects; hop += 1) {
|
|
124
|
+
// Every hop, not just the first: a public URL that redirects to
|
|
125
|
+
// `https://169.254.169.254` is the same probe with one step in front of it.
|
|
126
|
+
await assertPublicTarget(url, limits.resolveHost);
|
|
85
127
|
const controller = new AbortController();
|
|
86
128
|
const timer = setTimeout(() => controller.abort(), limits.timeoutMs);
|
|
87
129
|
let response;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata-document.js","sourceRoot":"","sources":["../src/metadata-document.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"metadata-document.js","sourceRoot":"","sources":["../src/metadata-document.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a caller-chosen URL may point.
|
|
3
|
+
*
|
|
4
|
+
* One place in this capability fetches a URL a stranger supplied: the Client ID
|
|
5
|
+
* Metadata Document path, reached from an unauthenticated `GET /oauth/authorize`
|
|
6
|
+
* whose `client_id` is an https URL. Without a guard, that is an SSRF primitive
|
|
7
|
+
* pointed at whatever network the application runs in — the document body is
|
|
8
|
+
* discarded on a failure, but *existence* and *latency* are both readable
|
|
9
|
+
* through the request's response time, and the outbound request happens whatever
|
|
10
|
+
* the answer (boop-jd4x).
|
|
11
|
+
*
|
|
12
|
+
* ## What this can and cannot promise
|
|
13
|
+
*
|
|
14
|
+
* The literal tables below are exact: an address in one of these ranges is never
|
|
15
|
+
* fetched. A *name* is harder, and is handled in two layers.
|
|
16
|
+
*
|
|
17
|
+
* 1. Names reserved by convention for a local network — `localhost`, `.local`,
|
|
18
|
+
* `.internal`, `.home.arpa` — are refused outright.
|
|
19
|
+
* 2. Everything else needs DNS, which this package will not do: it has no
|
|
20
|
+
* business importing `node:dns`, and a capability that did could not be
|
|
21
|
+
* tested without a resolver. So the resolver is injected
|
|
22
|
+
* (`metadataResolveHost`), and when an application supplies one, every
|
|
23
|
+
* address a name resolves to is run through the same table.
|
|
24
|
+
*
|
|
25
|
+
* Even with a resolver this is not airtight, and the gap has a name: between the
|
|
26
|
+
* lookup here and the connection the HTTP client makes, the name can be
|
|
27
|
+
* re-resolved to a different address (DNS rebinding). Closing that needs a
|
|
28
|
+
* transport that connects to an address this code chose, which `fetch` does not
|
|
29
|
+
* offer. What is here raises the cost of the probe from "free" to "needs a
|
|
30
|
+
* rebinding setup", and refuses the whole class of literal and by-name targets
|
|
31
|
+
* that a probe actually uses.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Is this string a literal address we refuse to connect to?
|
|
35
|
+
*
|
|
36
|
+
* `false` for anything that is not an address at all, including every hostname —
|
|
37
|
+
* a name is `blockedHostReason`'s business, because deciding it needs DNS.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isBlockedAddress(value: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Why this hostname may not be fetched, or `null` if it may.
|
|
42
|
+
*
|
|
43
|
+
* A sentence rather than a boolean, because the caller puts it in a refusal an
|
|
44
|
+
* operator has to read: "could not be fetched" tells them nothing, and a CIMD
|
|
45
|
+
* publisher who pointed at the wrong host needs to see which host.
|
|
46
|
+
*/
|
|
47
|
+
export declare function blockedHostReason(hostname: string): string | null;
|
|
48
|
+
//# sourceMappingURL=outbound-target.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbound-target.d.ts","sourceRoot":"","sources":["../src/outbound-target.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AA+GH;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAWvD;AASD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBjE"}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Where a caller-chosen URL may point.
|
|
4
|
+
*
|
|
5
|
+
* One place in this capability fetches a URL a stranger supplied: the Client ID
|
|
6
|
+
* Metadata Document path, reached from an unauthenticated `GET /oauth/authorize`
|
|
7
|
+
* whose `client_id` is an https URL. Without a guard, that is an SSRF primitive
|
|
8
|
+
* pointed at whatever network the application runs in — the document body is
|
|
9
|
+
* discarded on a failure, but *existence* and *latency* are both readable
|
|
10
|
+
* through the request's response time, and the outbound request happens whatever
|
|
11
|
+
* the answer (boop-jd4x).
|
|
12
|
+
*
|
|
13
|
+
* ## What this can and cannot promise
|
|
14
|
+
*
|
|
15
|
+
* The literal tables below are exact: an address in one of these ranges is never
|
|
16
|
+
* fetched. A *name* is harder, and is handled in two layers.
|
|
17
|
+
*
|
|
18
|
+
* 1. Names reserved by convention for a local network — `localhost`, `.local`,
|
|
19
|
+
* `.internal`, `.home.arpa` — are refused outright.
|
|
20
|
+
* 2. Everything else needs DNS, which this package will not do: it has no
|
|
21
|
+
* business importing `node:dns`, and a capability that did could not be
|
|
22
|
+
* tested without a resolver. So the resolver is injected
|
|
23
|
+
* (`metadataResolveHost`), and when an application supplies one, every
|
|
24
|
+
* address a name resolves to is run through the same table.
|
|
25
|
+
*
|
|
26
|
+
* Even with a resolver this is not airtight, and the gap has a name: between the
|
|
27
|
+
* lookup here and the connection the HTTP client makes, the name can be
|
|
28
|
+
* re-resolved to a different address (DNS rebinding). Closing that needs a
|
|
29
|
+
* transport that connects to an address this code chose, which `fetch` does not
|
|
30
|
+
* offer. What is here raises the cost of the probe from "free" to "needs a
|
|
31
|
+
* rebinding setup", and refuses the whole class of literal and by-name targets
|
|
32
|
+
* that a probe actually uses.
|
|
33
|
+
*/
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.isBlockedAddress = isBlockedAddress;
|
|
36
|
+
exports.blockedHostReason = blockedHostReason;
|
|
37
|
+
/** The four octets of a dotted-quad, or `null` if it is not one. */
|
|
38
|
+
function parseIpv4(value) {
|
|
39
|
+
const parts = value.split('.');
|
|
40
|
+
if (parts.length !== 4)
|
|
41
|
+
return null;
|
|
42
|
+
const octets = [];
|
|
43
|
+
for (const part of parts) {
|
|
44
|
+
// No octal, no hex, no leading zeros: `010.0.0.1` is a spelling of
|
|
45
|
+
// `8.0.0.1` in some resolvers and of `10.0.0.1` in others, and a table that
|
|
46
|
+
// has to guess which is a table with a hole in it. Anything ambiguous is
|
|
47
|
+
// not an address as far as this code is concerned, and a hostname that is
|
|
48
|
+
// not an address is decided by DNS instead.
|
|
49
|
+
if (!/^(0|[1-9]\d{0,2})$/.test(part))
|
|
50
|
+
return null;
|
|
51
|
+
const octet = Number(part);
|
|
52
|
+
if (octet > 255)
|
|
53
|
+
return null;
|
|
54
|
+
octets.push(octet);
|
|
55
|
+
}
|
|
56
|
+
return octets;
|
|
57
|
+
}
|
|
58
|
+
/** The sixteen bytes of an IPv6 address, or `null` if it is not one. */
|
|
59
|
+
function parseIpv6(value) {
|
|
60
|
+
// A zone index (`fe80::1%eth0`) names an interface, not a different address.
|
|
61
|
+
let text = value.toLowerCase().split('%')[0];
|
|
62
|
+
if (!text.includes(':'))
|
|
63
|
+
return null;
|
|
64
|
+
if (!/^[0-9a-f:.]*$/.test(text))
|
|
65
|
+
return null;
|
|
66
|
+
// A trailing dotted-quad (`::ffff:127.0.0.1`) is the same address as the two
|
|
67
|
+
// hextets it stands for, so rewrite it and carry on with one parser.
|
|
68
|
+
const lastColon = text.lastIndexOf(':');
|
|
69
|
+
const tail = text.slice(lastColon + 1);
|
|
70
|
+
if (tail.includes('.')) {
|
|
71
|
+
const quad = parseIpv4(tail);
|
|
72
|
+
if (!quad)
|
|
73
|
+
return null;
|
|
74
|
+
const high = ((quad[0] << 8) | quad[1]).toString(16);
|
|
75
|
+
const low = ((quad[2] << 8) | quad[3]).toString(16);
|
|
76
|
+
text = `${text.slice(0, lastColon + 1)}${high}:${low}`;
|
|
77
|
+
}
|
|
78
|
+
const halves = text.split('::');
|
|
79
|
+
if (halves.length > 2)
|
|
80
|
+
return null;
|
|
81
|
+
const head = halves[0] === '' ? [] : halves[0].split(':');
|
|
82
|
+
const rest = halves.length === 2 ? (halves[1] === '' ? [] : halves[1].split(':')) : [];
|
|
83
|
+
let groups;
|
|
84
|
+
if (halves.length === 2) {
|
|
85
|
+
const fill = 8 - head.length - rest.length;
|
|
86
|
+
if (fill < 1)
|
|
87
|
+
return null;
|
|
88
|
+
groups = [...head, ...Array(fill).fill('0'), ...rest];
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
groups = head;
|
|
92
|
+
}
|
|
93
|
+
if (groups.length !== 8)
|
|
94
|
+
return null;
|
|
95
|
+
const bytes = [];
|
|
96
|
+
for (const group of groups) {
|
|
97
|
+
if (!/^[0-9a-f]{1,4}$/.test(group))
|
|
98
|
+
return null;
|
|
99
|
+
const hextet = Number.parseInt(group, 16);
|
|
100
|
+
bytes.push(hextet >>> 8, hextet & 0xff);
|
|
101
|
+
}
|
|
102
|
+
return bytes;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Is this IPv4 address somewhere other than the public internet?
|
|
106
|
+
*
|
|
107
|
+
* The list is IANA's special-purpose registry, narrowed to the entries a probe
|
|
108
|
+
* would actually aim at. `169.254.0.0/16` is the one to notice: every major
|
|
109
|
+
* cloud puts its instance metadata service at `169.254.169.254`, and that
|
|
110
|
+
* endpoint hands out credentials.
|
|
111
|
+
*/
|
|
112
|
+
function isBlockedIpv4(octets) {
|
|
113
|
+
const [a, b] = octets;
|
|
114
|
+
if (a === 0)
|
|
115
|
+
return true; // "this network"
|
|
116
|
+
if (a === 10)
|
|
117
|
+
return true; // RFC 1918
|
|
118
|
+
if (a === 127)
|
|
119
|
+
return true; // loopback
|
|
120
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
121
|
+
return true; // RFC 6598 carrier-grade NAT
|
|
122
|
+
if (a === 169 && b === 254)
|
|
123
|
+
return true; // link-local, incl. cloud metadata
|
|
124
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
125
|
+
return true; // RFC 1918
|
|
126
|
+
if (a === 192 && b === 0)
|
|
127
|
+
return true; // IETF protocol assignments
|
|
128
|
+
if (a === 192 && b === 168)
|
|
129
|
+
return true; // RFC 1918
|
|
130
|
+
if (a === 198 && (b === 18 || b === 19))
|
|
131
|
+
return true; // RFC 2544 benchmarking
|
|
132
|
+
if (a >= 224)
|
|
133
|
+
return true; // multicast, reserved, and 255.255.255.255
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
/** The same question for IPv6, including addresses wearing an IPv4 coat. */
|
|
137
|
+
function isBlockedIpv6(bytes) {
|
|
138
|
+
const isZero = (from, to) => bytes.slice(from, to).every((byte) => byte === 0);
|
|
139
|
+
// `::ffff:0:0/96` — an IPv4 address in IPv6 clothing. Judged as the IPv4
|
|
140
|
+
// address it is, or `::ffff:127.0.0.1` would walk straight past the table.
|
|
141
|
+
if (isZero(0, 10) && bytes[10] === 0xff && bytes[11] === 0xff) {
|
|
142
|
+
return isBlockedIpv4(bytes.slice(12));
|
|
143
|
+
}
|
|
144
|
+
// `64:ff9b::/96` — the well-known NAT64 prefix, which is another way to spell
|
|
145
|
+
// an IPv4 destination for a network that has a translator. Same treatment.
|
|
146
|
+
if (bytes[0] === 0x00 && bytes[1] === 0x64 && bytes[2] === 0xff && bytes[3] === 0x9b &&
|
|
147
|
+
isZero(4, 12)) {
|
|
148
|
+
return isBlockedIpv4(bytes.slice(12));
|
|
149
|
+
}
|
|
150
|
+
// `::` (unspecified) and `::1` (loopback), and the rest of `::/64` with them:
|
|
151
|
+
// nothing in that range is a host on the public internet.
|
|
152
|
+
if (isZero(0, 8))
|
|
153
|
+
return true;
|
|
154
|
+
if ((bytes[0] & 0xfe) === 0xfc)
|
|
155
|
+
return true; // fc00::/7 unique local
|
|
156
|
+
if (bytes[0] === 0xfe && (bytes[1] & 0xc0) === 0x80)
|
|
157
|
+
return true; // fe80::/10 link-local
|
|
158
|
+
if (bytes[0] === 0xff)
|
|
159
|
+
return true; // ff00::/8 multicast
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Is this string a literal address we refuse to connect to?
|
|
164
|
+
*
|
|
165
|
+
* `false` for anything that is not an address at all, including every hostname —
|
|
166
|
+
* a name is `blockedHostReason`'s business, because deciding it needs DNS.
|
|
167
|
+
*/
|
|
168
|
+
function isBlockedAddress(value) {
|
|
169
|
+
const text = value.trim();
|
|
170
|
+
if (text === '')
|
|
171
|
+
return false;
|
|
172
|
+
const ipv4 = parseIpv4(text);
|
|
173
|
+
if (ipv4)
|
|
174
|
+
return isBlockedIpv4(ipv4);
|
|
175
|
+
const ipv6 = parseIpv6(text);
|
|
176
|
+
if (ipv6)
|
|
177
|
+
return isBlockedIpv6(ipv6);
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Names reserved for a local network. RFC 6761 (`localhost`), RFC 6762
|
|
182
|
+
* (`.local`), RFC 8375 (`.home.arpa`), and `.internal`, which is what cloud
|
|
183
|
+
* providers hand out for private zones and which ICANN has now reserved.
|
|
184
|
+
*/
|
|
185
|
+
const BLOCKED_SUFFIXES = ['.localhost', '.local', '.internal', '.home.arpa'];
|
|
186
|
+
/**
|
|
187
|
+
* Why this hostname may not be fetched, or `null` if it may.
|
|
188
|
+
*
|
|
189
|
+
* A sentence rather than a boolean, because the caller puts it in a refusal an
|
|
190
|
+
* operator has to read: "could not be fetched" tells them nothing, and a CIMD
|
|
191
|
+
* publisher who pointed at the wrong host needs to see which host.
|
|
192
|
+
*/
|
|
193
|
+
function blockedHostReason(hostname) {
|
|
194
|
+
// A URL spells an IPv6 literal in brackets; the address inside is what the
|
|
195
|
+
// table knows about.
|
|
196
|
+
const host = hostname.trim().toLowerCase().replace(/^\[|\]$/g, '');
|
|
197
|
+
if (host === '')
|
|
198
|
+
return 'the URL names no host';
|
|
199
|
+
if (isBlockedAddress(host)) {
|
|
200
|
+
return `${hostname} is a private, loopback, link-local or otherwise non-public address`;
|
|
201
|
+
}
|
|
202
|
+
// A trailing dot is a fully-qualified spelling of the same name.
|
|
203
|
+
const name = host.endsWith('.') ? host.slice(0, -1) : host;
|
|
204
|
+
if (name === 'localhost')
|
|
205
|
+
return `${hostname} names this machine`;
|
|
206
|
+
if (BLOCKED_SUFFIXES.some((suffix) => name.endsWith(suffix))) {
|
|
207
|
+
return `${hostname} is a name reserved for a local network`;
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=outbound-target.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbound-target.js","sourceRoot":"","sources":["../src/outbound-target.ts"],"names":[],"mappings":"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"}
|
package/dist/rate-limit.d.ts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
import type { RequestHandler } from 'express';
|
|
1
|
+
import type { Request, RequestHandler } from 'express';
|
|
2
2
|
/**
|
|
3
|
-
* Per-IP rate limiting for the
|
|
3
|
+
* Per-IP rate limiting for the paths that let a stranger write a row.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* cannot have.
|
|
5
|
+
* There are two, and for a long time only one of them had this. `POST
|
|
6
|
+
* /oauth/register` is unauthenticated by design — RFC 7591 open registration has
|
|
7
|
+
* no caller to authenticate — so there is no account to hang a quota on and the
|
|
8
|
+
* source address is the only thing a limiter has to go on. Neither donor has
|
|
9
|
+
* anything like this: paywalls' registration endpoint required an authenticated
|
|
10
|
+
* `service-user`, which is a gate this endpoint cannot have.
|
|
11
|
+
*
|
|
12
|
+
* The second is **first-sight metadata-document resolution** at `GET
|
|
13
|
+
* /oauth/authorize`. It is equally unauthenticated, it writes a row per distinct
|
|
14
|
+
* URL, and it makes an outbound request besides — so it is limited by the same
|
|
15
|
+
* counter, mounted by the consent handlers rather than by a router, because only
|
|
16
|
+
* the handler knows whether a given request is about to resolve a new client or
|
|
17
|
+
* serve one that already has a row (boop-jd4x).
|
|
11
18
|
*
|
|
12
19
|
* ## What this is, and what it is not
|
|
13
20
|
*
|
|
@@ -27,6 +34,14 @@ import type { RequestHandler } from 'express';
|
|
|
27
34
|
* boundary. Named here rather than fixed, because the mitigation (a sliding
|
|
28
35
|
* window) buys precision this does not need against a ceiling that is already
|
|
29
36
|
* the real bound.
|
|
37
|
+
*
|
|
38
|
+
* ## The address has to be the caller's
|
|
39
|
+
*
|
|
40
|
+
* Both limiters key on `req.ip`, which behind a proxy is the proxy unless the
|
|
41
|
+
* application has set `trust proxy`. Left unset, every caller shares one bucket:
|
|
42
|
+
* one abuser exhausts it for everybody, and no individual abuser is ever
|
|
43
|
+
* stopped. That is worth checking in a deployment — it is the difference between
|
|
44
|
+
* a limiter and a decoration.
|
|
30
45
|
*/
|
|
31
46
|
export interface RateLimiterOptions {
|
|
32
47
|
/** Requests allowed per window, per address. */
|
|
@@ -43,14 +58,41 @@ export interface RateLimiterOptions {
|
|
|
43
58
|
}
|
|
44
59
|
export declare const DEFAULT_REGISTRATION_RATE_LIMIT = 10;
|
|
45
60
|
export declare const DEFAULT_REGISTRATION_RATE_WINDOW_MS: number;
|
|
46
|
-
|
|
61
|
+
/**
|
|
62
|
+
* First-sight metadata-document resolutions allowed per address per window.
|
|
63
|
+
*
|
|
64
|
+
* The same number as registration, and for the same reason: reaching it is not
|
|
65
|
+
* something an honest browser does. A resolution is charged only when the
|
|
66
|
+
* `client_id` is an https URL with **no row yet** — every later visit to the
|
|
67
|
+
* same client is served from the row and costs nothing — so ten distinct,
|
|
68
|
+
* never-before-seen connectors an hour from one address is already generous.
|
|
69
|
+
*/
|
|
70
|
+
export declare const DEFAULT_METADATA_RESOLUTION_RATE_LIMIT = 10;
|
|
71
|
+
export interface FixedWindowRateLimiter {
|
|
72
|
+
/** The `POST /oauth/register` mount. */
|
|
47
73
|
middleware: RequestHandler;
|
|
48
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* The counter itself, for a caller that decides *whether* a request counts —
|
|
76
|
+
* the consent handlers charge only a first-sight resolution — and for tests
|
|
77
|
+
* and operator scripts.
|
|
78
|
+
*/
|
|
49
79
|
check(address: string): {
|
|
50
80
|
allowed: boolean;
|
|
51
81
|
retryAfterSeconds: number;
|
|
52
82
|
};
|
|
53
83
|
reset(): void;
|
|
54
84
|
}
|
|
55
|
-
|
|
85
|
+
/** The old name, kept so an application that imported it still compiles. */
|
|
86
|
+
export type RegistrationRateLimiter = FixedWindowRateLimiter;
|
|
87
|
+
/**
|
|
88
|
+
* The address a limiter counts against.
|
|
89
|
+
*
|
|
90
|
+
* Exported so the middleware and the consent handlers cannot pick different
|
|
91
|
+
* answers — two limiters keyed differently are two quotas, and the one an
|
|
92
|
+
* attacker would find is whichever is keyed more loosely.
|
|
93
|
+
*/
|
|
94
|
+
export declare function rateLimitAddress(req: Request): string;
|
|
95
|
+
export declare function createFixedWindowRateLimiter(options?: RateLimiterOptions): FixedWindowRateLimiter;
|
|
96
|
+
/** The registration endpoint's name for the same factory. */
|
|
97
|
+
export declare const createRegistrationRateLimiter: typeof createFixedWindowRateLimiter;
|
|
56
98
|
//# sourceMappingURL=rate-limit.d.ts.map
|
package/dist/rate-limit.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,OAAO,EAAE,cAAc,EAAY,MAAM,SAAS,CAAC;AAE/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,MAAM,WAAW,kBAAkB;IACjC,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,eAAO,MAAM,+BAA+B,KAAK,CAAC;AAClD,eAAO,MAAM,mCAAmC,QAAiB,CAAC;AAElE;;;;;;;;GAQG;AACH,eAAO,MAAM,sCAAsC,KAAK,CAAC;AAQzD,MAAM,WAAW,sBAAsB;IACrC,wCAAwC;IACxC,UAAU,EAAE,cAAc,CAAC;IAC3B;;;;OAIG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAA;KAAE,CAAC;IACxE,KAAK,IAAI,IAAI,CAAC;CACf;AAED,4EAA4E;AAC5E,MAAM,MAAM,uBAAuB,GAAG,sBAAsB,CAAC;AAE7D;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAErD;AAED,wBAAgB,4BAA4B,CAC1C,OAAO,GAAE,kBAAuB,GAC/B,sBAAsB,CAmDxB;AAED,6DAA6D;AAC7D,eAAO,MAAM,6BAA6B,qCAA+B,CAAC"}
|