@palbase/backend 32.0.0 → 33.0.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 (63) hide show
  1. package/dist/bin/palbase-backend.cjs +79 -26
  2. package/dist/bin/palbase-backend.cjs.map +1 -1
  3. package/dist/bin/palbase-backend.js +5 -5
  4. package/dist/{chunk-AZJIKCOR.js → chunk-IKDONZ5D.js} +39 -23
  5. package/dist/{chunk-AZJIKCOR.js.map → chunk-IKDONZ5D.js.map} +1 -1
  6. package/dist/{chunk-IXAX5CON.js → chunk-SI4KGEM3.js} +1 -1
  7. package/dist/{chunk-IXAX5CON.js.map → chunk-SI4KGEM3.js.map} +1 -1
  8. package/dist/{chunk-XEGZ3S2Q.js → chunk-TVCCR6SO.js} +32 -7
  9. package/dist/chunk-TVCCR6SO.js.map +1 -0
  10. package/dist/{chunk-5C5UCILO.js → chunk-WWUG2QXF.js} +3 -3
  11. package/dist/{chunk-KGP6ALIU.js → chunk-XOX6RFPZ.js} +30 -8
  12. package/dist/chunk-XOX6RFPZ.js.map +1 -0
  13. package/dist/{chunk-Z4CZRMNF.js → chunk-YIQ4RS4F.js} +2 -2
  14. package/dist/db/index.cjs +14 -4
  15. package/dist/db/index.cjs.map +1 -1
  16. package/dist/db/index.d.cts +1 -1
  17. package/dist/db/index.d.ts +1 -1
  18. package/dist/db/index.js +3 -3
  19. package/dist/engine/index.cjs +79 -26
  20. package/dist/engine/index.cjs.map +1 -1
  21. package/dist/engine/index.d.cts +3 -3
  22. package/dist/engine/index.d.ts +3 -3
  23. package/dist/engine/index.js +5 -5
  24. package/dist/{index-BILC9WwS.d.ts → index-D-3duy8Y.d.ts} +2 -2
  25. package/dist/{index-DgYkdNT9.d.cts → index-DB_nW-AV.d.cts} +87 -30
  26. package/dist/{index-CWGiJ2Up.d.cts → index-DLveQoOf.d.cts} +2 -2
  27. package/dist/{index-BgWnP07a.d.ts → index-DMZG3kpo.d.ts} +87 -30
  28. package/dist/index.cjs +59 -12
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +6 -6
  31. package/dist/index.d.ts +6 -6
  32. package/dist/index.js +5 -5
  33. package/dist/openapi/index.d.cts +2 -2
  34. package/dist/openapi/index.d.ts +2 -2
  35. package/dist/{registry-BcRYIQ-R.d.cts → registry-DSTThhKf.d.cts} +1 -1
  36. package/dist/{registry-ClzjxIWy.d.ts → registry-JjF5lcj4.d.ts} +1 -1
  37. package/dist/test/index.cjs +1155 -12
  38. package/dist/test/index.cjs.map +1 -1
  39. package/dist/test/index.d.cts +1 -1
  40. package/dist/test/index.d.ts +1 -1
  41. package/dist/test/index.js +1126 -8
  42. package/dist/test/index.js.map +1 -1
  43. package/docs/README.md +124 -80
  44. package/docs/auth.md +112 -26
  45. package/docs/background.md +16 -7
  46. package/docs/database.md +22 -18
  47. package/docs/endpoints.md +26 -20
  48. package/docs/errors.md +2 -3
  49. package/docs/events.md +63 -29
  50. package/docs/getting-started.md +8 -7
  51. package/docs/llms-full.txt +519 -244
  52. package/docs/migrations.md +6 -5
  53. package/docs/routing.md +34 -11
  54. package/docs/schema.md +78 -27
  55. package/docs/services.md +30 -13
  56. package/package.json +2 -1
  57. package/stack-images.json +24 -0
  58. package/template/db/public.ts +23 -0
  59. package/template/package.json +1 -1
  60. package/dist/chunk-KGP6ALIU.js.map +0 -1
  61. package/dist/chunk-XEGZ3S2Q.js.map +0 -1
  62. /package/dist/{chunk-5C5UCILO.js.map → chunk-WWUG2QXF.js.map} +0 -0
  63. /package/dist/{chunk-Z4CZRMNF.js.map → chunk-YIQ4RS4F.js.map} +0 -0
@@ -1,8 +1,8 @@
1
- import '../index-DgYkdNT9.cjs';
2
- export { A as App, g as AuthVerifier, B as BootRefused, h as CreateAppOptions, D as DECLARATION_REFUSAL, a as DeclarationRefused, E as EgressPolicy, j as EngineConfig, M as ModuleClients, R as RateLimiter, k as RequestDatabase, l as RouteEntry, m as RuntimeHooks, S as ScrubResult, n as SqlDriver, o as SqlTx, p as buildRouteTable, q as createApp, r as createLazyTransaction, s as createOps, t as createRequestDatabase, u as effectiveAuth, v as hostAllowed, w as installEgressFence, i as isDeclarationRefused, x as loadConfig, y as makeMemoryCache, z as matchRoute, F as quoteIdent, G as scrubSecrets, H as withTables } from '../index-CWGiJ2Up.cjs';
1
+ import '../index-DB_nW-AV.cjs';
2
+ export { A as App, g as AuthVerifier, B as BootRefused, h as CreateAppOptions, D as DECLARATION_REFUSAL, a as DeclarationRefused, E as EgressPolicy, j as EngineConfig, M as ModuleClients, R as RateLimiter, k as RequestDatabase, l as RouteEntry, m as RuntimeHooks, S as ScrubResult, n as SqlDriver, o as SqlTx, p as buildRouteTable, q as createApp, r as createLazyTransaction, s as createOps, t as createRequestDatabase, u as effectiveAuth, v as hostAllowed, w as installEgressFence, i as isDeclarationRefused, x as loadConfig, y as makeMemoryCache, z as matchRoute, F as quoteIdent, G as scrubSecrets, H as withTables } from '../index-DLveQoOf.cjs';
3
3
  import '../db/env.cjs';
4
4
  import '../stack.cjs';
5
5
  import 'zod';
6
6
  import 'node:async_hooks';
7
7
  import '../module-Dl1KFVtc.cjs';
8
- import '../registry-BcRYIQ-R.cjs';
8
+ import '../registry-DSTThhKf.cjs';
@@ -1,8 +1,8 @@
1
- import '../index-BgWnP07a.js';
2
- export { A as App, g as AuthVerifier, B as BootRefused, h as CreateAppOptions, D as DECLARATION_REFUSAL, a as DeclarationRefused, E as EgressPolicy, j as EngineConfig, M as ModuleClients, R as RateLimiter, k as RequestDatabase, l as RouteEntry, m as RuntimeHooks, S as ScrubResult, n as SqlDriver, o as SqlTx, p as buildRouteTable, q as createApp, r as createLazyTransaction, s as createOps, t as createRequestDatabase, u as effectiveAuth, v as hostAllowed, w as installEgressFence, i as isDeclarationRefused, x as loadConfig, y as makeMemoryCache, z as matchRoute, F as quoteIdent, G as scrubSecrets, H as withTables } from '../index-BILC9WwS.js';
1
+ import '../index-DMZG3kpo.js';
2
+ export { A as App, g as AuthVerifier, B as BootRefused, h as CreateAppOptions, D as DECLARATION_REFUSAL, a as DeclarationRefused, E as EgressPolicy, j as EngineConfig, M as ModuleClients, R as RateLimiter, k as RequestDatabase, l as RouteEntry, m as RuntimeHooks, S as ScrubResult, n as SqlDriver, o as SqlTx, p as buildRouteTable, q as createApp, r as createLazyTransaction, s as createOps, t as createRequestDatabase, u as effectiveAuth, v as hostAllowed, w as installEgressFence, i as isDeclarationRefused, x as loadConfig, y as makeMemoryCache, z as matchRoute, F as quoteIdent, G as scrubSecrets, H as withTables } from '../index-D-3duy8Y.js';
3
3
  import '../db/env.js';
4
4
  import '../stack.js';
5
5
  import 'zod';
6
6
  import 'node:async_hooks';
7
7
  import '../module-Dl1KFVtc.js';
8
- import '../registry-ClzjxIWy.js';
8
+ import '../registry-JjF5lcj4.js';
@@ -16,15 +16,15 @@ import {
16
16
  quoteIdent,
17
17
  scrubSecrets,
18
18
  withTables
19
- } from "../chunk-AZJIKCOR.js";
19
+ } from "../chunk-IKDONZ5D.js";
20
20
  import {
21
21
  DECLARATION_REFUSAL,
22
22
  DeclarationRefused,
23
23
  isDeclarationRefused
24
- } from "../chunk-5C5UCILO.js";
25
- import "../chunk-XEGZ3S2Q.js";
26
- import "../chunk-KGP6ALIU.js";
27
- import "../chunk-IXAX5CON.js";
24
+ } from "../chunk-WWUG2QXF.js";
25
+ import "../chunk-TVCCR6SO.js";
26
+ import "../chunk-XOX6RFPZ.js";
27
+ import "../chunk-SI4KGEM3.js";
28
28
  import "../chunk-BRLJOXWS.js";
29
29
  import "../chunk-KATPXCJ5.js";
30
30
  export {
@@ -1,6 +1,6 @@
1
- import { D as DBClient, b as DBOps, T as TxPlanBody, c as TxPlanResponse, A as AuthSpec, C as CacheClient, a as RuntimeServices, _ as __runWithRuntime, d as __requestALS } from './index-BgWnP07a.js';
1
+ import { D as DBClient, b as DBOps, T as TxPlanBody, c as TxPlanResponse, A as AuthSpec, C as CacheClient, a as RuntimeServices, _ as __runWithRuntime, d as __requestALS } from './index-DMZG3kpo.js';
2
2
  import { T as Token } from './module-Dl1KFVtc.js';
3
- import { R as RouteMeta } from './registry-ClzjxIWy.js';
3
+ import { R as RouteMeta } from './registry-JjF5lcj4.js';
4
4
 
5
5
  /**
6
6
  * engine/config.ts — settings from the environment, and the gate that refuses
@@ -28,16 +28,32 @@ interface User {
28
28
  /**
29
29
  * Whether that email address has been confirmed.
30
30
  *
31
- * Server-resolved from the verified user profile, not read from the JWT: a
32
- * token claim is only true as of when the token was minted, so a user who
33
- * verifies mid-session would keep reporting `false` until it expired.
31
+ * READ FROM THE VERIFIED TOKEN (`email_verified`), so it says what was true
32
+ * when that token was minted: a user who confirms their address mid-session
33
+ * keeps reporting `false` until their next one. {@link User.roles} is the
34
+ * deliberate exception — it is read from the table on every request.
35
+ *
36
+ * This paragraph used to claim the opposite ("server-resolved from the
37
+ * verified profile, not read from the JWT") and to describe a ~30-second
38
+ * profile cache. Measured 2026-09-04: the engine fills this field from
39
+ * `claims.email_verified` and no such cache exists anywhere in the runtime.
40
+ * Whether the claim or a profile read is the RIGHT source is a separate
41
+ * question and an open one — but the type may not answer it wrongly while it
42
+ * is open.
34
43
  *
35
44
  * `false` for a phone-only user (no address to confirm) and for any user who
36
- * has not clicked through yet. Before this field existed, answering "is this
37
- * address confirmed" in a handler cost an extra network round-trip PER
38
- * REQUEST for one boolean the runtime already had.
45
+ * has not clicked through yet.
39
46
  */
40
47
  emailVerified: boolean;
48
+ /**
49
+ * The caller's DATABASE role — the string RLS reads, not an application role.
50
+ *
51
+ * It is the literal `"authenticated"` for every signed-in caller and
52
+ * `"anon"` for every unauthenticated one, so `user.role === "admin"` is
53
+ * ALWAYS false and reads like a working check. Application roles are
54
+ * {@link User.roles}; this field says which Postgres role the request runs
55
+ * as, and that is the only question it answers.
56
+ */
41
57
  role: string;
42
58
  /**
43
59
  * The caller's APPLICATION roles, as assigned in `auth.user_roles`.
@@ -2241,7 +2257,7 @@ interface PalbaseFunctionsClient {
2241
2257
  invoke<T = unknown>(fnName: string, options?: PalbaseInvokeOptions): Promise<PalbaseResult<T>>;
2242
2258
  }
2243
2259
  /**
2244
- * Cross-user admin write surface, reached via `Flags.asService()`.
2260
+ * Cross-user admin write surface, reached via `Flags.$asService()`.
2245
2261
  *
2246
2262
  * These five methods set/clear overrides for an ARBITRARY user (named
2247
2263
  * explicitly), so they bypass the current-request-user scope that `Flags.*`
@@ -2249,10 +2265,10 @@ interface PalbaseFunctionsClient {
2249
2265
  * `Database` / `Database.$asService()` split: the privileged, cross-user path
2250
2266
  * is moved OFF the default surface so it is greppable and intentional — a
2251
2267
  * handler that calls `Flags.setOverrideForUser(...)` on the default surface is
2252
- * a compile error and must reach for `Flags.asService()` first.
2268
+ * a compile error and must reach for `Flags.$asService()` first.
2253
2269
  *
2254
2270
  * @example
2255
- * await Flags.asService().setOverrideForUser("user_123", "new_checkout", true);
2271
+ * await Flags.$asService().setOverrideForUser("user_123", "new_checkout", true);
2256
2272
  */
2257
2273
  interface PalbaseFlagsServiceClient {
2258
2274
  /**
@@ -2315,7 +2331,7 @@ interface PalbaseFlagsClient {
2315
2331
  * user until cleared.
2316
2332
  *
2317
2333
  * Errors when there is no signed-in user (an anonymous request); reach for
2318
- * `Flags.asService().setOverrideForUser(userId, key, value)` to write a flag
2334
+ * `Flags.$asService().setOverrideForUser(userId, key, value)` to write a flag
2319
2335
  * for an arbitrary (cross-user) target.
2320
2336
  *
2321
2337
  * @example
@@ -2325,8 +2341,12 @@ interface PalbaseFlagsClient {
2325
2341
  /**
2326
2342
  * Return the cross-user admin write surface ({@link PalbaseFlagsServiceClient}).
2327
2343
  * Use sparingly and explicitly — the default `Flags.setOverride(...)` path is
2328
- * bound to the current request user; `asService()` is how you write a flag
2329
- * override for an ARBITRARY user. Mirrors `Database.$asService()`.
2344
+ * bound to the current request user; this is how you write a flag override for
2345
+ * an ARBITRARY user. Mirrors `Database.$asService()`.
2346
+ *
2347
+ * NOT the name a handler writes: the ambient `Flags` singleton OMITS this
2348
+ * member and exposes `$asService()` instead — `Flags.asService()` still
2349
+ * type-checks there and THROWS, naming its replacement (H1).
2330
2350
  */
2331
2351
  asService(): PalbaseFlagsServiceClient;
2332
2352
  }
@@ -2603,7 +2623,7 @@ interface PalbaseLinksClient {
2603
2623
  * \@Controller("/todos")
2604
2624
  * export default class TodosController {
2605
2625
  * \@Post("") create(\@Body(CreateTodoBody) body: CreateTodoBody): unknown {
2606
- * return Database.insert("todos", { title: body.title });
2626
+ * return Database.public.todos.insert({ title: body.title });
2607
2627
  * }
2608
2628
  * }
2609
2629
  *
@@ -2688,11 +2708,6 @@ declare function __setRuntime(services: RuntimeServices): void;
2688
2708
  * The persistent worker calls this once per request so concurrent requests
2689
2709
  * never share a services slot. NOT part of the public author-facing API. */
2690
2710
  declare function __runWithRuntime<T>(services: RuntimeServices, fn: () => T): T;
2691
- /** Read the live clients, throwing if accessed outside a request scope.
2692
- *
2693
- * Resolves the ALS store first (persistent server, per-request), then the
2694
- * process-global fallback (dev-server / tests). NOT part of the public
2695
- * author-facing API — used by the runtime and the singleton Proxies. */
2696
2711
  declare function __getRuntime(): RuntimeServices;
2697
2712
  /** A lifecycle hook. Sync or async; the runtime awaits what it returns. */
2698
2713
  type LifecycleHook = () => void | Promise<void>;
@@ -2805,7 +2820,7 @@ declare const Database: EnvTypedDatabase;
2805
2820
  /** Firestore-like document client (PalDocs). */
2806
2821
  declare const Documents: PalbaseDocsClient;
2807
2822
  /**
2808
- * `buckets.<name>` — the storage twin of `Database.tables.<name>`, and the same
2823
+ * `buckets.<name>` — the storage twin of `Database.public.<name>`, and the same
2809
2824
  * mechanism: `config/storage.ts` generates a `Buckets` augmentation into
2810
2825
  * `palbase-env.d.ts`, so a bucket name is a property with no import and no
2811
2826
  * generic, and a typo is a compile error.
@@ -2869,21 +2884,46 @@ declare const Notifications: PalbaseNotificationsClient;
2869
2884
  /**
2870
2885
  * Feature flags.
2871
2886
  *
2872
- * Mirrors the `Database` / `Database.asService()` model. The default surface is
2887
+ * Mirrors the `Database` / `Database.$asService()` model. The default surface is
2873
2888
  * RLS-equivalent for flags: reads resolve against the CURRENT request user and
2874
2889
  * `Flags.setOverride(key, value)` writes an override for that same signed-in
2875
2890
  * user (no userId argument, no admin power). Cross-user admin writes
2876
- * (`setOverrideForUser`, …) live behind `Flags.asService()` — explicit and
2877
- * greppable, just like `Database.asService()`.
2891
+ * (`setOverrideForUser`, …) live behind `Flags.$asService()` — explicit and
2892
+ * greppable, just like `Database.$asService()`.
2878
2893
  *
2879
2894
  * @example
2880
2895
  * import { Flags } from "@palbase/backend";
2881
2896
  *
2882
2897
  * if (await Flags.isEnabled("new_checkout")) { ... } // current user
2883
2898
  * await Flags.setOverride("new_checkout", true); // current user
2884
- * await Flags.asService().setOverrideForUser("u_9", "x", true); // cross-user
2899
+ * await Flags.$asService().setOverrideForUser("u_9", "x", true); // cross-user
2900
+ */
2901
+ /**
2902
+ * The ambient `Flags` singleton's own surface — NOT the raw client's.
2903
+ *
2904
+ * MEASURED, as a consumer, against the built tarball: annotating the singleton
2905
+ * as `PalbaseFlagsClient` ERASED the `$asService` that `Object.assign` adds, so
2906
+ * `Flags.$asService()` did not exist for anyone outside this package —
2907
+ *
2908
+ * TS2551: Property '$asService' does not exist on type 'PalbaseFlagsClient'.
2909
+ * Did you mean 'asService'?
2910
+ *
2911
+ * — while the only path that DID compile (`asService()`) throws by design. The
2912
+ * feature was written and unreachable, which is the defect class this surface
2913
+ * exists to remove.
2914
+ *
2915
+ * `Database` never had the problem because its singleton carries its OWN type
2916
+ * (`EnvTypedDatabase`) rather than the raw client's (`DBClient`). This is that,
2917
+ * for `Flags`. The raw `PalbaseFlagsClient.asService()` is untouched (FR-023) —
2918
+ * it is the seam this forwards to.
2885
2919
  */
2886
- declare const Flags: PalbaseFlagsClient;
2920
+ type PalbaseFlagsAmbient = Omit<PalbaseFlagsClient, "asService"> & {
2921
+ /** RLS'i aşan, kullanıcılar arası yazma yüzeyi. */
2922
+ $asService(): PalbaseFlagsServiceClient;
2923
+ /** 31.0.0 öncesinin adı — SESSİZCE çalışmaz, yerini söyleyerek fırlatır. */
2924
+ asService(): never;
2925
+ };
2926
+ declare const Flags: PalbaseFlagsAmbient;
2887
2927
  /**
2888
2928
  * The Realtime broadcast singleton for the current request scope. Backend-side
2889
2929
  * Realtime is BROADCAST-ONLY (a stateless handler can push but not subscribe —
@@ -3454,8 +3494,20 @@ type WithRowOf<Rels, R extends keyof Rels, Q> = Rels[R] extends {
3454
3494
  * özdeş değil" hâle getirirdi ve tam eşitlik iddiaları kırılırdı. Ölçüldü:
3455
3495
  * `toEqualTypeOf` dört yerde düştü.
3456
3496
  */
3457
- type WithResult<Rels, W> = [keyof W] extends [never] ? unknown : {
3458
- [R in keyof W & keyof Rels]: WithRowOf<Rels, R, W[R]>;
3497
+ /**
3498
+ * Yalnız GERÇEKTEN istenen dallar. `undefined extends W[R]` olan anahtarlar
3499
+ * düşüyor — ve bu, `ReturnType<typeof t.findMany>` yazan herkes için kritik:
3500
+ * generic bir imzada `ReturnType` tip parametrelerini VARSAYILANLA değil
3501
+ * KISITLA örnekliyor, yani `W = WithInput<Rels>` oluyor ve her ilişki
3502
+ * (`?:` ile, dolayısıyla `| undefined`) zorunlu bir alana dönüşüyordu.
3503
+ * Gerçek projede ölçüldü: `type MemberRow = Awaited<ReturnType<…findMany>>[number]`
3504
+ * 32 ilişkiyi birden ister hâle geldi ve 4 yerde kırıldı.
3505
+ */
3506
+ type RequestedWith<W> = {
3507
+ [R in keyof W]-?: undefined extends W[R] ? never : R;
3508
+ }[keyof W];
3509
+ type WithResult<Rels, W> = [RequestedWith<W>] extends [never] ? unknown : {
3510
+ [R in RequestedWith<W> & keyof Rels]: WithRowOf<Rels, R, W[R]>;
3459
3511
  };
3460
3512
  type QueryInput<Row, K extends keyof Row = keyof Row, Rels = unknown> = {
3461
3513
  /**
@@ -3987,12 +4039,17 @@ type EnvServiceDatabase = EnvSchemaSurface & {
3987
4039
  }): Promise<Materialized<T>>;
3988
4040
  };
3989
4041
  /**
3990
- * The typed-by-default Database surface: the raw string-keyed `DBClient` ops
3991
- * PLUS a `tables` map typed against the project's generated `palbase-env.d.ts`,
3992
- * a `transaction` that runs a whole plan in one request, and `asService()` for
4042
+ * The typed-by-default Database surface: the `$`-prefixed string-keyed
4043
+ * `DBClient` ops PLUS one property per schema — `public` and every schema the
4044
+ * project declares typed against the generated `palbase-env.d.ts`, a
4045
+ * `$transaction` that runs a whole plan in one request, and `$asService()` for
3993
4046
  * the explicit RLS-bypass sibling.
3994
4047
  *
3995
- * The low-level `txPlan` op is deliberately NOT re-exposed here: `transaction`
4048
+ * NOT `tables`: that intermediate layer was removed (FR-001) and this sentence
4049
+ * outlived it by two majors, teaching a spelling that is TS2339. Pinned in
4050
+ * `src/database-ambient.test-d.ts`.
4051
+ *
4052
+ * The low-level `txPlan` op is deliberately NOT re-exposed here: `$transaction`
3996
4053
  * is the surface, and a hand-built plan would bypass the ref/guard machinery
3997
4054
  * that makes one safe to write.
3998
4055
  */
@@ -1,6 +1,6 @@
1
- import { D as DBClient, b as DBOps, T as TxPlanBody, c as TxPlanResponse, A as AuthSpec, C as CacheClient, a as RuntimeServices, _ as __runWithRuntime, d as __requestALS } from './index-DgYkdNT9.cjs';
1
+ import { D as DBClient, b as DBOps, T as TxPlanBody, c as TxPlanResponse, A as AuthSpec, C as CacheClient, a as RuntimeServices, _ as __runWithRuntime, d as __requestALS } from './index-DB_nW-AV.cjs';
2
2
  import { T as Token } from './module-Dl1KFVtc.cjs';
3
- import { R as RouteMeta } from './registry-BcRYIQ-R.cjs';
3
+ import { R as RouteMeta } from './registry-DSTThhKf.cjs';
4
4
 
5
5
  /**
6
6
  * engine/config.ts — settings from the environment, and the gate that refuses
@@ -28,16 +28,32 @@ interface User {
28
28
  /**
29
29
  * Whether that email address has been confirmed.
30
30
  *
31
- * Server-resolved from the verified user profile, not read from the JWT: a
32
- * token claim is only true as of when the token was minted, so a user who
33
- * verifies mid-session would keep reporting `false` until it expired.
31
+ * READ FROM THE VERIFIED TOKEN (`email_verified`), so it says what was true
32
+ * when that token was minted: a user who confirms their address mid-session
33
+ * keeps reporting `false` until their next one. {@link User.roles} is the
34
+ * deliberate exception — it is read from the table on every request.
35
+ *
36
+ * This paragraph used to claim the opposite ("server-resolved from the
37
+ * verified profile, not read from the JWT") and to describe a ~30-second
38
+ * profile cache. Measured 2026-09-04: the engine fills this field from
39
+ * `claims.email_verified` and no such cache exists anywhere in the runtime.
40
+ * Whether the claim or a profile read is the RIGHT source is a separate
41
+ * question and an open one — but the type may not answer it wrongly while it
42
+ * is open.
34
43
  *
35
44
  * `false` for a phone-only user (no address to confirm) and for any user who
36
- * has not clicked through yet. Before this field existed, answering "is this
37
- * address confirmed" in a handler cost an extra network round-trip PER
38
- * REQUEST for one boolean the runtime already had.
45
+ * has not clicked through yet.
39
46
  */
40
47
  emailVerified: boolean;
48
+ /**
49
+ * The caller's DATABASE role — the string RLS reads, not an application role.
50
+ *
51
+ * It is the literal `"authenticated"` for every signed-in caller and
52
+ * `"anon"` for every unauthenticated one, so `user.role === "admin"` is
53
+ * ALWAYS false and reads like a working check. Application roles are
54
+ * {@link User.roles}; this field says which Postgres role the request runs
55
+ * as, and that is the only question it answers.
56
+ */
41
57
  role: string;
42
58
  /**
43
59
  * The caller's APPLICATION roles, as assigned in `auth.user_roles`.
@@ -2241,7 +2257,7 @@ interface PalbaseFunctionsClient {
2241
2257
  invoke<T = unknown>(fnName: string, options?: PalbaseInvokeOptions): Promise<PalbaseResult<T>>;
2242
2258
  }
2243
2259
  /**
2244
- * Cross-user admin write surface, reached via `Flags.asService()`.
2260
+ * Cross-user admin write surface, reached via `Flags.$asService()`.
2245
2261
  *
2246
2262
  * These five methods set/clear overrides for an ARBITRARY user (named
2247
2263
  * explicitly), so they bypass the current-request-user scope that `Flags.*`
@@ -2249,10 +2265,10 @@ interface PalbaseFunctionsClient {
2249
2265
  * `Database` / `Database.$asService()` split: the privileged, cross-user path
2250
2266
  * is moved OFF the default surface so it is greppable and intentional — a
2251
2267
  * handler that calls `Flags.setOverrideForUser(...)` on the default surface is
2252
- * a compile error and must reach for `Flags.asService()` first.
2268
+ * a compile error and must reach for `Flags.$asService()` first.
2253
2269
  *
2254
2270
  * @example
2255
- * await Flags.asService().setOverrideForUser("user_123", "new_checkout", true);
2271
+ * await Flags.$asService().setOverrideForUser("user_123", "new_checkout", true);
2256
2272
  */
2257
2273
  interface PalbaseFlagsServiceClient {
2258
2274
  /**
@@ -2315,7 +2331,7 @@ interface PalbaseFlagsClient {
2315
2331
  * user until cleared.
2316
2332
  *
2317
2333
  * Errors when there is no signed-in user (an anonymous request); reach for
2318
- * `Flags.asService().setOverrideForUser(userId, key, value)` to write a flag
2334
+ * `Flags.$asService().setOverrideForUser(userId, key, value)` to write a flag
2319
2335
  * for an arbitrary (cross-user) target.
2320
2336
  *
2321
2337
  * @example
@@ -2325,8 +2341,12 @@ interface PalbaseFlagsClient {
2325
2341
  /**
2326
2342
  * Return the cross-user admin write surface ({@link PalbaseFlagsServiceClient}).
2327
2343
  * Use sparingly and explicitly — the default `Flags.setOverride(...)` path is
2328
- * bound to the current request user; `asService()` is how you write a flag
2329
- * override for an ARBITRARY user. Mirrors `Database.$asService()`.
2344
+ * bound to the current request user; this is how you write a flag override for
2345
+ * an ARBITRARY user. Mirrors `Database.$asService()`.
2346
+ *
2347
+ * NOT the name a handler writes: the ambient `Flags` singleton OMITS this
2348
+ * member and exposes `$asService()` instead — `Flags.asService()` still
2349
+ * type-checks there and THROWS, naming its replacement (H1).
2330
2350
  */
2331
2351
  asService(): PalbaseFlagsServiceClient;
2332
2352
  }
@@ -2603,7 +2623,7 @@ interface PalbaseLinksClient {
2603
2623
  * \@Controller("/todos")
2604
2624
  * export default class TodosController {
2605
2625
  * \@Post("") create(\@Body(CreateTodoBody) body: CreateTodoBody): unknown {
2606
- * return Database.insert("todos", { title: body.title });
2626
+ * return Database.public.todos.insert({ title: body.title });
2607
2627
  * }
2608
2628
  * }
2609
2629
  *
@@ -2688,11 +2708,6 @@ declare function __setRuntime(services: RuntimeServices): void;
2688
2708
  * The persistent worker calls this once per request so concurrent requests
2689
2709
  * never share a services slot. NOT part of the public author-facing API. */
2690
2710
  declare function __runWithRuntime<T>(services: RuntimeServices, fn: () => T): T;
2691
- /** Read the live clients, throwing if accessed outside a request scope.
2692
- *
2693
- * Resolves the ALS store first (persistent server, per-request), then the
2694
- * process-global fallback (dev-server / tests). NOT part of the public
2695
- * author-facing API — used by the runtime and the singleton Proxies. */
2696
2711
  declare function __getRuntime(): RuntimeServices;
2697
2712
  /** A lifecycle hook. Sync or async; the runtime awaits what it returns. */
2698
2713
  type LifecycleHook = () => void | Promise<void>;
@@ -2805,7 +2820,7 @@ declare const Database: EnvTypedDatabase;
2805
2820
  /** Firestore-like document client (PalDocs). */
2806
2821
  declare const Documents: PalbaseDocsClient;
2807
2822
  /**
2808
- * `buckets.<name>` — the storage twin of `Database.tables.<name>`, and the same
2823
+ * `buckets.<name>` — the storage twin of `Database.public.<name>`, and the same
2809
2824
  * mechanism: `config/storage.ts` generates a `Buckets` augmentation into
2810
2825
  * `palbase-env.d.ts`, so a bucket name is a property with no import and no
2811
2826
  * generic, and a typo is a compile error.
@@ -2869,21 +2884,46 @@ declare const Notifications: PalbaseNotificationsClient;
2869
2884
  /**
2870
2885
  * Feature flags.
2871
2886
  *
2872
- * Mirrors the `Database` / `Database.asService()` model. The default surface is
2887
+ * Mirrors the `Database` / `Database.$asService()` model. The default surface is
2873
2888
  * RLS-equivalent for flags: reads resolve against the CURRENT request user and
2874
2889
  * `Flags.setOverride(key, value)` writes an override for that same signed-in
2875
2890
  * user (no userId argument, no admin power). Cross-user admin writes
2876
- * (`setOverrideForUser`, …) live behind `Flags.asService()` — explicit and
2877
- * greppable, just like `Database.asService()`.
2891
+ * (`setOverrideForUser`, …) live behind `Flags.$asService()` — explicit and
2892
+ * greppable, just like `Database.$asService()`.
2878
2893
  *
2879
2894
  * @example
2880
2895
  * import { Flags } from "@palbase/backend";
2881
2896
  *
2882
2897
  * if (await Flags.isEnabled("new_checkout")) { ... } // current user
2883
2898
  * await Flags.setOverride("new_checkout", true); // current user
2884
- * await Flags.asService().setOverrideForUser("u_9", "x", true); // cross-user
2899
+ * await Flags.$asService().setOverrideForUser("u_9", "x", true); // cross-user
2900
+ */
2901
+ /**
2902
+ * The ambient `Flags` singleton's own surface — NOT the raw client's.
2903
+ *
2904
+ * MEASURED, as a consumer, against the built tarball: annotating the singleton
2905
+ * as `PalbaseFlagsClient` ERASED the `$asService` that `Object.assign` adds, so
2906
+ * `Flags.$asService()` did not exist for anyone outside this package —
2907
+ *
2908
+ * TS2551: Property '$asService' does not exist on type 'PalbaseFlagsClient'.
2909
+ * Did you mean 'asService'?
2910
+ *
2911
+ * — while the only path that DID compile (`asService()`) throws by design. The
2912
+ * feature was written and unreachable, which is the defect class this surface
2913
+ * exists to remove.
2914
+ *
2915
+ * `Database` never had the problem because its singleton carries its OWN type
2916
+ * (`EnvTypedDatabase`) rather than the raw client's (`DBClient`). This is that,
2917
+ * for `Flags`. The raw `PalbaseFlagsClient.asService()` is untouched (FR-023) —
2918
+ * it is the seam this forwards to.
2885
2919
  */
2886
- declare const Flags: PalbaseFlagsClient;
2920
+ type PalbaseFlagsAmbient = Omit<PalbaseFlagsClient, "asService"> & {
2921
+ /** RLS'i aşan, kullanıcılar arası yazma yüzeyi. */
2922
+ $asService(): PalbaseFlagsServiceClient;
2923
+ /** 31.0.0 öncesinin adı — SESSİZCE çalışmaz, yerini söyleyerek fırlatır. */
2924
+ asService(): never;
2925
+ };
2926
+ declare const Flags: PalbaseFlagsAmbient;
2887
2927
  /**
2888
2928
  * The Realtime broadcast singleton for the current request scope. Backend-side
2889
2929
  * Realtime is BROADCAST-ONLY (a stateless handler can push but not subscribe —
@@ -3454,8 +3494,20 @@ type WithRowOf<Rels, R extends keyof Rels, Q> = Rels[R] extends {
3454
3494
  * özdeş değil" hâle getirirdi ve tam eşitlik iddiaları kırılırdı. Ölçüldü:
3455
3495
  * `toEqualTypeOf` dört yerde düştü.
3456
3496
  */
3457
- type WithResult<Rels, W> = [keyof W] extends [never] ? unknown : {
3458
- [R in keyof W & keyof Rels]: WithRowOf<Rels, R, W[R]>;
3497
+ /**
3498
+ * Yalnız GERÇEKTEN istenen dallar. `undefined extends W[R]` olan anahtarlar
3499
+ * düşüyor — ve bu, `ReturnType<typeof t.findMany>` yazan herkes için kritik:
3500
+ * generic bir imzada `ReturnType` tip parametrelerini VARSAYILANLA değil
3501
+ * KISITLA örnekliyor, yani `W = WithInput<Rels>` oluyor ve her ilişki
3502
+ * (`?:` ile, dolayısıyla `| undefined`) zorunlu bir alana dönüşüyordu.
3503
+ * Gerçek projede ölçüldü: `type MemberRow = Awaited<ReturnType<…findMany>>[number]`
3504
+ * 32 ilişkiyi birden ister hâle geldi ve 4 yerde kırıldı.
3505
+ */
3506
+ type RequestedWith<W> = {
3507
+ [R in keyof W]-?: undefined extends W[R] ? never : R;
3508
+ }[keyof W];
3509
+ type WithResult<Rels, W> = [RequestedWith<W>] extends [never] ? unknown : {
3510
+ [R in RequestedWith<W> & keyof Rels]: WithRowOf<Rels, R, W[R]>;
3459
3511
  };
3460
3512
  type QueryInput<Row, K extends keyof Row = keyof Row, Rels = unknown> = {
3461
3513
  /**
@@ -3987,12 +4039,17 @@ type EnvServiceDatabase = EnvSchemaSurface & {
3987
4039
  }): Promise<Materialized<T>>;
3988
4040
  };
3989
4041
  /**
3990
- * The typed-by-default Database surface: the raw string-keyed `DBClient` ops
3991
- * PLUS a `tables` map typed against the project's generated `palbase-env.d.ts`,
3992
- * a `transaction` that runs a whole plan in one request, and `asService()` for
4042
+ * The typed-by-default Database surface: the `$`-prefixed string-keyed
4043
+ * `DBClient` ops PLUS one property per schema — `public` and every schema the
4044
+ * project declares typed against the generated `palbase-env.d.ts`, a
4045
+ * `$transaction` that runs a whole plan in one request, and `$asService()` for
3993
4046
  * the explicit RLS-bypass sibling.
3994
4047
  *
3995
- * The low-level `txPlan` op is deliberately NOT re-exposed here: `transaction`
4048
+ * NOT `tables`: that intermediate layer was removed (FR-001) and this sentence
4049
+ * outlived it by two majors, teaching a spelling that is TS2339. Pinned in
4050
+ * `src/database-ambient.test-d.ts`.
4051
+ *
4052
+ * The low-level `txPlan` op is deliberately NOT re-exposed here: `$transaction`
3996
4053
  * is the surface, and a hand-built plan would bypass the ref/guard machinery
3997
4054
  * that makes one safe to write.
3998
4055
  */
package/dist/index.cjs CHANGED
@@ -1286,9 +1286,29 @@ function __runWithRuntime(services, fn) {
1286
1286
  }, fn);
1287
1287
  }
1288
1288
  __name(__runWithRuntime, "__runWithRuntime");
1289
+ var REQUIRED_SERVICES = [
1290
+ "Database",
1291
+ "Auth",
1292
+ "Secrets",
1293
+ "Documents",
1294
+ "Storage",
1295
+ "Cache",
1296
+ "Log",
1297
+ "Notifications",
1298
+ "Flags",
1299
+ "Realtime"
1300
+ ];
1301
+ function refuseIncompleteBox(services) {
1302
+ const missing = REQUIRED_SERVICES.filter((k) => services[k] === void 0);
1303
+ if (missing.length > 0) {
1304
+ throw new Error(`the request scope is missing ${missing.join(", ")} \u2014 this box was most likely written by a DIFFERENT @palbase/backend major sharing the same process (the request store is a process-wide well-known Symbol, deliberately, so two bundles of ONE version can share it). Align the versions, or pass every service when building the scope yourself.`);
1305
+ }
1306
+ return services;
1307
+ }
1308
+ __name(refuseIncompleteBox, "refuseIncompleteBox");
1289
1309
  function __getRuntime() {
1290
1310
  const scoped = __requestALS.getStore();
1291
- if (scoped) return scoped.runtime;
1311
+ if (scoped) return refuseIncompleteBox(scoped.runtime);
1292
1312
  if (runtime === null) {
1293
1313
  throw new Error("Palbase services accessed outside a request scope. The Database/Documents/\u2026 singletons are only available inside an endpoint handler (or after the runtime has called __runWithRuntime / __setRuntime).");
1294
1314
  }
@@ -1430,7 +1450,12 @@ function makeTypedSurface(raw2) {
1430
1450
  $findById: /* @__PURE__ */ __name((table, id) => raw2.findById(table, id), "$findById"),
1431
1451
  $findMany: /* @__PURE__ */ __name((table, query, opts) => raw2.findMany(table, query, opts), "$findMany"),
1432
1452
  $put: /* @__PURE__ */ __name((table, data, opts) => raw2.put(table, data, opts), "$put"),
1433
- $updateMany: /* @__PURE__ */ __name((table, where, set) => raw2.updateMany(table, where, set), "$updateMany"),
1453
+ // `opts` İLETİLİR. Düşürüldüğü sürece `Database.$updateMany(t, w, s,
1454
+ // { returning: false })` sessizce SATIRLARI döndürüyordu, sayıyı değil —
1455
+ // ve `if (n === 0) throw new Conflict(...)` guard'ı HİÇ çalışmıyordu,
1456
+ // çünkü `[] === 0` yanlıştır. Komşu forwarder'lar (`$findMany`, `$put`)
1457
+ // kendi opsiyonlarını zaten iletiyordu; bu biri unutulmuştu.
1458
+ $updateMany: /* @__PURE__ */ __name((table, where, set, opts) => raw2.updateMany(table, where, set, opts), "$updateMany"),
1434
1459
  $deleteMany: /* @__PURE__ */ __name((table, where) => raw2.deleteMany(table, where), "$deleteMany"),
1435
1460
  $count: /* @__PURE__ */ __name((table, where) => raw2.count(table, where), "$count"),
1436
1461
  $search: /* @__PURE__ */ __name((table, params) => raw2.search(table, params), "$search"),
@@ -1561,13 +1586,13 @@ var Flags = Object.assign({
1561
1586
  * Lazily resolve the runtime's cross-user sibling on each call. We do NOT
1562
1587
  * cache it: `rawFlags.asService()` reads the CURRENT request scope through
1563
1588
  * the runtime proxy, so caching would leak one request's sibling into
1564
- * another concurrent request. Mirrors `Database.asService()`.
1589
+ * another concurrent request. Mirrors `Database.$asService()`.
1565
1590
  */
1566
1591
  $asService() {
1567
1592
  return rawFlags.asService();
1568
1593
  },
1569
1594
  /**
1570
- * The name this member carried before 31.0.0.
1595
+ * The name this member carried before 32.0.0.
1571
1596
  *
1572
1597
  * It does NOT work silently. A retired member that quietly keeps returning
1573
1598
  * is how a rename becomes a mystery: the old call site goes on compiling,
@@ -1577,7 +1602,7 @@ var Flags = Object.assign({
1577
1602
  * Database.$asService()" while the promise went unkept.
1578
1603
  */
1579
1604
  asService() {
1580
- throw new Error("Flags.asService() was renamed to Flags.$asService() in 31.0.0 \u2014 system members carry the `$` prefix, like Database.$asService().");
1605
+ throw new Error("Flags.asService() was renamed to Flags.$asService() in 32.0.0 \u2014 system members carry the `$` prefix, like Database.$asService().");
1581
1606
  }
1582
1607
  });
1583
1608
  var Realtime = makeServiceProxy("Realtime");
@@ -3317,15 +3342,27 @@ var CODECS = {
3317
3342
  // not representable and the author wants `asDecimal()` instead.
3318
3343
  number: {
3319
3344
  fromDb: /* @__PURE__ */ __name((v) => Number(v), "fromDb"),
3320
- toDb: /* @__PURE__ */ __name((v) => String(v), "toDb"),
3345
+ // YAZMA YOLUNDA SESSİZ BOZULMA YOK. `toDb(9007199254740993)` bir zamanlar
3346
+ // "9007199254740992" yazıyordu: uygulamanın tuttuğundan BAŞKA bir sayı
3347
+ // kolona gidiyordu ve hiçbir şey söylemiyordu. `asNumber()`'ın JSDoc'u
3348
+ // "2^53'ün altında güvenli" diyordu ama bunu uygulayan bir şey yoktu.
3349
+ toDb: /* @__PURE__ */ __name((v) => {
3350
+ if (typeof v === "number" && !Number.isSafeInteger(v)) {
3351
+ throw new Error(`asNumber() cannot write ${v}: it is outside the safe integer range and would be stored as a different number. Declare .asDecimal() and hand this column a string.`);
3352
+ }
3353
+ return String(v);
3354
+ }, "toDb"),
3321
3355
  tsType: "number"
3322
3356
  },
3323
3357
  // Exact-precision column → the string Postgres sent. It exists so the
3324
3358
  // DECLARATION is explicit: "this column is a decimal I handle as text",
3325
3359
  // rather than the ABSENCE of a declaration meaning the same thing by default.
3360
+ // NULL güvencesi ÇİFTİN KENDİSİNDE. Motor NULL'ı zaten atlıyor (üç ayrı
3361
+ // yerde), ama bir değişmez, onu koruyan üç `if`'in DIŞINDA yazılmalı: biri
3362
+ // düşerse `String(null)` kolonu "null" dizesine çevirirdi.
3326
3363
  decimal: {
3327
- fromDb: /* @__PURE__ */ __name((v) => String(v), "fromDb"),
3328
- toDb: /* @__PURE__ */ __name((v) => String(v), "toDb"),
3364
+ fromDb: /* @__PURE__ */ __name((v) => v === null || v === void 0 ? v : String(v), "fromDb"),
3365
+ toDb: /* @__PURE__ */ __name((v) => v === null || v === void 0 ? v : String(v), "toDb"),
3329
3366
  tsType: "string"
3330
3367
  }
3331
3368
  };
@@ -3558,12 +3595,22 @@ var ColumnBuilder = class _ColumnBuilder {
3558
3595
  if (this._def.transform !== void 0) {
3559
3596
  throw new Error(`.${method}() and .transform() are two ways to say the same thing on one column \u2014 keep .${method}().`);
3560
3597
  }
3561
- this._def.codec = codec;
3562
- return new _ColumnBuilder(this._def.type, this._def);
3598
+ if (this._def.codec !== void 0) {
3599
+ throw new Error(`column already declares .as${this._def.codec === "number" ? "Number" : "Decimal"}(); a column has ONE codec.`);
3600
+ }
3601
+ return new _ColumnBuilder(this._def.type, {
3602
+ ...this._def,
3603
+ codec
3604
+ });
3563
3605
  }
3564
3606
  transform(fns) {
3565
- this._def.transform = fns;
3566
- return new _ColumnBuilder(this._def.type, this._def);
3607
+ if (this._def.codec !== void 0) {
3608
+ throw new Error(`column already declares a named codec; .transform() and .as*() are two ways to say the same thing \u2014 keep the codec.`);
3609
+ }
3610
+ return new _ColumnBuilder(this._def.type, {
3611
+ ...this._def,
3612
+ transform: fns
3613
+ });
3567
3614
  }
3568
3615
  };
3569
3616
  function uuid() {