@rebasepro/server 0.14.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/api/rest/query-parser.d.ts +37 -1
  2. package/dist/api/rest/write-validation.d.ts +26 -0
  3. package/dist/auth/index.d.ts +3 -1
  4. package/dist/auth/interfaces.d.ts +14 -1
  5. package/dist/auth/jwks-routes.d.ts +17 -0
  6. package/dist/auth/jwt-keys.d.ts +108 -0
  7. package/dist/auth/jwt.d.ts +62 -2
  8. package/dist/{auth-CYoPVf-E.js → auth-BobZVd0j.js} +142 -167
  9. package/dist/auth-BobZVd0j.js.map +1 -0
  10. package/dist/boot/boot.d.ts +36 -50
  11. package/dist/boot/ddl-bootstrap.d.ts +15 -0
  12. package/dist/boot/env.d.ts +20 -0
  13. package/dist/boot/provision.d.ts +182 -0
  14. package/dist/boot/role.d.ts +88 -0
  15. package/dist/{cron-store-Dvr4Y1sZ.js → cron-store-CB1x-Ken.js} +3 -3
  16. package/dist/{cron-store-Dvr4Y1sZ.js.map → cron-store-CB1x-Ken.js.map} +1 -1
  17. package/dist/{ddl-bootstrap-BhXbTnBl.js → ddl-bootstrap-Cywoj8Ta.js} +40 -2
  18. package/dist/ddl-bootstrap-Cywoj8Ta.js.map +1 -0
  19. package/dist/env.d.ts +2 -0
  20. package/dist/functions/proxy.d.ts +41 -0
  21. package/dist/functions/selection.d.ts +45 -0
  22. package/dist/index.d.ts +8 -3
  23. package/dist/index.es.js +1113 -661
  24. package/dist/index.es.js.map +1 -1
  25. package/dist/init/shutdown.d.ts +4 -0
  26. package/dist/init/surfaces.d.ts +79 -0
  27. package/dist/init.d.ts +121 -1
  28. package/dist/jobs/index.d.ts +5 -0
  29. package/dist/jobs/job-queue.d.ts +14 -0
  30. package/dist/jobs/job-store.d.ts +22 -0
  31. package/dist/jobs/types.d.ts +125 -0
  32. package/dist/jobs-DR4SjGrD.js +326 -0
  33. package/dist/jobs-DR4SjGrD.js.map +1 -0
  34. package/dist/{jwt-_IFqfTOg.js → jwt-VJyXTdQQ.js} +447 -11
  35. package/dist/jwt-VJyXTdQQ.js.map +1 -0
  36. package/dist/{openapi-generator-DPKtUC9X.js → openapi-generator-DQeQ_q2f.js} +68 -3
  37. package/dist/openapi-generator-DQeQ_q2f.js.map +1 -0
  38. package/dist/proxy-Bj5DVllb.js +139 -0
  39. package/dist/proxy-Bj5DVllb.js.map +1 -0
  40. package/dist/{request-timeout-RivJsME0.js → request-timeout-BuFoEKwT.js} +6 -3
  41. package/dist/request-timeout-BuFoEKwT.js.map +1 -0
  42. package/dist/selection-_z6TM1DB.js +64 -0
  43. package/dist/selection-_z6TM1DB.js.map +1 -0
  44. package/dist/services/webhook-service.d.ts +43 -5
  45. package/dist/{src-C7rkDGxA.js → src-8XDWyDfR.js} +84 -13
  46. package/dist/src-8XDWyDfR.js.map +1 -0
  47. package/dist/src-Cz9nMgUR.js.map +1 -1
  48. package/dist/storage/keys.d.ts +17 -0
  49. package/dist/storage/routes.d.ts +1 -1
  50. package/dist/storage/storage-registry.d.ts +46 -4
  51. package/dist/storage/tus-handler.d.ts +1 -1
  52. package/package.json +5 -5
  53. package/dist/auth-CYoPVf-E.js.map +0 -1
  54. package/dist/ddl-bootstrap-BhXbTnBl.js.map +0 -1
  55. package/dist/jwt-_IFqfTOg.js.map +0 -1
  56. package/dist/openapi-generator-DPKtUC9X.js.map +0 -1
  57. package/dist/request-timeout-RivJsME0.js.map +0 -1
  58. package/dist/src-C7rkDGxA.js.map +0 -1
@@ -1,6 +1,42 @@
1
- import type { ListLimitBounds } from "@rebasepro/types";
1
+ import type { ListLimitBounds, OrderByTuple } from "@rebasepro/types";
2
2
  import { QueryOptions } from "../types";
3
3
  export declare const mapOperator: (op: string) => import("@rebasepro/types").WhereFilterOp | null;
4
+ type OrderByEntry = {
5
+ field: string;
6
+ direction: "asc" | "desc";
7
+ };
8
+ /**
9
+ * The parsed entries as the driver contract spells them: `[field, direction]`
10
+ * tuples in order of significance.
11
+ *
12
+ * The REST layer used to hand the driver `orderBy[0].field` and drop the rest,
13
+ * so `?orderBy=[{"field":"roles"},{"field":"created_at","direction":"desc"}]`
14
+ * — a shape this parser has always accepted and validated in full — sorted by
15
+ * `roles` alone and returned the ties in whatever order Postgres pleased.
16
+ */
17
+ export declare function orderByEntriesToTuples(entries?: OrderByEntry[]): OrderByTuple[] | undefined;
18
+ export interface ParsedAggregate {
19
+ fn: "count" | "sum" | "avg" | "min" | "max";
20
+ /** Absent only for `count()`, which counts rows rather than values. */
21
+ field?: string;
22
+ /** The key this appears under in the response. */
23
+ alias: string;
24
+ }
25
+ /**
26
+ * Parse `?select=count(),sum(total),avg(total)`.
27
+ *
28
+ * The spelling is SQL's, because whoever writes it is thinking in SQL and
29
+ * because any other spelling has to be learned first. `count()` with no field
30
+ * counts rows; every other function names a column.
31
+ *
32
+ * Aliases are derived rather than accepted: `sum(total)` returns as
33
+ * `sum_total`, `count()` as `count`. Letting a caller choose would mean
34
+ * checking their alias is not also a `groupBy` field — a rule nobody would
35
+ * guess, and a silently overwritten value if it went unchecked.
36
+ */
37
+ export declare function parseAggregateSelect(raw: unknown): ParsedAggregate[] | undefined;
38
+ /** Parse `?groupBy=status,country`. */
39
+ export declare function parseGroupBy(raw: unknown): string[] | undefined;
4
40
  export { DEFAULT_LIST_LIMIT, DEFAULT_VECTOR_LIST_LIMIT, MAX_LIST_LIMIT } from "@rebasepro/types";
5
41
  /**
6
42
  * Overridable list-pagination bounds for {@link parseQueryOptions}. Without
@@ -85,3 +85,29 @@ export declare function assertWriteValuesValid(values: Record<string, unknown>,
85
85
  export declare function projectResponseFields<T extends Record<string, unknown>>(rows: T[], fields: readonly string[] | undefined, collection: CollectionConfig, options?: {
86
86
  include?: readonly string[];
87
87
  }): T[];
88
+ /**
89
+ * Both write checks, as one call, for a transport that is not the REST router.
90
+ *
91
+ * The REST routes run `assertKnownWriteFields` and `assertWriteValuesValid`
92
+ * back to back on the caller's body, seven times over — and they were the only
93
+ * place either ran. The WebSocket `SAVE` handler took a client payload straight
94
+ * to `driver.save`, so the same write arrived validated through one door and
95
+ * unvalidated through the other: `PATCH /api/data/users/1 { age: 999 }` was a
96
+ * 400, and the socket wrote it.
97
+ *
98
+ * Exported for the sockets in `@rebasepro/server-postgres` and
99
+ * `@rebasepro/server-mongo`, which are the other request boundaries. Not the
100
+ * route builder — `index.ts` keeps that internal, and this is a rule rather
101
+ * than wiring.
102
+ *
103
+ * It stays at the boundary rather than moving into the driver deliberately: it
104
+ * validates what a *caller sent*, before `beforeSave` gets a chance to fill in
105
+ * or rewrite anything. In-process writes through `rebase.data` are trusted
106
+ * server code and are not run through it, which is the placement the REST layer
107
+ * already chose.
108
+ *
109
+ * @param values the caller's payload, exactly as it arrived
110
+ * @param collection resolved from the registry by path — never the copy the
111
+ * client sent, or the rules would be the caller's to pick
112
+ */
113
+ export declare function assertWriteRequestValid(values: Record<string, unknown>, collection: CollectionConfig): void;
@@ -1,6 +1,8 @@
1
1
  export * from "./interfaces";
2
- export { configureJwt, isJwtConfigured, generateAccessToken, verifyAccessToken, generateRefreshToken, hashRefreshToken, getRefreshTokenExpiry, getAccessTokenExpiry, generateDownloadToken, verifyDownloadToken } from "./jwt";
2
+ export { configureJwt, isJwtConfigured, generateAccessToken, verifyAccessToken, generateRefreshToken, hashRefreshToken, getRefreshTokenExpiry, getAccessTokenExpiry, generateDownloadToken, verifyDownloadToken, getJwks, hasAsymmetricSigningKey } from "./jwt";
3
3
  export type { JwtConfig, AccessTokenPayload, DownloadTokenPayload } from "./jwt";
4
+ export { createJwksRoutes } from "./jwks-routes";
5
+ export type { JwtSigningKeyConfig, JwtSigningAlgorithm, PublicJwk } from "./jwt-keys";
4
6
  export { hashPassword, verifyPassword, validatePasswordStrength } from "./password";
5
7
  export { safeCompare } from "./crypto-utils";
6
8
  export type { PasswordValidationResult } from "./password";
@@ -227,7 +227,20 @@ export interface PaginatedUsersResult {
227
227
  */
228
228
  export interface UserRepository {
229
229
  /**
230
- * Create a new user
230
+ * Create a new user.
231
+ *
232
+ * **An email already in use is a 409 `EMAIL_EXISTS`, not a 500.** Every
233
+ * caller checks first — `POST /auth/register` reads `getUserByEmail` and
234
+ * answers 409 — and every engine backs that check with a unique index,
235
+ * because a check cannot hold its answer still. What is left is the window
236
+ * between the two, and a double-clicked signup button is wide enough: both
237
+ * requests read "no such user", both insert, one wins.
238
+ *
239
+ * The loser's insert violates the index either way; the only question is
240
+ * what it is turned into. An unmapped driver error is a 500 "Internal
241
+ * Server Error", which tells the person who clicked twice that the server
242
+ * is broken rather than that they already have an account — and tells the
243
+ * operator to go looking for a fault that is not there.
231
244
  */
232
245
  createUser(data: CreateUserData): Promise<UserData>;
233
246
  /**
@@ -0,0 +1,17 @@
1
+ import { Hono } from "hono";
2
+ import type { HonoEnv } from "../api/types";
3
+ /**
4
+ * `GET /.well-known/jwks.json` — the public keys that verify this issuer's
5
+ * access tokens.
6
+ *
7
+ * Deliberately unauthenticated and world-readable: the whole point is that a
8
+ * gateway, an edge function or a neighbouring service can check a Rebase token
9
+ * without being trusted with anything. Public keys are not a secret, and
10
+ * `jwt-keys.ts` derives what is served here from the public half of each pair.
11
+ *
12
+ * Mounted at the root rather than under `basePath`, because `/.well-known/` is
13
+ * where every verifier looks — an issuer of `https://api.example.com` implies
14
+ * `https://api.example.com/.well-known/jwks.json`, whatever the API happens to
15
+ * be prefixed with.
16
+ */
17
+ export declare function createJwksRoutes(): Hono<HonoEnv>;
@@ -0,0 +1,108 @@
1
+ import { type KeyObject } from "crypto";
2
+ /**
3
+ * Asymmetric signing keys for access tokens, and the JWKS built from them.
4
+ *
5
+ * The symmetric secret this replaces is not going away — it still signs every
6
+ * purpose-scoped token (download, MFA-pending, password reset), which are read
7
+ * only by the server that minted them and are better off short. What a shared
8
+ * secret cannot do is let *anybody else* verify a session:
9
+ *
10
+ * - a gateway, an edge worker, or a second service that wants to check a token
11
+ * has to be handed the key that mints them, so every verifier becomes a
12
+ * forger;
13
+ * - and rotating it invalidates every token in circulation at once, which is
14
+ * why in practice it never gets rotated at all.
15
+ *
16
+ * A private key signs, the matching public key verifies, and the public half is
17
+ * published at `/.well-known/jwks.json` for anyone to fetch. Rotation stops
18
+ * being an outage: mint with the new key, keep the old one in the list until
19
+ * the last token signed by it has expired, then drop it.
20
+ *
21
+ * **The key is chosen by `kid`, and the algorithm comes from the key — never
22
+ * from the token.** A verifier that reads `alg` out of the header it is
23
+ * checking will accept an `HS256` token whose "secret" is the RSA public key it
24
+ * published, which is a complete authentication bypass and the best-known way
25
+ * to get this wrong. {@link resolveVerificationKey} therefore returns the
26
+ * algorithm alongside the key, and the caller pins it.
27
+ */
28
+ /** The algorithms a signing key may use. Both are widely supported by verifiers. */
29
+ export type JwtSigningAlgorithm = "RS256" | "ES256";
30
+ /**
31
+ * One asymmetric key pair, as an operator configures it.
32
+ *
33
+ * Only the private key is supplied: the public half is derived from it, so a
34
+ * mismatched pair — a configuration error that produces tokens nobody can
35
+ * verify, and which no amount of local testing catches because the signer never
36
+ * consults the public key — cannot be expressed.
37
+ */
38
+ export interface JwtSigningKeyConfig {
39
+ /**
40
+ * Names this key in the token header and in the JWKS. Any stable string;
41
+ * something that identifies *when* it was minted (`"2026-08"`) is the usual
42
+ * choice, because the question you ask of a `kid` later is always "is this
43
+ * the old one?".
44
+ */
45
+ kid: string;
46
+ /** PEM-encoded PKCS#8 or SEC1 private key. */
47
+ privateKey: string;
48
+ /**
49
+ * Defaults to the algorithm implied by the key type — RSA keys sign RS256,
50
+ * EC keys sign ES256. Worth setting only to be explicit.
51
+ */
52
+ algorithm?: JwtSigningAlgorithm;
53
+ }
54
+ /** A configured key, parsed and ready to sign or verify with. */
55
+ export interface ResolvedJwtKey {
56
+ kid: string;
57
+ algorithm: JwtSigningAlgorithm;
58
+ privateKey: KeyObject;
59
+ publicKey: KeyObject;
60
+ }
61
+ /** A JSON Web Key, as served by the JWKS endpoint. Public parameters only. */
62
+ export type PublicJwk = Record<string, unknown> & {
63
+ kid: string;
64
+ alg: JwtSigningAlgorithm;
65
+ use: "sig";
66
+ };
67
+ /**
68
+ * Parse the configured keys, deriving each public half from its private key.
69
+ *
70
+ * Throws on anything malformed. This runs at boot, from `configureJwt`, so a
71
+ * key that cannot sign takes the process down at start rather than at the first
72
+ * login — the same bargain every other credential in this file makes.
73
+ */
74
+ export declare function resolveSigningKeys(configs: JwtSigningKeyConfig[]): ResolvedJwtKey[];
75
+ /**
76
+ * The key a token names, or `null` if it names none we hold.
77
+ *
78
+ * The returned algorithm is the *key's*, and the caller must verify with that
79
+ * one alone. See the module docblock for what happens otherwise.
80
+ */
81
+ export declare function resolveVerificationKey(keys: ResolvedJwtKey[], kid: string | undefined): ResolvedJwtKey | null;
82
+ /**
83
+ * A PEM as an environment variable can actually carry it.
84
+ *
85
+ * A PEM is multi-line and environment variables are not, so every deployment
86
+ * tool solves it differently: `.env` files and most secret managers escape the
87
+ * newlines to `\n`, Kubernetes and Docker secrets pass the bytes through
88
+ * intact, and CI systems that mangle both are usually fed base64. All three
89
+ * arrive here, and guessing wrong produces "not a readable PEM private key" at
90
+ * boot with a key the operator can see is perfectly valid.
91
+ *
92
+ * Detection is on content, not on a flag: a PEM says so on its first line, and
93
+ * anything that does not is tried as base64.
94
+ */
95
+ export declare function normalizePemFromEnv(value: string): string;
96
+ /**
97
+ * The public halves, in JWKS form.
98
+ *
99
+ * Node exports a JWK containing only public parameters for a public
100
+ * `KeyObject` — no `d`, no primes — so the private material cannot leak
101
+ * through this path even if a private key were passed by mistake. The keys are
102
+ * derived from `publicKey` regardless, and this is asserted in the tests,
103
+ * because "cannot" is worth checking on the one endpoint whose entire job is to
104
+ * be world-readable.
105
+ */
106
+ export declare function toJwks(keys: ResolvedJwtKey[]): {
107
+ keys: PublicJwk[];
108
+ };
@@ -1,7 +1,26 @@
1
+ import { type JwtSigningKeyConfig, type PublicJwk } from "./jwt-keys";
1
2
  export interface JwtConfig {
2
3
  secret: string;
3
4
  accessExpiresIn?: string;
4
5
  refreshExpiresIn?: string;
6
+ /**
7
+ * Asymmetric keys for signing **access tokens**, newest first.
8
+ *
9
+ * Optional and additive: with none configured everything below behaves
10
+ * exactly as it did, signing HS256 with {@link JwtConfig.secret}. With one
11
+ * configured, access tokens are signed by {@link JwtConfig.activeKid} (or
12
+ * the first entry) and carry its `kid`, and every key in the list keeps
13
+ * verifying — which is what makes rotation a deploy rather than a mass
14
+ * sign-out. Tokens minted before any key existed carry no `kid` and are
15
+ * still verified against the secret until they expire.
16
+ *
17
+ * The secret stays required regardless: purpose-scoped tokens (download,
18
+ * MFA-pending, password reset) are read only by this server, and are
19
+ * shorter and cheaper symmetric. See `jwt-keys.ts`.
20
+ */
21
+ signingKeys?: JwtSigningKeyConfig[];
22
+ /** Which key signs. Defaults to the first entry of {@link JwtConfig.signingKeys}. */
23
+ activeKid?: string;
5
24
  }
6
25
  export interface AccessTokenPayload {
7
26
  /**
@@ -42,6 +61,19 @@ export interface AccessTokenPayload {
42
61
  * Validates the secret strength to prevent deployment with default/weak secrets.
43
62
  */
44
63
  export declare function configureJwt(config: JwtConfig): void;
64
+ /**
65
+ * The public keys, in JWKS form, for `/.well-known/jwks.json`.
66
+ *
67
+ * An empty `keys` array on a backend with no asymmetric keys configured is the
68
+ * correct answer rather than a 404: it says "this issuer publishes none",
69
+ * which a verifier can act on, where a 404 is indistinguishable from a
70
+ * misconfigured URL.
71
+ */
72
+ export declare function getJwks(): {
73
+ keys: PublicJwk[];
74
+ };
75
+ /** Is this backend signing access tokens asymmetrically? */
76
+ export declare function hasAsymmetricSigningKey(): boolean;
45
77
  /**
46
78
  * Has this server been given a JWT secret?
47
79
  *
@@ -141,12 +173,40 @@ export declare function verifyMfaPendingToken(token: string): {
141
173
  export interface DownloadTokenPayload {
142
174
  purpose: "file-read";
143
175
  path: string;
176
+ /**
177
+ * The storage source the grant is good for, canonicalized by
178
+ * {@link canonicalStorageId}. Always present on a decoded payload; see
179
+ * {@link verifyDownloadToken} for how tokens minted before this claim
180
+ * existed are read.
181
+ */
182
+ storageId: string;
144
183
  }
145
184
  /**
146
185
  * Generate a short-lived download token scoped to a specific file path or prefix
186
+ * *within one storage source*.
187
+ *
188
+ * Both halves of that scope are load-bearing. A key is only unique inside its
189
+ * own bucket, and a project with more than one source routinely holds the same
190
+ * key in several of them — `avatars/u1.png` in `(default)` and in `media` are
191
+ * different objects, quite possibly with different owners. A token that names
192
+ * only the path is therefore a grant on every source at once: authorize a read
193
+ * on the source whose `storageAuthorize` hook says yes, then spend the token
194
+ * against `?storageId=` pointing somewhere else.
195
+ *
196
+ * `storageId` is optional here only because omitting it *is* the default
197
+ * source, which is what the overwhelming majority of deployments have. A mint
198
+ * site that forgets to pass a named source produces a default-scoped token,
199
+ * which fails closed at `/file/*` rather than over-granting.
147
200
  */
148
- export declare function generateDownloadToken(path: string, expiresInSeconds?: number): string;
201
+ export declare function generateDownloadToken(path: string, expiresInSeconds?: number, storageId?: string | null): string;
149
202
  /**
150
- * Verify and decode a download token
203
+ * Verify and decode a download token.
204
+ *
205
+ * A token minted before `storageId` existed carries no such claim. It is read
206
+ * as a grant on the **default source** rather than on all of them: that is the
207
+ * fail-closed reading, and it is what such a token almost always was, since a
208
+ * named source has to be asked for explicitly. The cost is bounded by the
209
+ * five-minute TTL — for at most that long after a deploy, an in-flight token
210
+ * for a *named* source is refused and the client re-fetches `/metadata`.
151
211
  */
152
212
  export declare function verifyDownloadToken(token: string): DownloadTokenPayload | null;