@rebasepro/server 0.14.2-canary.g991e9df → 0.14.2-canary.gf913b76
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/dist/api/logs-routes.d.ts +47 -8
- package/dist/auth/index.d.ts +4 -0
- package/dist/auth/resolve-rate-limit-store.d.ts +51 -0
- package/dist/auth/sql-rate-limit-store.d.ts +63 -0
- package/dist/{auth-BobZVd0j.js → auth-CIWux0Ol.js} +198 -3
- package/dist/auth-CIWux0Ol.js.map +1 -0
- package/dist/boot/boot.d.ts +15 -0
- package/dist/{cron-store-CB1x-Ken.js → cron-store-DvabPKwA.js} +2 -2
- package/dist/{cron-store-CB1x-Ken.js.map → cron-store-DvabPKwA.js.map} +1 -1
- package/dist/ddl-bootstrap-Cywoj8Ta.js.map +1 -1
- package/dist/index.es.js +109 -14
- package/dist/index.es.js.map +1 -1
- package/dist/{jobs-DR4SjGrD.js → jobs-DtxzEBEK.js} +2 -2
- package/dist/{jobs-DR4SjGrD.js.map → jobs-DtxzEBEK.js.map} +1 -1
- package/dist/logs-routes-CWBLQj2l.js +245 -0
- package/dist/logs-routes-CWBLQj2l.js.map +1 -0
- package/dist/{openapi-generator-DQeQ_q2f.js → openapi-generator-CkXIlxMM.js} +2 -2
- package/dist/{openapi-generator-DQeQ_q2f.js.map → openapi-generator-CkXIlxMM.js.map} +1 -1
- package/dist/{src-8XDWyDfR.js → src-Cx24hMDv.js} +44 -12
- package/dist/src-Cx24hMDv.js.map +1 -0
- package/package.json +5 -5
- package/dist/auth-BobZVd0j.js.map +0 -1
- package/dist/logs-routes-BYA72C_C.js +0 -100
- package/dist/logs-routes-BYA72C_C.js.map +0 -1
- package/dist/src-8XDWyDfR.js.map +0 -1
|
@@ -9,19 +9,34 @@ export interface LogEntry {
|
|
|
9
9
|
message: string;
|
|
10
10
|
metadata?: Record<string, unknown>;
|
|
11
11
|
}
|
|
12
|
+
/** What a caller can narrow the log by, in either direction (query or stream). */
|
|
13
|
+
export interface LogFilterOptions {
|
|
14
|
+
level?: string;
|
|
15
|
+
source?: string;
|
|
16
|
+
search?: string;
|
|
17
|
+
since?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Notified for every entry pushed, in push order. */
|
|
20
|
+
export type LogListener = (entry: LogEntry) => void;
|
|
12
21
|
declare class LogRingBuffer {
|
|
13
22
|
private buffer;
|
|
14
23
|
private maxSize;
|
|
15
24
|
private idCounter;
|
|
25
|
+
private listeners;
|
|
16
26
|
constructor(maxSize?: number);
|
|
17
27
|
push(entry: Omit<LogEntry, "id">): void;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Follow the buffer. Returns the unsubscribe — call it, always: a listener
|
|
30
|
+
* left behind holds its whole closure, and on this class that closure is a
|
|
31
|
+
* pending-entry array.
|
|
32
|
+
*
|
|
33
|
+
* A listener must not log. It is called from inside `push`, so anything that
|
|
34
|
+
* reaches `addLog` from here recurses until the stack gives out.
|
|
35
|
+
*/
|
|
36
|
+
subscribe(listener: LogListener): () => void;
|
|
37
|
+
query(options: LogFilterOptions & {
|
|
22
38
|
limit?: number;
|
|
23
39
|
offset?: number;
|
|
24
|
-
since?: string;
|
|
25
40
|
}): {
|
|
26
41
|
entries: LogEntry[];
|
|
27
42
|
total: number;
|
|
@@ -31,7 +46,31 @@ declare class LogRingBuffer {
|
|
|
31
46
|
export declare const logBuffer: LogRingBuffer;
|
|
32
47
|
/** Add a log entry */
|
|
33
48
|
export declare function addLog(level: LogEntry["level"], source: LogEntry["source"], message: string, metadata?: Record<string, unknown>): void;
|
|
49
|
+
export interface LogMiddlewareOptions {
|
|
50
|
+
/**
|
|
51
|
+
* Paths this sink ignores, matched exactly against `c.req.path`.
|
|
52
|
+
*
|
|
53
|
+
* For requests whose only reason to exist is to read the log. Recording
|
|
54
|
+
* those makes the reader the loudest thing in its own output, and on a quiet
|
|
55
|
+
* server it is also the thing evicting real entries out of the ring.
|
|
56
|
+
*/
|
|
57
|
+
ignorePaths?: string[];
|
|
58
|
+
}
|
|
34
59
|
/** Hono middleware to log API requests */
|
|
35
|
-
export declare function logMiddleware(): MiddlewareHandler<HonoEnv>;
|
|
36
|
-
|
|
37
|
-
|
|
60
|
+
export declare function logMiddleware(options?: LogMiddlewareOptions): MiddlewareHandler<HonoEnv>;
|
|
61
|
+
/**
|
|
62
|
+
* The stream's timings, injectable only so they can be tested.
|
|
63
|
+
*
|
|
64
|
+
* The defaults above are the contract and nothing in production passes this. A
|
|
65
|
+
* heartbeat is a 25-second wait to observe, and a suite that cannot observe it is
|
|
66
|
+
* a suite where the keepalive can rot — which surfaces as "the tail dies after a
|
|
67
|
+
* few minutes behind the load balancer", months later, on someone else's cluster.
|
|
68
|
+
*/
|
|
69
|
+
export interface LogStreamTiming {
|
|
70
|
+
flushMs?: number;
|
|
71
|
+
heartbeatMs?: number;
|
|
72
|
+
maxPending?: number;
|
|
73
|
+
}
|
|
74
|
+
export declare function createLogsRoutes(timing?: LogStreamTiming): Hono<HonoEnv>;
|
|
75
|
+
declare const _default: Hono<HonoEnv, import("hono/types").BlankSchema, "/">;
|
|
76
|
+
export default _default;
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -41,6 +41,10 @@ export { createRateLimiter, defaultAuthLimiter, strictAuthLimiter, createDataRat
|
|
|
41
41
|
export type { DataRateLimitConfig } from "./rate-limiter";
|
|
42
42
|
export { MemoryRateLimitStore } from "./rate-limit-store";
|
|
43
43
|
export type { RateLimitStore, RateLimitDecision } from "./rate-limit-store";
|
|
44
|
+
export { createSqlRateLimitStore } from "./sql-rate-limit-store";
|
|
45
|
+
export type { SqlRateLimitStoreOptions } from "./sql-rate-limit-store";
|
|
46
|
+
export { resolveRateLimitStoreKind, RateLimitStoreConfigurationError } from "./resolve-rate-limit-store";
|
|
47
|
+
export type { RateLimitStoreKind, RateLimitStoreEnv } from "./resolve-rate-limit-store";
|
|
44
48
|
export { createApiKeyStore, createApiKeyRoutes, isApiKeyToken, validateApiKey, httpMethodToOperation, isOperationAllowed } from "./api-keys";
|
|
45
49
|
export type { ApiKey, ApiKeyMasked, ApiKeyPermission, ApiKeyWithSecret, CreateApiKeyRequest, UpdateApiKeyRequest, ApiKeyStore, ApiKeyOperation } from "./api-keys";
|
|
46
50
|
export { createBuiltinAuthAdapter } from "./builtin-auth-adapter";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Choosing where rate-limit counts live, from configuration and environment.
|
|
3
|
+
*
|
|
4
|
+
* Pure, and separate from both stores, because the decision is the risk. Getting
|
|
5
|
+
* it wrong in either direction is silent: a deployment that wanted shared counts
|
|
6
|
+
* and got per-process ones enforces N times its limit, and a single-container
|
|
7
|
+
* deployment that got shared ones pays a database round trip on every request
|
|
8
|
+
* for a count nobody is sharing with.
|
|
9
|
+
*
|
|
10
|
+
* ## Why memory is still the default
|
|
11
|
+
*
|
|
12
|
+
* The same reason `role.ts` gives for having no channel-bus refusal: **a process
|
|
13
|
+
* cannot read its own replica count.** A single container scaled to three needs
|
|
14
|
+
* a shared store exactly as much as a split deployment does, and neither is
|
|
15
|
+
* visible from inside one process. So this does not guess. It stays on the
|
|
16
|
+
* default that costs nothing, and the deployment that knows it has peers says
|
|
17
|
+
* so — with one environment variable, which is the same sentence whether it is
|
|
18
|
+
* written in a compose file, a Helm value, or a platform-pinned pod spec.
|
|
19
|
+
*/
|
|
20
|
+
/** Where a deployment keeps its rate-limit counts. */
|
|
21
|
+
export type RateLimitStoreKind = "memory" | "sql";
|
|
22
|
+
/** The environment this module reads. Narrowed so it can be called with a literal. */
|
|
23
|
+
export interface RateLimitStoreEnv {
|
|
24
|
+
/** `memory` (default) or `sql`. */
|
|
25
|
+
REBASE_RATE_LIMIT_STORE?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A refusal to boot, phrased as the variable to change.
|
|
29
|
+
*
|
|
30
|
+
* Its own class so `bootFromBundle` can present it as a configuration problem
|
|
31
|
+
* rather than a crash, matching `RoleConfigurationError`.
|
|
32
|
+
*/
|
|
33
|
+
export declare class RateLimitStoreConfigurationError extends Error {
|
|
34
|
+
readonly hint?: string | undefined;
|
|
35
|
+
constructor(message: string, hint?: string | undefined);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Which store this process should build.
|
|
39
|
+
*
|
|
40
|
+
* A misspelled value **refuses to boot** rather than falling back to memory.
|
|
41
|
+
* Every other outcome here is silent — the whole failure mode of this setting is
|
|
42
|
+
* that nothing looks wrong — so the one moment it can be caught is the moment it
|
|
43
|
+
* is read. `REBASE_RATE_LIMIT_STORE=postgres` is the obvious thing to type and
|
|
44
|
+
* would otherwise be a per-replica limit wearing the shape of a shared one.
|
|
45
|
+
*
|
|
46
|
+
* A blank or whitespace-only value is *unset*, not invalid:
|
|
47
|
+
* `REBASE_RATE_LIMIT_STORE=${SOMETHING}` with `SOMETHING` undefined is the
|
|
48
|
+
* ordinary way to write a compose file, and it is how the platform neutralises a
|
|
49
|
+
* tenant's own variable.
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveRateLimitStoreKind(env: RateLimitStoreEnv): RateLimitStoreKind;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A rate-limit store every process in a deployment shares.
|
|
3
|
+
*
|
|
4
|
+
* {@link MemoryRateLimitStore} counts in one process's memory, so N replicas
|
|
5
|
+
* enforce N times the limit between them. That is fine for the single container
|
|
6
|
+
* almost every deployment runs, and wrong the moment a deployment splits — an
|
|
7
|
+
* `api` pod and a `functions` pod each grant a caller the full allowance, and
|
|
8
|
+
* nothing anywhere reports it. A limit that silently lets through 3× the traffic
|
|
9
|
+
* is worse than no limit, because it is believed.
|
|
10
|
+
*
|
|
11
|
+
* So this keeps the counts in Postgres, on the same seam the cron store already
|
|
12
|
+
* uses: `DataDriver.admin.executeSql`. No new dependency, no new service, and it
|
|
13
|
+
* degrades on a driver with no SQL (Mongo) rather than failing to boot.
|
|
14
|
+
*
|
|
15
|
+
* ## Why two buckets rather than one row per hit
|
|
16
|
+
*
|
|
17
|
+
* The memory store keeps a timestamp per hit, which is exact and costs a row per
|
|
18
|
+
* request here. This keeps **one row per key per window** and approximates the
|
|
19
|
+
* same sliding behaviour by weighting the previous window by how much of it is
|
|
20
|
+
* still in view:
|
|
21
|
+
*
|
|
22
|
+
* estimate = current + previous × (1 − elapsed ÷ window)
|
|
23
|
+
*
|
|
24
|
+
* At the moment a window rolls over the previous bucket counts fully and decays
|
|
25
|
+
* to nothing by the end, so the boundary burst a fixed window allows — spend the
|
|
26
|
+
* whole allowance in the last second, spend it again in the first second of the
|
|
27
|
+
* next — is bounded rather than free. This is the standard approximation and its
|
|
28
|
+
* error is at most one window's distribution skew, which for a floor against
|
|
29
|
+
* runaway clients is not a number worth a row per request.
|
|
30
|
+
*
|
|
31
|
+
* ## Why a denied hit is given back
|
|
32
|
+
*
|
|
33
|
+
* `INSERT … ON CONFLICT DO UPDATE … RETURNING` is atomic per row, which is what
|
|
34
|
+
* the interface demands: two concurrent hits cannot both read `limit - 1` and
|
|
35
|
+
* both be allowed. But it means the count is incremented *before* the decision,
|
|
36
|
+
* so a request that turns out to be over the limit has already been charged for.
|
|
37
|
+
*
|
|
38
|
+
* The memory store does not charge denied hits, and a store whose semantics
|
|
39
|
+
* differ from the default is a store nobody can safely switch to — the limit
|
|
40
|
+
* would tighten for reasons an operator would attribute to their traffic. So a
|
|
41
|
+
* denial issues a compensating decrement. That is a second statement, on the
|
|
42
|
+
* path that is by definition the rare one; the allowed path stays at one.
|
|
43
|
+
*/
|
|
44
|
+
import type { DataDriver } from "@rebasepro/types";
|
|
45
|
+
import type { RateLimitStore } from "./rate-limit-store.js";
|
|
46
|
+
export interface SqlRateLimitStoreOptions {
|
|
47
|
+
/** How often to delete expired buckets. Defaults to 5 minutes. */
|
|
48
|
+
sweepMs?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Clock, for tests. Driving the window directly beats sleeping through half
|
|
51
|
+
* of one — a real sleep overruns on a loaded machine and takes the hits it
|
|
52
|
+
* expected to still be in the window with it.
|
|
53
|
+
*/
|
|
54
|
+
now?: () => number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build the store, or `undefined` if this driver cannot back it.
|
|
58
|
+
*
|
|
59
|
+
* Returning `undefined` rather than throwing matches `createCronStore`: a
|
|
60
|
+
* deployment on a driver with no SQL admin should degrade to per-process counts
|
|
61
|
+
* with a warning, not refuse to serve.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createSqlRateLimitStore(driver: DataDriver, options?: SqlRateLimitStoreOptions): RateLimitStore | undefined;
|
|
@@ -2,7 +2,7 @@ import { createRequire as __createRequire } from "module";
|
|
|
2
2
|
import process from "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
4
|
import { i as __toESM, n as __exportAll } from "./rolldown-runtime-DSJWtz9O.js";
|
|
5
|
-
import { C as ListLimitError, D as isAnonymousUid, E as ANONYMOUS_USER_ID, T as resolveClientListLimit, a as deserializeLogicalCondition, i as deserializeFilter, r as UnknownFilterOperatorError } from "./src-
|
|
5
|
+
import { C as ListLimitError, D as isAnonymousUid, E as ANONYMOUS_USER_ID, T as resolveClientListLimit, a as deserializeLogicalCondition, i as deserializeFilter, r as UnknownFilterOperatorError } from "./src-Cx24hMDv.js";
|
|
6
6
|
import "./src-Cz9nMgUR.js";
|
|
7
7
|
import { n as createDdlBootstrapper, o as revokeInternalTableSql, s as isSQLAdmin } from "./ddl-bootstrap-Cywoj8Ta.js";
|
|
8
8
|
import { S as canonicalStorageId, T as require_jsonwebtoken, _ as verifyMfaPendingToken, a as generateMfaPendingToken, c as getJwks, d as hasAsymmetricSigningKey, f as hashRefreshToken, g as verifyDownloadToken, h as verifyAccessToken, i as generateDownloadToken, l as getRefreshTokenExpiry, n as configureJwt, o as generateRefreshToken, p as isJwtConfigured, r as generateAccessToken, s as getAccessTokenExpiry, t as MAX_COOKIE_AGE_MS, u as getRefreshTokenTtlMs, w as tryCanonicalStorageKey } from "./jwt-VJyXTdQQ.js";
|
|
@@ -10036,6 +10036,198 @@ function createSpotifyProvider(config) {
|
|
|
10036
10036
|
};
|
|
10037
10037
|
}
|
|
10038
10038
|
//#endregion
|
|
10039
|
+
//#region src/auth/sql-rate-limit-store.ts
|
|
10040
|
+
var SCHEMA = "rebase";
|
|
10041
|
+
var TABLE_NAME = "rate_limit_hits";
|
|
10042
|
+
var TABLE$1 = `${SCHEMA}.${TABLE_NAME}`;
|
|
10043
|
+
/** How often expired buckets are deleted. */
|
|
10044
|
+
var DEFAULT_SWEEP_MS = 300 * 1e3;
|
|
10045
|
+
/**
|
|
10046
|
+
* Build the store, or `undefined` if this driver cannot back it.
|
|
10047
|
+
*
|
|
10048
|
+
* Returning `undefined` rather than throwing matches `createCronStore`: a
|
|
10049
|
+
* deployment on a driver with no SQL admin should degrade to per-process counts
|
|
10050
|
+
* with a warning, not refuse to serve.
|
|
10051
|
+
*/
|
|
10052
|
+
function createSqlRateLimitStore(driver, options = {}) {
|
|
10053
|
+
const admin = driver.admin;
|
|
10054
|
+
if (!isSQLAdmin(admin)) {
|
|
10055
|
+
logger.warn("⚠️ [rate-limit] The configured driver has no SQL admin, so rate-limit counts cannot be shared between processes. Falling back to this process's memory — each replica will enforce the limit independently.");
|
|
10056
|
+
return;
|
|
10057
|
+
}
|
|
10058
|
+
const exec = (sqlText, options) => admin.executeSql(sqlText, options?.params ? { params: options.params } : void 0);
|
|
10059
|
+
const ddl = createDdlBootstrapper(exec, "rate-limit-store");
|
|
10060
|
+
const now = options.now ?? Date.now;
|
|
10061
|
+
const sweepMs = options.sweepMs ?? DEFAULT_SWEEP_MS;
|
|
10062
|
+
let sweepTimer;
|
|
10063
|
+
let ready;
|
|
10064
|
+
/**
|
|
10065
|
+
* Create the table once, and remember whether it worked.
|
|
10066
|
+
*
|
|
10067
|
+
* Lazily rather than at boot because the store is constructed while routes
|
|
10068
|
+
* are being mounted, and an `await` there would put a database round trip on
|
|
10069
|
+
* the boot path of every deployment — including the ones that never take a
|
|
10070
|
+
* request. The first request pays for it.
|
|
10071
|
+
*
|
|
10072
|
+
* The result is cached even on failure: a store that cannot create its table
|
|
10073
|
+
* must not retry the DDL on every request, which would turn one broken
|
|
10074
|
+
* permission into a per-request query storm.
|
|
10075
|
+
*
|
|
10076
|
+
* Readiness is decided by reading the table, never by the DDL not throwing —
|
|
10077
|
+
* `ensureObject` routes through `step`, which logs a failure and returns
|
|
10078
|
+
* normally. Keying on the absence of an exception would report a table that
|
|
10079
|
+
* was never created as ready, and every later request would fall into the
|
|
10080
|
+
* per-request error path instead of the one-time fallback.
|
|
10081
|
+
*/
|
|
10082
|
+
async function ensureReady() {
|
|
10083
|
+
if (!ready) ready = (async () => {
|
|
10084
|
+
try {
|
|
10085
|
+
await ddl.ensureObject("Creating schema rebase", "CREATE SCHEMA IF NOT EXISTS rebase");
|
|
10086
|
+
await ddl.ensureObject(`Creating ${TABLE$1}`, `
|
|
10087
|
+
CREATE TABLE IF NOT EXISTS ${TABLE$1} (
|
|
10088
|
+
key TEXT NOT NULL,
|
|
10089
|
+
bucket BIGINT NOT NULL,
|
|
10090
|
+
hits INTEGER NOT NULL DEFAULT 0,
|
|
10091
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
10092
|
+
PRIMARY KEY (key, bucket)
|
|
10093
|
+
)
|
|
10094
|
+
`);
|
|
10095
|
+
await ddl.ensureObject("Creating idx_rate_limit_hits_expiry", `
|
|
10096
|
+
CREATE INDEX IF NOT EXISTS idx_rate_limit_hits_expiry
|
|
10097
|
+
ON ${TABLE$1}(expires_at)
|
|
10098
|
+
`);
|
|
10099
|
+
const readable = await ddl.isReadable(TABLE$1);
|
|
10100
|
+
if (readable) await ddl.step("Revoking end-user access to rate_limit_hits", () => exec(revokeInternalTableSql(SCHEMA, TABLE_NAME)));
|
|
10101
|
+
else logger.error("[rate-limit] The shared rate-limit table could not be created, so counts stay per-process. Every replica will enforce the limit independently.");
|
|
10102
|
+
return readable;
|
|
10103
|
+
} catch (err) {
|
|
10104
|
+
logger.error("[rate-limit] Could not prepare the shared rate-limit table. Falling back to per-process counts for the life of this process.", { error: err instanceof Error ? err.message : String(err) });
|
|
10105
|
+
return false;
|
|
10106
|
+
}
|
|
10107
|
+
})();
|
|
10108
|
+
return ready;
|
|
10109
|
+
}
|
|
10110
|
+
function startSweeping() {
|
|
10111
|
+
if (sweepTimer) return;
|
|
10112
|
+
sweepTimer = setInterval(() => {
|
|
10113
|
+
exec(`DELETE FROM ${TABLE$1} WHERE expires_at < now()`).catch((err) => {
|
|
10114
|
+
logger.debug("[rate-limit] Bucket sweep failed", { error: err instanceof Error ? err.message : String(err) });
|
|
10115
|
+
});
|
|
10116
|
+
}, sweepMs);
|
|
10117
|
+
sweepTimer.unref?.();
|
|
10118
|
+
}
|
|
10119
|
+
return {
|
|
10120
|
+
async hit(key, windowMs, limit) {
|
|
10121
|
+
if (!await ensureReady()) return {
|
|
10122
|
+
allowed: true,
|
|
10123
|
+
remaining: limit,
|
|
10124
|
+
retryAfterMs: 0
|
|
10125
|
+
};
|
|
10126
|
+
startSweeping();
|
|
10127
|
+
const at = now();
|
|
10128
|
+
const bucket = Math.floor(at / windowMs);
|
|
10129
|
+
const carry = 1 - (at - bucket * windowMs) / windowMs;
|
|
10130
|
+
const expiresAt = new Date((bucket + 2) * windowMs).toISOString();
|
|
10131
|
+
let current;
|
|
10132
|
+
let previous;
|
|
10133
|
+
try {
|
|
10134
|
+
const row = firstRow(await exec(`WITH bumped AS (
|
|
10135
|
+
INSERT INTO ${TABLE$1} AS r (key, bucket, hits, expires_at)
|
|
10136
|
+
VALUES ($1, $2, 1, $3)
|
|
10137
|
+
ON CONFLICT (key, bucket)
|
|
10138
|
+
DO UPDATE SET hits = r.hits + 1, expires_at = EXCLUDED.expires_at
|
|
10139
|
+
RETURNING hits
|
|
10140
|
+
)
|
|
10141
|
+
SELECT
|
|
10142
|
+
(SELECT hits FROM bumped) AS current,
|
|
10143
|
+
COALESCE((SELECT hits FROM ${TABLE$1} WHERE key = $1 AND bucket = $4), 0) AS previous`, { params: [
|
|
10144
|
+
key,
|
|
10145
|
+
bucket,
|
|
10146
|
+
expiresAt,
|
|
10147
|
+
bucket - 1
|
|
10148
|
+
] }));
|
|
10149
|
+
current = Number(row?.current ?? 1);
|
|
10150
|
+
previous = Number(row?.previous ?? 0);
|
|
10151
|
+
} catch (err) {
|
|
10152
|
+
logger.debug("[rate-limit] Shared count unavailable for this request", { error: err instanceof Error ? err.message : String(err) });
|
|
10153
|
+
return {
|
|
10154
|
+
allowed: true,
|
|
10155
|
+
remaining: limit,
|
|
10156
|
+
retryAfterMs: 0
|
|
10157
|
+
};
|
|
10158
|
+
}
|
|
10159
|
+
const estimate = current + previous * carry;
|
|
10160
|
+
if (estimate > limit) {
|
|
10161
|
+
exec(`UPDATE ${TABLE$1} SET hits = GREATEST(hits - 1, 0) WHERE key = $1 AND bucket = $2`, { params: [key, bucket] }).catch(() => {});
|
|
10162
|
+
return {
|
|
10163
|
+
allowed: false,
|
|
10164
|
+
remaining: 0,
|
|
10165
|
+
retryAfterMs: Math.max(0, (bucket + 1) * windowMs - at)
|
|
10166
|
+
};
|
|
10167
|
+
}
|
|
10168
|
+
return {
|
|
10169
|
+
allowed: true,
|
|
10170
|
+
remaining: Math.max(0, Math.floor(limit - estimate)),
|
|
10171
|
+
retryAfterMs: 0
|
|
10172
|
+
};
|
|
10173
|
+
},
|
|
10174
|
+
dispose() {
|
|
10175
|
+
if (sweepTimer) clearInterval(sweepTimer);
|
|
10176
|
+
sweepTimer = void 0;
|
|
10177
|
+
}
|
|
10178
|
+
};
|
|
10179
|
+
}
|
|
10180
|
+
/**
|
|
10181
|
+
* The first row of whatever shape this driver returns.
|
|
10182
|
+
*
|
|
10183
|
+
* `SqlExec` declares an array of rows, but drivers in this repository have also
|
|
10184
|
+
* returned a `{ rows }` envelope through the same escape hatch. Reading one
|
|
10185
|
+
* shape only is how a store silently counts nothing and reports every caller as
|
|
10186
|
+
* being on their first request — a limiter that never limits, with no error
|
|
10187
|
+
* anywhere. Tolerating both costs one line.
|
|
10188
|
+
*/
|
|
10189
|
+
function firstRow(result) {
|
|
10190
|
+
if (Array.isArray(result)) return result[0];
|
|
10191
|
+
const rows = result?.rows;
|
|
10192
|
+
return Array.isArray(rows) ? rows[0] : void 0;
|
|
10193
|
+
}
|
|
10194
|
+
//#endregion
|
|
10195
|
+
//#region src/auth/resolve-rate-limit-store.ts
|
|
10196
|
+
/**
|
|
10197
|
+
* A refusal to boot, phrased as the variable to change.
|
|
10198
|
+
*
|
|
10199
|
+
* Its own class so `bootFromBundle` can present it as a configuration problem
|
|
10200
|
+
* rather than a crash, matching `RoleConfigurationError`.
|
|
10201
|
+
*/
|
|
10202
|
+
var RateLimitStoreConfigurationError = class extends Error {
|
|
10203
|
+
hint;
|
|
10204
|
+
constructor(message, hint) {
|
|
10205
|
+
super(message);
|
|
10206
|
+
this.hint = hint;
|
|
10207
|
+
this.name = "RateLimitStoreConfigurationError";
|
|
10208
|
+
}
|
|
10209
|
+
};
|
|
10210
|
+
/**
|
|
10211
|
+
* Which store this process should build.
|
|
10212
|
+
*
|
|
10213
|
+
* A misspelled value **refuses to boot** rather than falling back to memory.
|
|
10214
|
+
* Every other outcome here is silent — the whole failure mode of this setting is
|
|
10215
|
+
* that nothing looks wrong — so the one moment it can be caught is the moment it
|
|
10216
|
+
* is read. `REBASE_RATE_LIMIT_STORE=postgres` is the obvious thing to type and
|
|
10217
|
+
* would otherwise be a per-replica limit wearing the shape of a shared one.
|
|
10218
|
+
*
|
|
10219
|
+
* A blank or whitespace-only value is *unset*, not invalid:
|
|
10220
|
+
* `REBASE_RATE_LIMIT_STORE=${SOMETHING}` with `SOMETHING` undefined is the
|
|
10221
|
+
* ordinary way to write a compose file, and it is how the platform neutralises a
|
|
10222
|
+
* tenant's own variable.
|
|
10223
|
+
*/
|
|
10224
|
+
function resolveRateLimitStoreKind(env) {
|
|
10225
|
+
const raw = (env.REBASE_RATE_LIMIT_STORE ?? "").trim().toLowerCase();
|
|
10226
|
+
if (raw === "") return "memory";
|
|
10227
|
+
if (raw === "memory" || raw === "sql") return raw;
|
|
10228
|
+
throw new RateLimitStoreConfigurationError(`REBASE_RATE_LIMIT_STORE="${raw}" is not a rate-limit store.`, "Use \"sql\" to share counts across processes, or \"memory\" (the default) to keep them in this process. A deployment running more than one replica or more than one role wants \"sql\" — otherwise each process enforces the whole limit by itself.");
|
|
10229
|
+
}
|
|
10230
|
+
//#endregion
|
|
10039
10231
|
//#region src/auth/api-keys/api-key-store.ts
|
|
10040
10232
|
/**
|
|
10041
10233
|
* Database operations for Service API Keys.
|
|
@@ -10433,6 +10625,7 @@ function createCustomAuthAdapter(options) {
|
|
|
10433
10625
|
//#region src/auth/index.ts
|
|
10434
10626
|
var auth_exports = /* @__PURE__ */ __exportAll({
|
|
10435
10627
|
MemoryRateLimitStore: () => MemoryRateLimitStore,
|
|
10628
|
+
RateLimitStoreConfigurationError: () => RateLimitStoreConfigurationError,
|
|
10436
10629
|
apiKeyKeyGenerator: () => apiKeyKeyGenerator,
|
|
10437
10630
|
configureJwt: () => configureJwt,
|
|
10438
10631
|
createAdapterAuthMiddleware: () => createAdapterAuthMiddleware,
|
|
@@ -10457,6 +10650,7 @@ var auth_exports = /* @__PURE__ */ __exportAll({
|
|
|
10457
10650
|
createResetPasswordRoute: () => createResetPasswordRoute,
|
|
10458
10651
|
createSlackProvider: () => createSlackProvider,
|
|
10459
10652
|
createSpotifyProvider: () => createSpotifyProvider,
|
|
10653
|
+
createSqlRateLimitStore: () => createSqlRateLimitStore,
|
|
10460
10654
|
createTwitterProvider: () => createTwitterProvider,
|
|
10461
10655
|
decideOAuthAutoLink: () => decideOAuthAutoLink,
|
|
10462
10656
|
defaultAuthLimiter: () => defaultAuthLimiter,
|
|
@@ -10490,6 +10684,7 @@ var auth_exports = /* @__PURE__ */ __exportAll({
|
|
|
10490
10684
|
requireAdmin: () => requireAdmin,
|
|
10491
10685
|
requireAuth: () => requireAuth,
|
|
10492
10686
|
resolveAuthHooks: () => resolveAuthHooks,
|
|
10687
|
+
resolveRateLimitStoreKind: () => resolveRateLimitStoreKind,
|
|
10493
10688
|
safeCompare: () => safeCompare,
|
|
10494
10689
|
strictAuthLimiter: () => strictAuthLimiter,
|
|
10495
10690
|
tryVerifyOidcIdToken: () => tryVerifyOidcIdToken,
|
|
@@ -10501,6 +10696,6 @@ var auth_exports = /* @__PURE__ */ __exportAll({
|
|
|
10501
10696
|
verifyPassword: () => verifyPassword
|
|
10502
10697
|
});
|
|
10503
10698
|
//#endregion
|
|
10504
|
-
export {
|
|
10699
|
+
export { createAuthMiddleware as $, _coercedNumber as A, validatePasswordStrength as B, providerVerifiedEmail as C, resolveListLimitParam as Ct, _enum as D, ZodNumber as E, createEmailService as F, getPasswordResetTemplate as G, generateSecurePassword as H, assertEmailLinkBases as I, RawHtml as J, getUserInvitationTemplate as K, resolveEmailLinkBase as L, createDataRateLimiter as M, MemoryRateLimitStore as N, object as O, SMTPEmailService as P, createAdapterAuthMiddleware as Q, resolveAuthHooks as R, pkceTokenParams as S, parseQueryOptions as St, createBuiltinAuthAdapter as T, isPublicStoragePath as Tt, getEmailVerificationTemplate as U, verifyPassword as V, getMagicLinkTemplate as W, html as X, escapeHtml as Y, raw as Z, verifyOidcIdToken as _, httpMethodToOperation as _t, resolveRateLimitStoreKind as a, queryTokenAuth as at, createGoogleProvider as b, parseAggregateSelect as bt, createSlackProvider as c, createApiKeyPreAuth as ct, createDiscordProvider as d, isApiKeyToken as dt, createRequireAuth as et, createTwitterProvider as f, validateApiKey as ft, tryVerifyOidcIdToken as g, scopeDataDriver as gt, createMicrosoftProvider as h, SERVICE_IDENTITY as ht, createApiKeyStore as i, publicObjectAuth as it, DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT as j, string as k, createBitbucketProvider as l, createFunctionApiKeyGuard as lt, createAppleProvider as m, safeCompare as mt, createCustomAuthAdapter as n, fileTokenAuth as nt, createSqlRateLimitStore as o, requireAdmin as ot, createFacebookProvider as p, extractBearerToken as pt, getWelcomeEmailTemplate as q, createApiKeyRoutes as r, optionalAuth as rt, createSpotifyProvider as s, requireAuth as st, auth_exports as t, extractUserFromToken as tt, createGitLabProvider as u, createStorageApiKeyGuard as ut, createGitHubProvider as v, isOperationAllowed as vt, createJwksRoutes as w, PUBLIC_STORAGE_PREFIX as wt, oauthCodeFlowSchema as x, parseGroupBy as xt, createLinkedinProvider as y, orderByEntriesToTuples as yt, hashPassword as z };
|
|
10505
10700
|
|
|
10506
|
-
//# sourceMappingURL=auth-
|
|
10701
|
+
//# sourceMappingURL=auth-CIWux0Ol.js.map
|