@rebasepro/server 0.9.1-canary.26fe4b2 → 0.9.1-canary.29ed165

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.
@@ -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
- constructor(statusCode: number, code: string, message: string, details?: unknown);
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;
@@ -12,8 +12,11 @@ import { CollectionConfig } from "@rebasepro/types";
12
12
  * columns, so the set is exact);
13
13
  * - the foreign-key column behind an owning relation, which callers may write
14
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;
15
17
  * - nothing else. `id` in particular is not automatically known — see below.
16
18
  */
17
19
  export declare function assertKnownWriteFields(values: Record<string, unknown>, collection: CollectionConfig, options?: {
18
20
  rowIndex?: number;
21
+ extraKnownFields?: readonly string[];
19
22
  }): void;
package/dist/index.es.js CHANGED
@@ -3539,12 +3539,23 @@ var ApiError = class ApiError extends Error {
3539
3539
  statusCode;
3540
3540
  code;
3541
3541
  details;
3542
- constructor(statusCode, code, message, details) {
3542
+ /**
3543
+ * Whether this outcome is a routine part of normal operation rather than
3544
+ * something an operator should look at. Expected errors log at debug; every
3545
+ * other operational error logs at warn.
3546
+ *
3547
+ * The motivating case is `POST /auth/refresh` with no session: clients
3548
+ * refresh on page load before they know whether one exists, so every
3549
+ * anonymous page view is a 401 — correct, and not worth a warning line.
3550
+ */
3551
+ expected;
3552
+ constructor(statusCode, code, message, details, expected = false) {
3543
3553
  super(message);
3544
3554
  this.name = "ApiError";
3545
3555
  this.statusCode = statusCode;
3546
3556
  this.code = code;
3547
3557
  this.details = details;
3558
+ this.expected = expected;
3548
3559
  }
3549
3560
  static badRequest(message, code = "BAD_REQUEST", details) {
3550
3561
  return new ApiError(400, code, message, details);
@@ -3552,6 +3563,13 @@ var ApiError = class ApiError extends Error {
3552
3563
  static unauthorized(message, code = "UNAUTHORIZED") {
3553
3564
  return new ApiError(401, code, message);
3554
3565
  }
3566
+ /**
3567
+ * A 401 that is a normal outcome, not an incident — logged at debug.
3568
+ * See {@link ApiError.expected}.
3569
+ */
3570
+ static unauthenticated(message, code = "UNAUTHORIZED") {
3571
+ return new ApiError(401, code, message, void 0, true);
3572
+ }
3555
3573
  static forbidden(message, code = "FORBIDDEN") {
3556
3574
  return new ApiError(403, code, message);
3557
3575
  }
@@ -3584,7 +3602,10 @@ var errorHandler = (err, c) => {
3584
3602
  const error = err;
3585
3603
  const reqId = typeof c.get === "function" ? c.get("requestId") : void 0;
3586
3604
  if (error instanceof ApiError || error.name === "ApiError") {
3587
- logger.warn(`⚠️ [API] ${c.req.method} ${c.req.path} → ${error.statusCode} ${error.code}: ${error.message}` + (reqId ? ` [${reqId}]` : ""));
3605
+ const expected = error instanceof ApiError && error.expected;
3606
+ const line = `[API] ${c.req.method} ${c.req.path} → ${error.statusCode} ${error.code}: ${error.message}` + (reqId ? ` [${reqId}]` : "");
3607
+ if (expected) logger.debug(line);
3608
+ else logger.warn(`⚠️ ${line}`);
3588
3609
  return c.json({ error: {
3589
3610
  message: error.message,
3590
3611
  code: error.code || "INTERNAL_ERROR",
@@ -3826,6 +3847,8 @@ function parseQueryOptions(query) {
3826
3847
  * columns, so the set is exact);
3827
3848
  * - the foreign-key column behind an owning relation, which callers may write
3828
3849
  * directly instead of through the relation property;
3850
+ * - anything named in `options.extraKnownFields` — for an auth collection the
3851
+ * credential keys the auth adapter consumes before a row is ever built;
3829
3852
  * - nothing else. `id` in particular is not automatically known — see below.
3830
3853
  */
3831
3854
  function assertKnownWriteFields(values, collection, options) {
@@ -3833,6 +3856,7 @@ function assertKnownWriteFields(values, collection, options) {
3833
3856
  if (!collection.properties || Object.keys(collection.properties).length === 0) return;
3834
3857
  const known = new Set(Object.keys(collection.properties));
3835
3858
  for (const relation of Object.values(resolveCollectionRelations(collection))) if (relation.localKey) known.add(relation.localKey);
3859
+ for (const field of options?.extraKnownFields ?? []) known.add(field);
3836
3860
  const unknown = Object.keys(values).filter((key) => !known.has(key));
3837
3861
  if (unknown.length === 0) return;
3838
3862
  const where = options?.rowIndex !== void 0 ? `Row ${options.rowIndex}: ` : "";
@@ -4081,9 +4105,13 @@ var RestApiGenerator = class {
4081
4105
  const body = await parseJsonBody(c);
4082
4106
  const isAuth = collection.auth;
4083
4107
  const isAuthCollection = isAuth === true || isAuth && typeof isAuth === "object" && isAuth.enabled === true;
4108
+ const collectionAuthConfig = typeof isAuth === "object" ? isAuth : void 0;
4084
4109
  if (!isAuthCollection) assertKnownWriteFields(body, resolvedCollection);
4110
+ else {
4111
+ const contract = this.authAdapter?.describeUserCreationContract?.(collectionAuthConfig);
4112
+ if (contract?.validate) assertKnownWriteFields(body, resolvedCollection, { extraKnownFields: contract.extraFields });
4113
+ }
4085
4114
  if (isAuthCollection && this.authAdapter?.prepareUserCreation) {
4086
- const collectionAuthConfig = typeof isAuth === "object" ? isAuth : void 0;
4087
4115
  const prepared = await this.authAdapter.prepareUserCreation(body, collectionAuthConfig);
4088
4116
  const entity = await driver.save({
4089
4117
  path,
@@ -10347,7 +10375,7 @@ function createAuthRoutes(config) {
10347
10375
  oldPassword: string().min(1, "Old password is required").max(128),
10348
10376
  newPassword: string().min(1, "New password is required").max(128)
10349
10377
  });
10350
- const refreshSchema = object({ refreshToken: config.cookieAuth ? string().optional() : string().min(1, "Refresh token is required") });
10378
+ const refreshSchema = object({ refreshToken: string().min(1).optional() });
10351
10379
  object({ refreshToken: string().optional() });
10352
10380
  object({
10353
10381
  displayName: string().max(255).optional(),
@@ -10731,7 +10759,7 @@ function createAuthRoutes(config) {
10731
10759
  */
10732
10760
  router.post("/refresh", async (c) => {
10733
10761
  const refreshToken = readRefreshToken(c, parseBody(refreshSchema, await c.req.json().catch(() => ({}))), config.cookieAuth);
10734
- if (!refreshToken) throw ApiError.badRequest("Refresh token is required", "INVALID_INPUT");
10762
+ if (!refreshToken) throw ApiError.unauthenticated("No refresh token presented", "NO_SESSION");
10735
10763
  const tokenHash = hashRefreshToken(refreshToken);
10736
10764
  const storedToken = await authRepo.findRefreshTokenByHash(tokenHash);
10737
10765
  if (!storedToken) {
@@ -11253,6 +11281,16 @@ function createBuiltinAuthAdapter(config) {
11253
11281
  collectionAuthConfig: collectionAuth ?? collectionAuthConfig
11254
11282
  });
11255
11283
  },
11284
+ describeUserCreationContract(collectionAuth) {
11285
+ if ((collectionAuth ?? collectionAuthConfig)?.onCreateUser || resolvedOps.onAdminCreateUser) return {
11286
+ validate: false,
11287
+ extraFields: []
11288
+ };
11289
+ return {
11290
+ validate: true,
11291
+ extraFields: ["password"]
11292
+ };
11293
+ },
11256
11294
  async finalizeUserCreation(entity, clearPassword) {
11257
11295
  return finalizeAdminUserCreation(entity, clearPassword, {
11258
11296
  authRepo: authRepository,