@rebasepro/server 0.9.1-canary.fd3754b → 0.10.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/dist/api/errors.d.ts +16 -1
- package/dist/api/rest/api-generator.d.ts +13 -1
- package/dist/api/rest/write-validation.d.ts +22 -0
- package/dist/api/types.d.ts +2 -2
- package/dist/auth/admin-users-route.d.ts +3 -3
- package/dist/auth/api-keys/api-key-middleware.d.ts +60 -1
- package/dist/auth/api-keys/api-key-permission-guard.d.ts +33 -0
- package/dist/auth/api-keys/api-key-types.d.ts +21 -8
- package/dist/auth/api-keys/index.d.ts +2 -2
- package/dist/auth/auth-hooks.d.ts +7 -7
- package/dist/auth/collection-callback-warning.d.ts +21 -0
- package/dist/auth/index.d.ts +4 -1
- package/dist/auth/interfaces.d.ts +28 -28
- package/dist/auth/jwt.d.ts +20 -4
- package/dist/auth/magic-link-routes.d.ts +2 -2
- package/dist/auth/mfa-routes.d.ts +1 -1
- package/dist/auth/middleware.d.ts +24 -3
- package/dist/auth/rate-limit-store.d.ts +49 -0
- package/dist/auth/rate-limiter.d.ts +68 -6
- package/dist/auth/reset-password-admin.d.ts +1 -1
- package/dist/auth/session-routes.d.ts +2 -2
- package/dist/cron/cron-store.d.ts +18 -0
- package/dist/env.d.ts +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.es.js +2124 -505
- package/dist/index.es.js.map +1 -1
- package/dist/init/middlewares.d.ts +1 -0
- package/dist/init.d.ts +62 -0
- package/dist/{jwt-BwIn8xmk.js → jwt-B3zjddCa.js} +177 -9
- package/dist/jwt-B3zjddCa.js.map +1 -0
- package/dist/{openapi-generator-Z9oYWLf_.js → openapi-generator-DqSIwNLV.js} +8 -2
- package/dist/openapi-generator-DqSIwNLV.js.map +1 -0
- package/dist/{src-B4OLmNVa.js → src-CsHhSKbi.js} +60 -2
- package/dist/src-CsHhSKbi.js.map +1 -0
- package/dist/storage/LocalStorageController.d.ts +19 -2
- package/dist/storage/routes.d.ts +8 -1
- package/dist/storage/tus-handler.d.ts +21 -1
- package/dist/storage/types.d.ts +33 -0
- package/dist/utils/compression.d.ts +16 -0
- package/dist/utils/sql.d.ts +2 -2
- package/package.json +12 -11
- package/dist/index.umd.js +0 -48745
- package/dist/index.umd.js.map +0 -1
- package/dist/jwt-BwIn8xmk.js.map +0 -1
- package/dist/ms-DnYXB-Wd.js +0 -162
- package/dist/ms-DnYXB-Wd.js.map +0 -1
- package/dist/openapi-generator-Z9oYWLf_.js.map +0 -1
- package/dist/src-B4OLmNVa.js.map +0 -1
- package/dist/src-B9tjYmqP.js +0 -24598
- package/dist/src-B9tjYmqP.js.map +0 -1
package/dist/api/errors.d.ts
CHANGED
|
@@ -9,9 +9,24 @@ export declare class ApiError extends Error {
|
|
|
9
9
|
readonly statusCode: number;
|
|
10
10
|
readonly code: string;
|
|
11
11
|
readonly details?: unknown;
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Whether this outcome is a routine part of normal operation rather than
|
|
14
|
+
* something an operator should look at. Expected errors log at debug; every
|
|
15
|
+
* other operational error logs at warn.
|
|
16
|
+
*
|
|
17
|
+
* The motivating case is `POST /auth/refresh` with no session: clients
|
|
18
|
+
* refresh on page load before they know whether one exists, so every
|
|
19
|
+
* anonymous page view is a 401 — correct, and not worth a warning line.
|
|
20
|
+
*/
|
|
21
|
+
readonly expected: boolean;
|
|
22
|
+
constructor(statusCode: number, code: string, message: string, details?: unknown, expected?: boolean);
|
|
13
23
|
static badRequest(message: string, code?: string, details?: unknown): ApiError;
|
|
14
24
|
static unauthorized(message: string, code?: string): ApiError;
|
|
25
|
+
/**
|
|
26
|
+
* A 401 that is a normal outcome, not an incident — logged at debug.
|
|
27
|
+
* See {@link ApiError.expected}.
|
|
28
|
+
*/
|
|
29
|
+
static unauthenticated(message: string, code?: string): ApiError;
|
|
15
30
|
static forbidden(message: string, code?: string): ApiError;
|
|
16
31
|
static notFound(message: string, code?: string): ApiError;
|
|
17
32
|
static conflict(message: string, code?: string): ApiError;
|
|
@@ -5,12 +5,15 @@ import { HonoEnv } from "../types";
|
|
|
5
5
|
* Lightweight REST API generator that leverages existing Rebase DataDriver.
|
|
6
6
|
* Supports `include` query parameter for eager-loading relations via Drizzle.
|
|
7
7
|
*/
|
|
8
|
+
/** Rows accepted by a single POST /<collection>/bulk. See `maxBulkRows`. */
|
|
9
|
+
export declare const DEFAULT_MAX_BULK_ROWS = 1000;
|
|
8
10
|
export declare class RestApiGenerator {
|
|
9
11
|
private collections;
|
|
10
12
|
private router;
|
|
11
13
|
private driver;
|
|
14
|
+
private maxBulkRows;
|
|
12
15
|
private authAdapter?;
|
|
13
|
-
constructor(collections: CollectionConfig[], driver: DataDriver, authAdapter?: AuthAdapter);
|
|
16
|
+
constructor(collections: CollectionConfig[], driver: DataDriver, authAdapter?: AuthAdapter, maxBulkRows?: number);
|
|
14
17
|
/**
|
|
15
18
|
* Generate REST routes using existing DataDriver
|
|
16
19
|
*/
|
|
@@ -21,6 +24,15 @@ export declare class RestApiGenerator {
|
|
|
21
24
|
* No-ops if the request is not authenticated via an API key.
|
|
22
25
|
*/
|
|
23
26
|
private enforceApiKeyPermission;
|
|
27
|
+
/**
|
|
28
|
+
* API key permission check for nested paths. The operation targets the
|
|
29
|
+
* LAST collection in the path (e.g. "posts" for /authors/1/posts), so
|
|
30
|
+
* that is the slug the key must hold permission for — checking the
|
|
31
|
+
* parent instead would let a key scoped to "authors" write "posts".
|
|
32
|
+
* `parseSubPath` always yields a collectionPath ending in a collection
|
|
33
|
+
* slug, never an id.
|
|
34
|
+
*/
|
|
35
|
+
private enforceSubcollectionApiKeyPermission;
|
|
24
36
|
/**
|
|
25
37
|
* Get the request-scoped driver. Throws if none is set — never falls
|
|
26
38
|
* back to the unscoped `this.driver` to avoid bypassing RLS/auth.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { CollectionConfig } from "@rebasepro/types";
|
|
2
|
+
/**
|
|
3
|
+
* Reject a write naming a field the collection does not have.
|
|
4
|
+
*
|
|
5
|
+
* Unknown keys used to travel all the way into the INSERT, where Postgres
|
|
6
|
+
* rejected them — so a typo came back as `column "titel" does not exist`,
|
|
7
|
+
* phrased by the database, from a stack the caller cannot see, and only if the
|
|
8
|
+
* column really was absent. It is a request problem and belongs in a 400.
|
|
9
|
+
*
|
|
10
|
+
* What counts as known:
|
|
11
|
+
* - a declared property (for an introspected BaaS collection these *are* the
|
|
12
|
+
* columns, so the set is exact);
|
|
13
|
+
* - the foreign-key column behind an owning relation, which callers may write
|
|
14
|
+
* directly instead of through the relation property;
|
|
15
|
+
* - anything named in `options.extraKnownFields` — for an auth collection the
|
|
16
|
+
* credential keys the auth adapter consumes before a row is ever built;
|
|
17
|
+
* - nothing else. `id` in particular is not automatically known — see below.
|
|
18
|
+
*/
|
|
19
|
+
export declare function assertKnownWriteFields(values: Record<string, unknown>, collection: CollectionConfig, options?: {
|
|
20
|
+
rowIndex?: number;
|
|
21
|
+
extraKnownFields?: readonly string[];
|
|
22
|
+
}): void;
|
package/dist/api/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { VectorSearchParams, LogicalCondition, FilterValues } from "@rebasepro/types";
|
|
2
|
-
import { AuthResult } from "../auth/middleware";
|
|
2
|
+
import type { AuthResult } from "../auth/middleware";
|
|
3
3
|
import { DataDriver } from "@rebasepro/types";
|
|
4
4
|
import type { ApiKeyMasked } from "../auth/api-keys/api-key-types";
|
|
5
5
|
/**
|
|
@@ -9,7 +9,7 @@ import type { ApiKeyMasked } from "../auth/api-keys/api-key-types";
|
|
|
9
9
|
export type HonoEnv = {
|
|
10
10
|
Variables: {
|
|
11
11
|
user?: AuthResult | {
|
|
12
|
-
|
|
12
|
+
uid?: string;
|
|
13
13
|
roles?: string[];
|
|
14
14
|
};
|
|
15
15
|
driver?: DataDriver;
|
|
@@ -10,9 +10,18 @@
|
|
|
10
10
|
* 4. Sets `c.set("user", ...)` and `c.set("apiKey", ...)` for downstream use
|
|
11
11
|
* 5. Scopes the DataDriver via `withAuth()` using the API key's service identity
|
|
12
12
|
*
|
|
13
|
+
* Authorization is double-gated for API keys: the key's own permission list
|
|
14
|
+
* (checked by the REST generator) is one ceiling, and Postgres RLS is another,
|
|
15
|
+
* independent one — `withAuth()` runs API-key requests as the restricted
|
|
16
|
+
* `rebase_user` role like any other caller. An `admin` key passes RLS via the
|
|
17
|
+
* injected `default_admin` policies; a non-admin key (roles `["service"]`,
|
|
18
|
+
* uid `api-key:<id>`) only sees rows that a policy explicitly grants to the
|
|
19
|
+
* `service` role or to the public. Owner-style policies
|
|
20
|
+
* (`owner_id = auth.uid()`) never match an API key's synthetic uid.
|
|
21
|
+
*
|
|
13
22
|
* @module
|
|
14
23
|
*/
|
|
15
|
-
import type { Context } from "hono";
|
|
24
|
+
import type { Context, MiddlewareHandler } from "hono";
|
|
16
25
|
import type { DataDriver } from "@rebasepro/types";
|
|
17
26
|
import type { HonoEnv } from "../../api/types";
|
|
18
27
|
import type { ApiKeyStore } from "./api-key-store";
|
|
@@ -37,3 +46,53 @@ export interface ApiKeyAuthOptions {
|
|
|
37
46
|
* `createAuthMiddleware()` when a `rk_` prefixed token is detected.
|
|
38
47
|
*/
|
|
39
48
|
export declare function validateApiKey(c: Context<HonoEnv>, token: string, options: ApiKeyAuthOptions): Promise<Response | true>;
|
|
49
|
+
/**
|
|
50
|
+
* Permission guard for API-key requests to the storage router.
|
|
51
|
+
*
|
|
52
|
+
* Storage previously did not accept API keys at all (`rk_` tokens were
|
|
53
|
+
* misparsed as JWTs and 401'd). Now that the pre-auth middleware
|
|
54
|
+
* authenticates them, this guard decides what they may do: the key needs a
|
|
55
|
+
* `"storage"` permission entry (or the global `"*"` wildcard) covering the
|
|
56
|
+
* operation derived from the HTTP method. Requests not authenticated via an
|
|
57
|
+
* API key pass through to the storage router's own auth gates.
|
|
58
|
+
*
|
|
59
|
+
* TUS resumable-upload routes (`/tus`, `/tus/:id`) are classified as `write`
|
|
60
|
+
* for EVERY method: the protocol's offset check is a GET and its cancel is a
|
|
61
|
+
* DELETE, but both are steps of an upload — a write-scoped key must be able
|
|
62
|
+
* to complete (and abort) its own resumable upload without also holding
|
|
63
|
+
* `read`/`delete` on stored objects.
|
|
64
|
+
*/
|
|
65
|
+
export declare function createStorageApiKeyGuard(): MiddlewareHandler<HonoEnv>;
|
|
66
|
+
/**
|
|
67
|
+
* Permission guard for API-key requests to the custom-functions router.
|
|
68
|
+
*
|
|
69
|
+
* The collection permission guard lives in the REST generator and never sees
|
|
70
|
+
* function routes, so before this middleware existed ANY valid API key —
|
|
71
|
+
* however narrowly scoped — could invoke every custom function. This guard
|
|
72
|
+
* closes that: API-key requests must hold a `"functions"`/`"functions/<name>"`
|
|
73
|
+
* permission entry (or the global `"*"` wildcard) for the derived operation.
|
|
74
|
+
*
|
|
75
|
+
* Non-API-key requests (JWT, service key, anonymous) pass through untouched —
|
|
76
|
+
* functions decide their own auth for those, as before.
|
|
77
|
+
*
|
|
78
|
+
* @param mountPrefix - The path the functions router is mounted at
|
|
79
|
+
* (e.g. `/api/functions`), used to extract the function
|
|
80
|
+
* name from the request path.
|
|
81
|
+
*/
|
|
82
|
+
export declare function createFunctionApiKeyGuard(mountPrefix: string): MiddlewareHandler<HonoEnv>;
|
|
83
|
+
/**
|
|
84
|
+
* Standalone pre-auth middleware for `rk_` bearer tokens.
|
|
85
|
+
*
|
|
86
|
+
* Routers whose auth gate is JWT-based (`requireAuth` / `createRequireAuth` —
|
|
87
|
+
* the admin surfaces: admin users/roles, api-keys management, cron, backups,
|
|
88
|
+
* logs, schema editor) don't know about API keys. Mounting this middleware in
|
|
89
|
+
* front of them authenticates `rk_` tokens and populates the request context;
|
|
90
|
+
* the downstream gates then see the already-resolved user and apply their
|
|
91
|
+
* role checks (`requireAdmin`) as usual — so an `admin: true` key passes and
|
|
92
|
+
* a non-admin key is rejected with 403.
|
|
93
|
+
*
|
|
94
|
+
* Requests without an `rk_` bearer token pass through untouched. An invalid,
|
|
95
|
+
* revoked, or expired `rk_` token is rejected here (401) rather than falling
|
|
96
|
+
* through to be misparsed as a JWT.
|
|
97
|
+
*/
|
|
98
|
+
export declare function createApiKeyPreAuth(options: ApiKeyAuthOptions): MiddlewareHandler<HonoEnv>;
|
|
@@ -30,3 +30,36 @@ export declare function httpMethodToOperation(method: string): ApiKeyOperation;
|
|
|
30
30
|
* @returns `true` if the operation is permitted.
|
|
31
31
|
*/
|
|
32
32
|
export declare function isOperationAllowed(permissions: ApiKeyPermission[], collection: string, operation: ApiKeyOperation): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Check whether the given permissions array allows a storage operation.
|
|
35
|
+
*
|
|
36
|
+
* Storage, like functions, lives outside the collection namespace: an entry
|
|
37
|
+
* with collection `"storage"` grants storage access for the listed
|
|
38
|
+
* operations (GET routes → `read`, upload/folder/tus → `write`,
|
|
39
|
+
* DELETE routes → `delete`), and the global `"*"` wildcard also matches.
|
|
40
|
+
* A collection-scoped key gets no storage access at all.
|
|
41
|
+
*
|
|
42
|
+
* Matching is exactly {@link isOperationAllowed} with `"storage"` as the
|
|
43
|
+
* resource name — delegated so the semantics can never drift.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isStorageAllowed(permissions: ApiKeyPermission[], operation: ApiKeyOperation): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Check whether the given permissions array allows invoking a custom function.
|
|
48
|
+
*
|
|
49
|
+
* Functions live outside the collection namespace, so they have their own
|
|
50
|
+
* resource names in the permission list:
|
|
51
|
+
*
|
|
52
|
+
* - `{ collection: "functions", ... }` grants every function
|
|
53
|
+
* - `{ collection: "functions/<name>", ... }` grants a single function
|
|
54
|
+
* - the global `"*"` wildcard (full-access keys) also matches
|
|
55
|
+
*
|
|
56
|
+
* The operation is derived from the HTTP method as usual (GET → read,
|
|
57
|
+
* POST/PUT/PATCH → write, DELETE → delete). The functions index route is
|
|
58
|
+
* checked as `functionName === ""`, which only the `"functions"` and `"*"`
|
|
59
|
+
* entries can grant.
|
|
60
|
+
*
|
|
61
|
+
* A collection-scoped key (e.g. read-only on `events`) therefore can NOT
|
|
62
|
+
* invoke functions — before this guard existed, any valid key could call
|
|
63
|
+
* every function.
|
|
64
|
+
*/
|
|
65
|
+
export declare function isFunctionAllowed(permissions: ApiKeyPermission[], functionName: string, operation: ApiKeyOperation): boolean;
|
|
@@ -10,10 +10,13 @@
|
|
|
10
10
|
/**
|
|
11
11
|
* A single permission entry scoping an API key to a collection and set of operations.
|
|
12
12
|
*
|
|
13
|
-
* Use `"*"` as the collection value to grant access to all collections
|
|
13
|
+
* Use `"*"` as the collection value to grant access to all collections
|
|
14
|
+
* (and all custom functions). Custom functions are addressed with the
|
|
15
|
+
* `functions` namespace: `"functions"` grants every function,
|
|
16
|
+
* `"functions/<name>"` grants a single one.
|
|
14
17
|
*/
|
|
15
18
|
export interface ApiKeyPermission {
|
|
16
|
-
/** Collection slug, or `"*"` for
|
|
19
|
+
/** Collection slug, `"functions"`/`"functions/<name>"`, or `"*"` for everything. */
|
|
17
20
|
collection: string;
|
|
18
21
|
/** Allowed operations on the collection. */
|
|
19
22
|
operations: ("read" | "write" | "delete")[];
|
|
@@ -30,9 +33,19 @@ export interface ApiKey {
|
|
|
30
33
|
/** SHA-256 hash of the full plaintext key. */
|
|
31
34
|
key_hash: string;
|
|
32
35
|
permissions: ApiKeyPermission[];
|
|
33
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* When true, the key is granted the `admin` role: it passes the
|
|
38
|
+
* admin-gated routes (users, roles, cron, backups, logs, API keys) and
|
|
39
|
+
* the RLS `default_admin` policies. Non-admin keys carry only the
|
|
40
|
+
* `service` role — RLS grants them nothing unless a collection policy
|
|
41
|
+
* explicitly names that role.
|
|
42
|
+
*/
|
|
34
43
|
admin: boolean;
|
|
35
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Requests per 15-minute window. `null` means "no per-key override" —
|
|
46
|
+
* the data rate limiter then applies its default API-key limit
|
|
47
|
+
* (1000/window unless configured otherwise), not unlimited.
|
|
48
|
+
*/
|
|
36
49
|
rate_limit: number | null;
|
|
37
50
|
created_by: string;
|
|
38
51
|
created_at: string;
|
|
@@ -50,7 +63,7 @@ export interface ApiKeyMasked {
|
|
|
50
63
|
name: string;
|
|
51
64
|
key_prefix: string;
|
|
52
65
|
permissions: ApiKeyPermission[];
|
|
53
|
-
/** When true, the key is granted the `admin` role (
|
|
66
|
+
/** When true, the key is granted the `admin` role (admin routes + RLS `default_admin` policies). */
|
|
54
67
|
admin: boolean;
|
|
55
68
|
rate_limit: number | null;
|
|
56
69
|
created_by: string;
|
|
@@ -66,9 +79,9 @@ export interface ApiKeyMasked {
|
|
|
66
79
|
export interface CreateApiKeyRequest {
|
|
67
80
|
name: string;
|
|
68
81
|
permissions: ApiKeyPermission[];
|
|
69
|
-
/** When true, grants the `admin` role
|
|
82
|
+
/** When true, grants the `admin` role (admin routes + RLS `default_admin` policies). */
|
|
70
83
|
admin?: boolean;
|
|
71
|
-
/** Requests per 15-minute window. Omit or `null`
|
|
84
|
+
/** Requests per 15-minute window. Omit or `null` to use the server default (1000/window). */
|
|
72
85
|
rate_limit?: number | null;
|
|
73
86
|
/** ISO-8601 expiration timestamp. Omit for no expiration. */
|
|
74
87
|
expires_at?: string | null;
|
|
@@ -80,7 +93,7 @@ export interface CreateApiKeyRequest {
|
|
|
80
93
|
export interface UpdateApiKeyRequest {
|
|
81
94
|
name?: string;
|
|
82
95
|
permissions?: ApiKeyPermission[];
|
|
83
|
-
/** When true, grants the `admin` role
|
|
96
|
+
/** When true, grants the `admin` role (admin routes + RLS `default_admin` policies). */
|
|
84
97
|
admin?: boolean;
|
|
85
98
|
rate_limit?: number | null;
|
|
86
99
|
expires_at?: string | null;
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
export type { ApiKey, ApiKeyMasked, ApiKeyPermission, ApiKeyWithSecret, CreateApiKeyRequest, UpdateApiKeyRequest } from "./api-key-types";
|
|
10
10
|
export { createApiKeyStore } from "./api-key-store";
|
|
11
11
|
export type { ApiKeyStore } from "./api-key-store";
|
|
12
|
-
export { isApiKeyToken, validateApiKey } from "./api-key-middleware";
|
|
12
|
+
export { isApiKeyToken, validateApiKey, createApiKeyPreAuth, createFunctionApiKeyGuard, createStorageApiKeyGuard } from "./api-key-middleware";
|
|
13
13
|
export type { ApiKeyAuthOptions } from "./api-key-middleware";
|
|
14
|
-
export { httpMethodToOperation, isOperationAllowed } from "./api-key-permission-guard";
|
|
14
|
+
export { httpMethodToOperation, isOperationAllowed, isFunctionAllowed, isStorageAllowed } from "./api-key-permission-guard";
|
|
15
15
|
export type { ApiKeyOperation } from "./api-key-permission-guard";
|
|
16
16
|
export { createApiKeyRoutes } from "./api-key-routes";
|
|
17
17
|
export type { ApiKeyRouteOptions } from "./api-key-routes";
|
|
@@ -135,7 +135,7 @@ export interface AuthHooks {
|
|
|
135
135
|
*
|
|
136
136
|
* This is fire-and-forget — errors are logged but do not fail the request.
|
|
137
137
|
*/
|
|
138
|
-
afterLogout?(
|
|
138
|
+
afterLogout?(uid: string): Promise<void>;
|
|
139
139
|
/**
|
|
140
140
|
* Called after successful MFA verification.
|
|
141
141
|
*
|
|
@@ -143,12 +143,12 @@ export interface AuthHooks {
|
|
|
143
143
|
*
|
|
144
144
|
* This is fire-and-forget — errors are logged but do not fail the request.
|
|
145
145
|
*/
|
|
146
|
-
onMfaVerified?(
|
|
146
|
+
onMfaVerified?(uid: string, factorId: string): Promise<void>;
|
|
147
147
|
/**
|
|
148
148
|
* Customize JWT access token claims before signing.
|
|
149
149
|
*
|
|
150
150
|
* Return the modified claims object. The returned claims are merged
|
|
151
|
-
* into the JWT payload alongside standard claims (
|
|
151
|
+
* into the JWT payload alongside standard claims (uid, roles).
|
|
152
152
|
*
|
|
153
153
|
* @param claims - The default claims that would be included.
|
|
154
154
|
* @param user - The authenticated user data.
|
|
@@ -178,14 +178,14 @@ export interface AuthHooks {
|
|
|
178
178
|
*
|
|
179
179
|
* This is fire-and-forget — errors are logged but do not fail the request.
|
|
180
180
|
*/
|
|
181
|
-
onPasswordReset?(
|
|
181
|
+
onPasswordReset?(uid: string): Promise<void>;
|
|
182
182
|
/**
|
|
183
183
|
* Called before a user is deleted.
|
|
184
184
|
*
|
|
185
185
|
* Throw an error to prevent deletion (e.g. for users with active
|
|
186
186
|
* subscriptions, pending transactions, etc.).
|
|
187
187
|
*/
|
|
188
|
-
beforeUserDelete?(
|
|
188
|
+
beforeUserDelete?(uid: string): Promise<void>;
|
|
189
189
|
/**
|
|
190
190
|
* Called after a user is deleted.
|
|
191
191
|
*
|
|
@@ -193,7 +193,7 @@ export interface AuthHooks {
|
|
|
193
193
|
*
|
|
194
194
|
* This is fire-and-forget — errors are logged but do not fail the request.
|
|
195
195
|
*/
|
|
196
|
-
afterUserDelete?(
|
|
196
|
+
afterUserDelete?(uid: string): Promise<void>;
|
|
197
197
|
/**
|
|
198
198
|
* Optional hook to customize or override the default user creation flow via the admin panel/REST API.
|
|
199
199
|
* When provided, this replaces the built-in password generation, hashing, and invitation email logic.
|
|
@@ -212,7 +212,7 @@ export interface AuthHooks {
|
|
|
212
212
|
* Optional hook to customize or override the default password reset flow via the admin panel.
|
|
213
213
|
* When provided, this replaces the built-in password reset token generation, hashing, and email logic.
|
|
214
214
|
*/
|
|
215
|
-
onAdminResetPassword?(
|
|
215
|
+
onAdminResetPassword?(uid: string, ctx: {
|
|
216
216
|
authRepo: AuthRepository;
|
|
217
217
|
emailService?: EmailService;
|
|
218
218
|
emailConfig?: EmailConfig;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Warn when the auth collection hangs data callbacks that auth will not fire.
|
|
3
|
+
*
|
|
4
|
+
* Creating a user through the auth subsystem — registration, OAuth, the admin
|
|
5
|
+
* user routes — writes to the user store directly, because that path owns
|
|
6
|
+
* password hashing, identity rows and its own transaction. It deliberately does
|
|
7
|
+
* not go through the collection save pipeline: a `beforeSave` able to rewrite
|
|
8
|
+
* `password_hash` on its way to the database is a footgun, not a feature, and
|
|
9
|
+
* the auth hooks (`afterUserCreate`, `beforeUserCreate`, …) exist to hang
|
|
10
|
+
* behaviour off those events with the right contract.
|
|
11
|
+
*
|
|
12
|
+
* The cost is a reasonable expectation quietly not being met: someone puts
|
|
13
|
+
* "send the welcome email" in `afterSave` on their users collection, tests it
|
|
14
|
+
* by creating a user in the admin, and it works — because *that* is a
|
|
15
|
+
* collection write. Then a real signup does nothing at all. Which is why this
|
|
16
|
+
* is said at boot, naming the callbacks that will not run.
|
|
17
|
+
*/
|
|
18
|
+
export declare function warnOnAuthCollectionDataCallbacks(collection?: {
|
|
19
|
+
slug?: string;
|
|
20
|
+
callbacks?: Record<string, unknown>;
|
|
21
|
+
}): void;
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -28,7 +28,10 @@ export type { AuthModuleConfig, CookieAuthConfig } from "./routes";
|
|
|
28
28
|
export { mountMagicLinkRoutes } from "./magic-link-routes";
|
|
29
29
|
export { createResetPasswordRoute } from "./reset-password-admin";
|
|
30
30
|
export type { ResetPasswordRouteConfig } from "./reset-password-admin";
|
|
31
|
-
export { createRateLimiter, defaultAuthLimiter, strictAuthLimiter, createApiKeyRateLimiter, apiKeyKeyGenerator } from "./rate-limiter";
|
|
31
|
+
export { createRateLimiter, defaultAuthLimiter, strictAuthLimiter, createApiKeyRateLimiter, createDataRateLimiter, apiKeyKeyGenerator } from "./rate-limiter";
|
|
32
|
+
export type { DataRateLimitConfig } from "./rate-limiter";
|
|
33
|
+
export { MemoryRateLimitStore } from "./rate-limit-store";
|
|
34
|
+
export type { RateLimitStore, RateLimitDecision } from "./rate-limit-store";
|
|
32
35
|
export { createApiKeyStore, createApiKeyRoutes, isApiKeyToken, validateApiKey, httpMethodToOperation, isOperationAllowed } from "./api-keys";
|
|
33
36
|
export type { ApiKey, ApiKeyMasked, ApiKeyPermission, ApiKeyWithSecret, CreateApiKeyRequest, UpdateApiKeyRequest, ApiKeyStore, ApiKeyOperation } from "./api-keys";
|
|
34
37
|
export { createBuiltinAuthAdapter } from "./builtin-auth-adapter";
|
|
@@ -40,7 +40,7 @@ export interface CreateUserData {
|
|
|
40
40
|
*/
|
|
41
41
|
export interface UserIdentityData {
|
|
42
42
|
id: string;
|
|
43
|
-
|
|
43
|
+
uid: string;
|
|
44
44
|
provider: string;
|
|
45
45
|
providerId: string;
|
|
46
46
|
profileData?: Record<string, unknown> | null;
|
|
@@ -113,7 +113,7 @@ export interface CreateRoleData {
|
|
|
113
113
|
*/
|
|
114
114
|
export interface RefreshTokenInfo {
|
|
115
115
|
id: string;
|
|
116
|
-
|
|
116
|
+
uid: string;
|
|
117
117
|
tokenHash: string;
|
|
118
118
|
expiresAt: Date;
|
|
119
119
|
createdAt: Date;
|
|
@@ -124,14 +124,14 @@ export interface RefreshTokenInfo {
|
|
|
124
124
|
* Password reset token info
|
|
125
125
|
*/
|
|
126
126
|
export interface PasswordResetTokenInfo {
|
|
127
|
-
|
|
127
|
+
uid: string;
|
|
128
128
|
expiresAt: Date;
|
|
129
129
|
}
|
|
130
130
|
/**
|
|
131
131
|
* Magic link token info
|
|
132
132
|
*/
|
|
133
133
|
export interface MagicLinkTokenInfo {
|
|
134
|
-
|
|
134
|
+
uid: string;
|
|
135
135
|
expiresAt: Date;
|
|
136
136
|
}
|
|
137
137
|
/**
|
|
@@ -185,11 +185,11 @@ export interface UserRepository {
|
|
|
185
185
|
/**
|
|
186
186
|
* Get all identities linked to a user
|
|
187
187
|
*/
|
|
188
|
-
getUserIdentities(
|
|
188
|
+
getUserIdentities(uid: string): Promise<UserIdentityData[]>;
|
|
189
189
|
/**
|
|
190
190
|
* Link a new OAuth identity to a user
|
|
191
191
|
*/
|
|
192
|
-
linkUserIdentity(
|
|
192
|
+
linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void>;
|
|
193
193
|
/**
|
|
194
194
|
* Update a user
|
|
195
195
|
*/
|
|
@@ -225,23 +225,23 @@ export interface UserRepository {
|
|
|
225
225
|
/**
|
|
226
226
|
* Get roles for a user
|
|
227
227
|
*/
|
|
228
|
-
getUserRoles(
|
|
228
|
+
getUserRoles(uid: string): Promise<RoleData[]>;
|
|
229
229
|
/**
|
|
230
230
|
* Get role IDs for a user
|
|
231
231
|
*/
|
|
232
|
-
getUserRoleIds(
|
|
232
|
+
getUserRoleIds(uid: string): Promise<string[]>;
|
|
233
233
|
/**
|
|
234
234
|
* Set roles for a user (replaces existing roles)
|
|
235
235
|
*/
|
|
236
|
-
setUserRoles(
|
|
236
|
+
setUserRoles(uid: string, roleIds: string[]): Promise<void>;
|
|
237
237
|
/**
|
|
238
238
|
* Assign a specific role to a new user
|
|
239
239
|
*/
|
|
240
|
-
assignDefaultRole(
|
|
240
|
+
assignDefaultRole(uid: string, roleId: string): Promise<void>;
|
|
241
241
|
/**
|
|
242
242
|
* Get user with their roles
|
|
243
243
|
*/
|
|
244
|
-
getUserWithRoles(
|
|
244
|
+
getUserWithRoles(uid: string): Promise<{
|
|
245
245
|
user: UserData;
|
|
246
246
|
roles: RoleData[];
|
|
247
247
|
} | null>;
|
|
@@ -280,7 +280,7 @@ export interface TokenRepository {
|
|
|
280
280
|
/**
|
|
281
281
|
* Create a new refresh token
|
|
282
282
|
*/
|
|
283
|
-
createRefreshToken(
|
|
283
|
+
createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void>;
|
|
284
284
|
/**
|
|
285
285
|
* Find a refresh token by hash
|
|
286
286
|
*/
|
|
@@ -292,19 +292,19 @@ export interface TokenRepository {
|
|
|
292
292
|
/**
|
|
293
293
|
* Delete all refresh tokens for a user
|
|
294
294
|
*/
|
|
295
|
-
deleteAllRefreshTokensForUser(
|
|
295
|
+
deleteAllRefreshTokensForUser(uid: string): Promise<void>;
|
|
296
296
|
/**
|
|
297
297
|
* List all refresh tokens for a user
|
|
298
298
|
*/
|
|
299
|
-
listRefreshTokensForUser(
|
|
299
|
+
listRefreshTokensForUser(uid: string): Promise<RefreshTokenInfo[]>;
|
|
300
300
|
/**
|
|
301
301
|
* Delete a specific refresh token by its primary key ID
|
|
302
302
|
*/
|
|
303
|
-
deleteRefreshTokenById(id: string,
|
|
303
|
+
deleteRefreshTokenById(id: string, uid: string): Promise<void>;
|
|
304
304
|
/**
|
|
305
305
|
* Create a password reset token
|
|
306
306
|
*/
|
|
307
|
-
createPasswordResetToken(
|
|
307
|
+
createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
|
|
308
308
|
/**
|
|
309
309
|
* Find a valid (not expired, not used) password reset token by hash
|
|
310
310
|
*/
|
|
@@ -316,7 +316,7 @@ export interface TokenRepository {
|
|
|
316
316
|
/**
|
|
317
317
|
* Delete all password reset tokens for a user
|
|
318
318
|
*/
|
|
319
|
-
deleteAllPasswordResetTokensForUser(
|
|
319
|
+
deleteAllPasswordResetTokensForUser(uid: string): Promise<void>;
|
|
320
320
|
/**
|
|
321
321
|
* Clean up expired tokens
|
|
322
322
|
*/
|
|
@@ -324,7 +324,7 @@ export interface TokenRepository {
|
|
|
324
324
|
/**
|
|
325
325
|
* Create a magic link token
|
|
326
326
|
*/
|
|
327
|
-
createMagicLinkToken(
|
|
327
|
+
createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
|
|
328
328
|
/**
|
|
329
329
|
* Find a valid (not expired, not used) magic link token by hash
|
|
330
330
|
*/
|
|
@@ -339,7 +339,7 @@ export interface TokenRepository {
|
|
|
339
339
|
*/
|
|
340
340
|
export interface MfaFactor {
|
|
341
341
|
id: string;
|
|
342
|
-
|
|
342
|
+
uid: string;
|
|
343
343
|
factorType: "totp";
|
|
344
344
|
friendlyName?: string;
|
|
345
345
|
verified: boolean;
|
|
@@ -361,7 +361,7 @@ export interface MfaChallengeInfo {
|
|
|
361
361
|
*/
|
|
362
362
|
export interface RecoveryCode {
|
|
363
363
|
id: string;
|
|
364
|
-
|
|
364
|
+
uid: string;
|
|
365
365
|
usedAt?: Date;
|
|
366
366
|
}
|
|
367
367
|
/**
|
|
@@ -372,11 +372,11 @@ export interface MfaRepository {
|
|
|
372
372
|
/**
|
|
373
373
|
* Create a new MFA factor for a user
|
|
374
374
|
*/
|
|
375
|
-
createMfaFactor(
|
|
375
|
+
createMfaFactor(uid: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor>;
|
|
376
376
|
/**
|
|
377
377
|
* Get all MFA factors for a user
|
|
378
378
|
*/
|
|
379
|
-
getMfaFactors(
|
|
379
|
+
getMfaFactors(uid: string): Promise<MfaFactor[]>;
|
|
380
380
|
/**
|
|
381
381
|
* Get a specific MFA factor by ID
|
|
382
382
|
*/
|
|
@@ -390,7 +390,7 @@ export interface MfaRepository {
|
|
|
390
390
|
/**
|
|
391
391
|
* Delete an MFA factor
|
|
392
392
|
*/
|
|
393
|
-
deleteMfaFactor(factorId: string,
|
|
393
|
+
deleteMfaFactor(factorId: string, uid: string): Promise<void>;
|
|
394
394
|
/**
|
|
395
395
|
* Create an MFA challenge
|
|
396
396
|
*/
|
|
@@ -406,23 +406,23 @@ export interface MfaRepository {
|
|
|
406
406
|
/**
|
|
407
407
|
* Create recovery codes for a user
|
|
408
408
|
*/
|
|
409
|
-
createRecoveryCodes(
|
|
409
|
+
createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void>;
|
|
410
410
|
/**
|
|
411
411
|
* Use a recovery code (mark as used)
|
|
412
412
|
*/
|
|
413
|
-
useRecoveryCode(
|
|
413
|
+
useRecoveryCode(uid: string, codeHash: string): Promise<boolean>;
|
|
414
414
|
/**
|
|
415
415
|
* Get unused recovery code count for a user
|
|
416
416
|
*/
|
|
417
|
-
getUnusedRecoveryCodeCount(
|
|
417
|
+
getUnusedRecoveryCodeCount(uid: string): Promise<number>;
|
|
418
418
|
/**
|
|
419
419
|
* Delete all recovery codes for a user
|
|
420
420
|
*/
|
|
421
|
-
deleteAllRecoveryCodes(
|
|
421
|
+
deleteAllRecoveryCodes(uid: string): Promise<void>;
|
|
422
422
|
/**
|
|
423
423
|
* Check if a user has any verified MFA factors
|
|
424
424
|
*/
|
|
425
|
-
hasVerifiedMfaFactors(
|
|
425
|
+
hasVerifiedMfaFactors(uid: string): Promise<boolean>;
|
|
426
426
|
}
|
|
427
427
|
/**
|
|
428
428
|
* Combined auth repository interface for convenience
|
package/dist/auth/jwt.d.ts
CHANGED
|
@@ -4,9 +4,14 @@ export interface JwtConfig {
|
|
|
4
4
|
refreshExpiresIn?: string;
|
|
5
5
|
}
|
|
6
6
|
export interface AccessTokenPayload {
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* The user's id — the same spelling the domain model, the auth adapters and
|
|
9
|
+
* the RLS layer (`auth.uid()`) all use. Tokens minted before this rename
|
|
10
|
+
* carry `uid` instead, and older external IdPs may send `sub`;
|
|
11
|
+
* {@link verifyAccessToken} accepts all three and normalises to this.
|
|
12
|
+
*/
|
|
13
|
+
uid: string;
|
|
8
14
|
roles: string[];
|
|
9
|
-
uid?: string;
|
|
10
15
|
/** Authentication Assurance Level: aal1 = password/oauth, aal2 = MFA verified */
|
|
11
16
|
aal?: "aal1" | "aal2";
|
|
12
17
|
/** Email claim from the JWT, if present */
|
|
@@ -28,7 +33,7 @@ export declare function configureJwt(config: JwtConfig): void;
|
|
|
28
33
|
/**
|
|
29
34
|
* Generate an access token (short-lived, 1 hour by default)
|
|
30
35
|
*/
|
|
31
|
-
export declare function generateAccessToken(
|
|
36
|
+
export declare function generateAccessToken(uid: string, roles: string[], aal?: "aal1" | "aal2", customClaims?: Record<string, unknown>): string;
|
|
32
37
|
/**
|
|
33
38
|
* Get the expiration time of an access token in milliseconds from now
|
|
34
39
|
*/
|
|
@@ -38,7 +43,18 @@ export declare function getAccessTokenExpiryMs(): number;
|
|
|
38
43
|
*/
|
|
39
44
|
export declare function getAccessTokenExpiry(): number;
|
|
40
45
|
/**
|
|
41
|
-
* Verify and decode an access token
|
|
46
|
+
* Verify and decode an access token.
|
|
47
|
+
*
|
|
48
|
+
* Every token this server issues is signed with the same secret, so what a
|
|
49
|
+
* token *is* comes from its claims, not from its signature. A download token
|
|
50
|
+
* ({@link generateDownloadToken}) is therefore a validly-signed string that
|
|
51
|
+
* must never authenticate anybody: it is scoped to one file path and handed out
|
|
52
|
+
* in URLs, which is a far weaker thing to hold than a session.
|
|
53
|
+
*
|
|
54
|
+
* Today it is rejected below for want of an id — but only by luck, since
|
|
55
|
+
* nothing stops a future download token from carrying one. So the purpose is
|
|
56
|
+
* checked explicitly: a token minted for reading a file is not a token for
|
|
57
|
+
* being a user.
|
|
42
58
|
*/
|
|
43
59
|
export declare function verifyAccessToken(token: string): AccessTokenPayload | null;
|
|
44
60
|
/**
|
|
@@ -23,10 +23,10 @@ export declare function mountMagicLinkRoutes(deps: {
|
|
|
23
23
|
isAnonymous?: boolean;
|
|
24
24
|
metadata?: Record<string, unknown> | null;
|
|
25
25
|
}, roleIds: string[], accessToken: string, refreshToken: string, providerId: string) => unknown;
|
|
26
|
-
createSessionAndTokens: (
|
|
26
|
+
createSessionAndTokens: (uid: string, userAgent: string, ipAddress: string) => Promise<{
|
|
27
27
|
roleIds: string[];
|
|
28
28
|
accessToken: string;
|
|
29
29
|
refreshToken: string;
|
|
30
30
|
}>;
|
|
31
|
-
applyTransformHook: (response: AuthResponsePayload, method: TransformAuthResponseContext["method"], request: Request,
|
|
31
|
+
applyTransformHook: (response: AuthResponsePayload, method: TransformAuthResponseContext["method"], request: Request, uid: string) => Promise<AuthResponsePayload>;
|
|
32
32
|
}): void;
|